from decimal import Decimal
from numbers import Number, Real
from geojson.base import GeoJSON
DEFAULT_PRECISION = 6
class Geometry(GeoJSON):
"""
Represents an abstract base class for a WGS84 geometry.
"""
def __init__(self, coordinates=None, validate=False, precision=None, **extra):
"""
Initialises a Geometry object.
:param coordinates: Coordinates of the Geometry object.
:type coordinates: tuple or list of tuple
:param validate: Raise exception if validation errors are present?
:type validate: boolean
:param precision: Number of decimal places for lat/lon coords.
:type precision: integer
"""
super().__init__(**extra)
if precision is None:
precision = DEFAULT_PRECISION
self["coordinates"] = self.clean_coordinates(
coordinates or [], precision)
if validate:
errors = self.errors()
if errors:
raise ValueError(f'{errors}: {coordinates}')
@classmethod
def clean_coordinates(cls, coords, precision):
if isinstance(coords, cls):
return coords['coordinates']
new_coords = []
if isinstance(coords, Geometry):
coords = [coords]
for coord in coords:
if isinstance(coord, (list, tuple)):
new_coords.append(cls.clean_coordinates(coord, precision))
elif isinstance(coord, Geometry):
new_coords.append(coord['coordinates'])
elif isinstance(coord, (Real, Decimal)):
new_coords.append(round(coord, precision))
else:
raise ValueError("%r is not a JSON compliant number" % coord)
return new_coords
class GeometryCollection(GeoJSON):
"""
Represents an abstract base class for collections of WGS84 geometries.
"""
def __init__(self, geometries=None, **extra):
super().__init__(**extra)
self["geometries"] = geometries or []
[docs] def errors(self):
errors = [geom.errors() for geom in self['geometries']]
return [err for err in errors if err]
def __getitem__(self, key):
try:
return self.get("geometries", ())[key]
except (KeyError, TypeError, IndexError):
return super(GeoJSON, self).__getitem__(key)
# Marker classes.
def check_point(coord):
if not isinstance(coord, list):
return 'each position must be a list'
if len(coord) not in (2, 3):
return 'a position must have exactly 2 or 3 values'
for number in coord:
if not isinstance(number, Number):
return 'a position cannot have inner positions'
class Point(Geometry):
[docs] def errors(self):
return check_point(self['coordinates'])
class MultiPoint(Geometry):
[docs] def errors(self):
return self.check_list_errors(check_point, self['coordinates'])
def check_line_string(coord):
if not isinstance(coord, list):
return 'each line must be a list of positions'
if len(coord) < 2:
return ('the "coordinates" member must be an array of '
'two or more positions')
for pos in coord:
error = check_point(pos)
if error:
return error
class LineString(MultiPoint):
[docs] def errors(self):
return check_line_string(self['coordinates'])
class MultiLineString(Geometry):
[docs] def errors(self):
return self.check_list_errors(check_line_string, self['coordinates'])
def check_polygon(coord):
if not isinstance(coord, list):
return 'Each polygon must be a list of linear rings'
if not all(isinstance(elem, list) for elem in coord):
return "Each element of a polygon's coordinates must be a list"
lengths = all(len(elem) >= 4 for elem in coord)
if lengths is False:
return 'Each linear ring must contain at least 4 positions'
isring = all(elem[0] == elem[-1] for elem in coord)
if isring is False:
return 'Each linear ring must end where it started'
class Polygon(Geometry):
[docs] def errors(self):
return check_polygon(self['coordinates'])
class MultiPolygon(Geometry):
[docs] def errors(self):
return self.check_list_errors(check_polygon, self['coordinates'])
class Default:
"""
GeoJSON default object.
"""