import json
import math
[docs]
class AnnotationGeoJSON:
"""
Generate GeoJSON for an annotation via an iterator.
"""
def __init__(self, annotationId, asFeatures=False, mustConvert=False):
"""
Return an itertor for converting an annotation into geojson.
:param annotatioId: the id of the annotation. No permissions checks
are performed.
:param asFeatures: if False, return a geojson string. If True, return
the features of the geojson. This can be wrapped in
`{'type': 'FeatureCollection', 'features: [...output...]}`
to make it a full geojson object.
:param mustConvert: if True, raise an exception if any annotation
elements cannot be converted. Otherwise, skip those elements.
"""
from ..models.annotation import Annotation
from ..models.annotationelement import Annotationelement
self._id = annotationId
self.annotation = Annotation().load(id=self._id, force=True, getElements=False)
self.elemIterator = Annotationelement().yieldElements(self.annotation)
self.stage = 'header'
self.first = self.annotation['annotation']
self.asFeatures = asFeatures
self.mustConvert = mustConvert
def __iter__(self):
from ..models.annotationelement import Annotationelement
self.elemIterator = Annotationelement().yieldElements(self.annotation)
self.stage = 'header'
return self
def __next__(self):
if self.stage == 'header':
self.stage = 'firstelement'
if not self.asFeatures:
return '{"type":"FeatureCollection","features":['
if self.stage == 'done':
raise StopIteration
try:
while True:
element = next(self.elemIterator)
result = self.elementToGeoJSON(element)
if result is not None:
break
if self.mustConvert:
msg = f'Element of type {element["type"]} cannot be represented as geojson'
raise Exception(msg)
prefix = ''
if self.stage == 'firstelement':
result['properties']['annotation'] = self.first
self.stage = 'elements'
else:
prefix = ','
if not self.asFeatures:
return prefix + json.dumps(result, separators=(',', ':'))
return result
except StopIteration:
self.stage = 'done'
if not self.asFeatures:
return ']}'
raise
[docs]
def rotate(self, r, cx, cy, x, y, z):
if not r:
return [x + cx, y + cy, z]
cosr = math.cos(r)
sinr = math.sin(r)
x -= cx
y -= cy
return [x * cosr - y * sinr + cx, x * sinr + y * sinr + cy, z]
[docs]
def circleType(self, element, geom, prop):
x, y, z = element['center']
r = element['radius']
geom['type'] = 'Polygon'
geom['coordinates'] = [[
[x - r, y - r, z],
[x + r, y - r, z],
[x + r, y + r, z],
[x - r, y + r, z],
[x - r, y - r, z],
]]
[docs]
def ellipseType(self, element, geom, prop):
return self.rectangleType(element, geom, prop)
[docs]
def pointType(self, element, geom, prop):
geom['type'] = 'Point'
geom['coordinates'] = element['center']
[docs]
def polylineType(self, element, geom, prop):
if element['closed']:
geom['type'] = 'Polygon'
geom['coordinates'] = [element['points'][:]]
geom['coordinates'][0].append(geom['coordinates'][0][0])
if element.get('holes'):
for hole in element['holes']:
hole = hole[:]
hole.append(hole[0])
geom['coordinates'].append(hole)
else:
geom['type'] = 'LineString'
geom['coordinates'] = element['points']
[docs]
def rectangleType(self, element, geom, prop):
x, y, z = element['center']
width = element['width']
height = element['height']
rotation = element.get('rotation', 0)
left = x - width / 2
right = x + width / 2
top = y - height / 2
bottom = y + height / 2
geom['type'] = 'Polygon'
geom['coordinates'] = [[
self.rotate(rotation, x, y, left, top, z),
self.rotate(rotation, x, y, right, top, z),
self.rotate(rotation, x, y, right, bottom, z),
self.rotate(rotation, x, y, left, bottom, z),
self.rotate(rotation, x, y, left, top, z),
]]
# not represented
# heatmap, griddata, image, pixelmap, arrow, rectanglegrid
# heatmap could be MultiPoint, griddata could be rectangle with lots of
# properties, image and pixelmap could be rectangle with the image id as a
# property, arrow and rectangelgrid aren't really supported
[docs]
def elementToGeoJSON(self, element):
elemType = element.get('type', '')
funcName = elemType + 'Type'
if not hasattr(self, funcName):
return None
result = {
'type': 'Feature',
'geometry': {},
'properties': {
k: v if k != 'id' else str(v)
for k, v in element.items() if k in {
'id', 'label', 'group', 'user', 'lineColor', 'lineWidth',
'fillColor', 'radius', 'width', 'height', 'rotation',
'normal',
}
},
}
getattr(self, funcName)(element, result['geometry'], result['properties'])
if result['geometry']['type'].lower() != element['type']:
result['properties']['type'] = element['type']
return result
@property
def geojson(self):
return ''.join(self)
[docs]
class GeoJSONAnnotation:
def __init__(self, geojson):
if not isinstance(geojson, (dict, list, tuple)):
geojson = json.loads(geojson)
self._elements = []
self._annotation = {'elements': self._elements}
self._parseFeature(geojson)
def _parseFeature(self, geoelem):
if isinstance(geoelem, (list, tuple)):
for entry in geoelem:
self._parseFeature(entry)
if not isinstance(geoelem, dict) or 'type' not in geoelem:
return
if geoelem['type'] == 'FeatureCollection':
return self._parseFeature(geoelem.get('features', []))
if geoelem['type'] == 'GeometryCollection' and isinstance(geoelem.get('geometries'), list):
for entry in geoelem['geometry']:
self._parseFeature({'type': 'Feature', 'geometry': entry})
return
if geoelem['type'] in {'Point', 'LineString', 'Polygon', 'MultiPoint',
'MultiLineString', 'MultiPolygon'}:
geoelem = {'type': 'Feature', 'geometry': geoelem}
element = {k: v for k, v in geoelem.get('properties', {}).items() if k in {
'id', 'label', 'group', 'user', 'lineColor', 'lineWidth',
'fillColor', 'radius', 'width', 'height', 'rotation',
'normal',
}}
if 'annotation' in geoelem.get('properties', {}):
self._annotation.update(geoelem['properties']['annotation'])
self._annotation['elements'] = self._elements
elemtype = geoelem.get('properties', {}).get('type', '') or geoelem['geometry']['type']
func = getattr(self, elemtype.lower() + 'Type', None)
if func is not None:
result = func(geoelem['geometry'], element)
if isinstance(result, list):
self._elements.extend(result)
else:
self._elements.append(result)
[docs]
def circleType(self, elem, result):
cx = sum(e[0] for e in elem['coordinates'][0][:4]) / 4
cy = sum(e[1] for e in elem['coordinates'][0][:4]) / 4
try:
cz = elem['coordinates'][0][0][2]
except Exception:
cz = 0
radius = (max(e[0] for e in elem['coordinates'][0][:4]) -
min(e[0] for e in elem['coordinates'][0][:4])) / 2
result['type'] = 'circle'
result['radius'] = radius
result['center'] = [cx, cy, cz]
return result
[docs]
def ellipseType(self, elem, result):
result = self.rectangleType(elem, result)
result['type'] = 'ellipse'
return result
[docs]
def rectangleType(self, elem, result):
coor = elem['coordinates'][0]
cx = sum(e[0] for e in coor[:4]) / 4
cy = sum(e[1] for e in coor[:4]) / 4
try:
cz = elem['coordinates'][0][0][2]
except Exception:
cz = 0
width = ((coor[0][0] - coor[1][0]) ** 2 + (coor[0][1] - coor[1][1]) ** 2) ** 0.5
height = ((coor[1][0] - coor[2][0]) ** 2 + (coor[1][1] - coor[2][1]) ** 2) ** 0.5
rotation = math.atan2(coor[1][1] - coor[0][1], coor[1][0] - coor[0][0])
result['center'] = [cx, cy, cz]
result['width'] = width
result['height'] = height
result['rotation'] = rotation
result['type'] = 'rectangle'
return result
[docs]
def pointType(self, elem, result):
result['center'] = (elem['coordinates'] + [0, 0, 0])[:3]
result['type'] = 'point'
return result
[docs]
def multipointType(self, elem, result):
results = []
result['type'] = 'point'
for entry in elem['coordinates']:
subresult = result.copy()
subresult['center'] = (entry + [0, 0, 0])[:3]
results.append(subresult)
return results
[docs]
def polylineType(self, elem, result):
if elem.get('type') == 'LineString':
return self.linestringType(elem, result)
return self.polygonType(elem, result)
[docs]
def polygonType(self, elem, result):
result['points'] = [(pt + [0])[:3] for pt in elem['coordinates'][0][:-1]]
if len(elem['coordinates']) > 1:
result['holes'] = [
[(pt + [0])[:3] for pt in loop[:-1]]
for loop in elem['coordinates'][1:]
]
result['closed'] = True
result['type'] = 'polyline'
return result
[docs]
def multipolygonType(self, elem, result):
results = []
result['closed'] = True
result['type'] = 'polyline'
for entry in elem['coordinates']:
subresult = result.copy()
subresult['points'] = [(pt + [0])[:3] for pt in entry[0][:-1]]
if len(entry) > 1:
subresult['holes'] = [
[(pt + [0])[:3] for pt in loop[:-1]]
for loop in entry[1:]
]
results.append(subresult)
return results
[docs]
def linestringType(self, elem, result):
result['points'] = [(pt + [0])[:3] for pt in elem['coordinates']]
result['closed'] = False
result['type'] = 'polyline'
return result
[docs]
def multilinestringType(self, elem, result):
results = []
result['closed'] = False
result['type'] = 'polyline'
for entry in elem['coordinates']:
subresult = result.copy()
subresult['points'] = [(pt + [0])[:3] for pt in entry]
results.append(subresult)
return results
[docs]
def annotationToJSON(self):
return json.dumps(self._annotation)
@property
def annotation(self):
return self._annotation
@property
def elements(self):
return self._elements
@property
def elementCount(self):
return len(self._elements)
[docs]
def isGeoJSON(annotation):
"""
Check if a list or dictionary appears to contain a GeoJSON record.
:param annotation: a list or dictionary.
:returns: True if this appears to be GeoJSON
"""
if isinstance(annotation, list):
if len(annotation) < 1:
return False
annotation = annotation[0]
if not isinstance(annotation, dict) or 'type' not in annotation:
return False
return annotation['type'] in {
'Feature', 'FeatureCollection', 'GeometryCollection', 'Point',
'LineString', 'Polygon', 'MultiPoint', 'MultiLineString',
'MultiPolygon'}