469 lines
12 KiB
Python
469 lines
12 KiB
Python
from re import (
|
|
compile,
|
|
match
|
|
)
|
|
|
|
from mapbox.errors import (
|
|
ValidationError,
|
|
InvalidZoomError,
|
|
InvalidColumnError,
|
|
InvalidRowError,
|
|
InvalidFileFormatError,
|
|
InvalidPeriodError,
|
|
InvalidFeatureFormatError,
|
|
InvalidMarkerNameError,
|
|
InvalidLabelError,
|
|
InvalidColorError
|
|
)
|
|
|
|
from mapbox.services.base import Service
|
|
|
|
from dateutil.parser import parse
|
|
|
|
from uritemplate import URITemplate
|
|
|
|
class Maps(Service):
|
|
"""Access to Maps API V4
|
|
|
|
Attributes
|
|
----------
|
|
api_name : str
|
|
The API's name.
|
|
|
|
api_version : str
|
|
The API's version number.
|
|
|
|
valid_file_formats : list
|
|
The possible values for file_format.
|
|
|
|
valid_feature_formats : list
|
|
The possible values for feature_format.
|
|
|
|
valid_marker_names : list
|
|
The possible values for marker_name.
|
|
|
|
base_uri : str
|
|
The API's base URI, currently https://api.mapbox.com/v4.
|
|
"""
|
|
|
|
api_name = "maps"
|
|
|
|
api_version = "v4"
|
|
|
|
valid_file_formats = [
|
|
"grid.json",
|
|
"mvt",
|
|
"png",
|
|
"png32",
|
|
"png64",
|
|
"png128",
|
|
"png256",
|
|
"jpg70",
|
|
"jpg80",
|
|
"jpg90"
|
|
]
|
|
|
|
valid_feature_formats = [
|
|
"json",
|
|
"kml"
|
|
]
|
|
|
|
valid_marker_names = [
|
|
"pin-s",
|
|
"pin-l"
|
|
]
|
|
|
|
@property
|
|
def base_uri(self):
|
|
"""Forms base URI."""
|
|
|
|
return "https://{}/{}".format(self.host, self.api_version)
|
|
|
|
def _validate_z(self, z):
|
|
"""Validates z (zoom), raising error if invalid."""
|
|
|
|
if (z < 0) or (z > 20):
|
|
raise InvalidZoomError(
|
|
"{} is not a valid value for z (zoom)".format(z)
|
|
)
|
|
|
|
return z
|
|
|
|
def _validate_x(self, x, z):
|
|
"""Validates x (column), raising error if invalid."""
|
|
|
|
if (x < 0) or (x > ((2**z) - 1)):
|
|
raise InvalidColumnError(
|
|
"{} is not a valid value for x (column)".format(x)
|
|
)
|
|
|
|
return x
|
|
|
|
def _validate_y(self, y, z):
|
|
"""Validates y (row), raising error if invalid."""
|
|
|
|
if (y < 0) or (y > ((2**z) - 1)):
|
|
raise InvalidRowError(
|
|
"{} is not a valid value for y (row)".format(y)
|
|
)
|
|
|
|
return y
|
|
|
|
def _validate_retina(self, retina):
|
|
"""Validates retina."""
|
|
|
|
if retina:
|
|
retina = "@2x"
|
|
else:
|
|
retina = ""
|
|
|
|
return retina
|
|
|
|
def _validate_file_format(self, file_format):
|
|
"""Validates file format, raising error if invalid."""
|
|
|
|
if file_format not in self.valid_file_formats:
|
|
raise InvalidFileFormatError(
|
|
"{} is not a valid file format".format(file_format)
|
|
)
|
|
|
|
return file_format
|
|
|
|
def _validate_timestamp(self, timestamp):
|
|
"""Validates timestamp, raising error if invalid."""
|
|
|
|
try:
|
|
parse(timestamp)
|
|
except:
|
|
raise InvalidPeriodError(
|
|
"{} is not an ISO-formatted string".format(timestamp)
|
|
)
|
|
|
|
return timestamp
|
|
|
|
def _validate_feature_format(self, feature_format):
|
|
"""Validates feature format, raising error if invalid."""
|
|
|
|
if feature_format not in self.valid_feature_formats:
|
|
raise InvalidFeatureFormatError(
|
|
"{} is not a valid feature format".format(feature_format)
|
|
)
|
|
|
|
return feature_format
|
|
|
|
def _validate_marker_name(self, marker_name):
|
|
"""Validates marker name, raising error if invalid."""
|
|
|
|
if marker_name not in self.valid_marker_names:
|
|
raise InvalidMarkerNameError(
|
|
"{} is not a valid marker name".format(marker_name)
|
|
)
|
|
|
|
return marker_name
|
|
|
|
def _validate_label(self, label):
|
|
"""Validates label, raising error if invalid."""
|
|
|
|
letter_pattern = compile("^[a-z]{1}$")
|
|
number_pattern = compile("^[0]{1}$|^[1-9]{1,2}$")
|
|
icon_pattern = compile("^[a-zA-Z ]{1,}$")
|
|
|
|
if not match(letter_pattern, label)\
|
|
and not match(number_pattern, label)\
|
|
and not match(icon_pattern, label):
|
|
raise InvalidLabelError(
|
|
"{} is not a valid label".format(label)
|
|
)
|
|
|
|
return label
|
|
|
|
def _validate_color(self, color):
|
|
"""Validates color, raising error if invalid."""
|
|
|
|
three_digit_pattern = compile("^[a-f0-9]{3}$")
|
|
six_digit_pattern = compile("^[a-f0-9]{6}$")
|
|
|
|
if not match(three_digit_pattern, color)\
|
|
and not match(six_digit_pattern, color):
|
|
raise InvalidColorError(
|
|
"{} is not a valid color".format(color)
|
|
)
|
|
|
|
return color
|
|
|
|
def tile(self, map_id, x, y, z, retina=False,
|
|
file_format="png", style_id=None, timestamp=None):
|
|
|
|
"""Returns an image tile, vector tile, or UTFGrid
|
|
in the specified file format.
|
|
|
|
Parameters
|
|
----------
|
|
map_id : str
|
|
The tile's unique identifier in the format username.id.
|
|
|
|
x : int
|
|
The tile's column, where 0 is the minimum value
|
|
and ((2**z) - 1) is the maximum value.
|
|
|
|
y : int
|
|
The tile's row, where 0 is the minimum value
|
|
and ((2**z) - 1) is the maximum value.
|
|
|
|
z : int
|
|
The tile's zoom level, where 0 is the minimum value
|
|
and 20 is the maximum value.
|
|
|
|
retina : bool, optional
|
|
The tile's scale, where True indicates Retina scale
|
|
(double scale) and False indicates regular scale.
|
|
|
|
The default value is false.
|
|
|
|
file_format : str, optional
|
|
The tile's file format.
|
|
|
|
The default value is png.
|
|
|
|
style_id : str, optional
|
|
The tile's style id.
|
|
|
|
style_id must be used together with timestamp.
|
|
|
|
timestamp : str, optional
|
|
The style id's ISO-formatted timestamp, found by
|
|
accessing the "modified" property of a style object.
|
|
|
|
timestamp must be used together with style_id.
|
|
|
|
Returns
|
|
-------
|
|
request.Response
|
|
The response object with a tile in the specified format.
|
|
"""
|
|
|
|
# Check x, y, and z.
|
|
|
|
if x is None or y is None or z is None:
|
|
raise ValidationError(
|
|
"x, y, and z must be not be None"
|
|
)
|
|
|
|
# Validate x, y, z, retina, and file_format.
|
|
|
|
x = self._validate_x(x, z)
|
|
y = self._validate_y(y, z)
|
|
z = self._validate_z(z)
|
|
retina = self._validate_retina(retina)
|
|
file_format = self._validate_file_format(file_format)
|
|
|
|
# Create dict to assist in building URI resource path.
|
|
|
|
path_values = dict(
|
|
map_id=map_id,
|
|
x=str(x),
|
|
y=str(y),
|
|
z=str(z)
|
|
)
|
|
|
|
# Start building URI resource path.
|
|
|
|
path_part = "/{map_id}/{z}/{x}/{y}"
|
|
uri = URITemplate(self.base_uri + path_part).expand(**path_values)
|
|
|
|
# Finish building URI resource path.
|
|
# As in static.py, this two-part process avoids
|
|
# undesired escaping of "@" in "@2x."
|
|
|
|
path_part = "{}.{}".format(retina, file_format)
|
|
uri += path_part
|
|
|
|
# Validate timestamp and build URI query parameters.
|
|
|
|
query_parameters = dict()
|
|
|
|
if style_id is not None and timestamp is not None:
|
|
timestamp = self._validate_timestamp(timestamp)
|
|
style = "{}@{}".format(style_id, timestamp)
|
|
query_parameters["style"] = style
|
|
|
|
# Send HTTP GET request.
|
|
|
|
response = self.session.get(uri, params=query_parameters)
|
|
self.handle_http_error(response)
|
|
|
|
return response
|
|
|
|
def features(self, map_id, feature_format="json"):
|
|
"""Returns vector features from Mapbox Editor projects
|
|
as GeoJSON or KML.
|
|
|
|
Parameters
|
|
----------
|
|
map_id : str
|
|
The map's unique identifier in the format username.id.
|
|
|
|
feature_format : str, optional
|
|
The vector's feature format.
|
|
|
|
The default value is json.
|
|
|
|
Returns
|
|
-------
|
|
request.Response
|
|
The response object with vector features.
|
|
"""
|
|
|
|
# Validate feature_format.
|
|
|
|
feature_format = self._validate_feature_format(feature_format)
|
|
|
|
# Create dict to assist in building URI resource path.
|
|
|
|
path_values = dict(
|
|
map_id=map_id,
|
|
feature_format=feature_format
|
|
)
|
|
|
|
# Build URI resource path.
|
|
|
|
path_part = "/{map_id}/features.{feature_format}"
|
|
uri = URITemplate(self.base_uri + path_part).expand(**path_values)
|
|
|
|
# Send HTTP GET request.
|
|
|
|
response = self.session.get(uri)
|
|
self.handle_http_error(response)
|
|
|
|
return response
|
|
|
|
def metadata(self, map_id, secure=False):
|
|
"""Returns TileJSON metadata for a tileset.
|
|
|
|
Parameters
|
|
----------
|
|
map_id : str
|
|
The map's unique identifier in the format username.id.
|
|
|
|
secure : bool, optional
|
|
The representation of the requested resources,
|
|
where True indicates representation as HTTPS endpoints.
|
|
|
|
The default value is False.
|
|
|
|
Returns
|
|
-------
|
|
request.Response
|
|
The response object with TileJSON metadata for the
|
|
specified tileset.
|
|
"""
|
|
|
|
# Create dict to assist in building URI resource path.
|
|
|
|
path_values = dict(
|
|
map_id=map_id
|
|
)
|
|
|
|
# Build URI resource path.
|
|
|
|
path_part = "/{map_id}.json"
|
|
uri = URITemplate(self.base_uri + path_part).expand(**path_values)
|
|
|
|
# Build URI query parameters.
|
|
|
|
query_parameters = dict()
|
|
|
|
if secure:
|
|
query_parameters["secure"] = ""
|
|
|
|
# Send HTTP GET request.
|
|
|
|
response = self.session.get(uri, params=query_parameters)
|
|
self.handle_http_error(response)
|
|
|
|
return response
|
|
|
|
def marker(self, marker_name=None, label=None,
|
|
color=None, retina=False):
|
|
"""Returns a single marker image without any
|
|
background map.
|
|
|
|
Parameters
|
|
----------
|
|
marker_name : str
|
|
The marker's shape and size.
|
|
|
|
label : str, optional
|
|
The marker's alphanumeric label.
|
|
|
|
Options are a through z, 0 through 99, or the
|
|
name of a valid Maki icon.
|
|
|
|
color : str, optional
|
|
The marker's color.
|
|
|
|
Options are three- or six-digit hexadecimal
|
|
color codes.
|
|
|
|
retina : bool, optional
|
|
The marker's scale, where True indicates Retina scale
|
|
(double scale) and False indicates regular scale.
|
|
|
|
The default value is false.
|
|
|
|
Returns
|
|
-------
|
|
request.Response
|
|
The response object with the specified marker.
|
|
"""
|
|
|
|
# Check for marker_name.
|
|
|
|
if marker_name is None:
|
|
raise ValidationError(
|
|
"marker_name is a required argument"
|
|
)
|
|
|
|
# Validate marker_name and retina.
|
|
|
|
marker_name = self._validate_marker_name(marker_name)
|
|
retina = self._validate_retina(retina)
|
|
|
|
# Create dict and start building URI resource path.
|
|
|
|
path_values = dict(
|
|
marker_name=marker_name
|
|
)
|
|
|
|
path_part = "/marker/{marker_name}"
|
|
|
|
# Validate label, update dict,
|
|
# and continue building URI resource path.
|
|
|
|
if label is not None:
|
|
label = self._validate_label(label)
|
|
path_values["label"] = label
|
|
path_part += "-{label}"
|
|
|
|
# Validate color, update dict,
|
|
# and continue building URI resource path.
|
|
|
|
if color is not None:
|
|
color = self._validate_color(color)
|
|
path_values["color"] = color
|
|
path_part += "+{color}"
|
|
|
|
uri = URITemplate(self.base_uri + path_part).expand(**path_values)
|
|
|
|
# Finish building URI resource path.
|
|
|
|
path_part = "{}.png".format(retina)
|
|
uri += path_part
|
|
|
|
# Send HTTP GET request.
|
|
|
|
response = self.session.get(uri)
|
|
self.handle_http_error(response)
|
|
|
|
return response
|