import synapse.exc as s_exc
import synapse.lib.gis as s_gis
import synapse.lib.layer as s_layer
import synapse.lib.types as s_types
import synapse.lib.module as s_module
import synapse.lib.grammar as s_grammar
units = {
'mm': 1,
'millimeter': 1,
'millimeters': 1,
'cm': 10,
'centimeter': 10,
'centimeters': 10,
# international foot
'foot': 304.8,
'feet': 304.8,
'm': 1000,
'meter': 1000,
'meters': 1000,
# international mile
'mile': 1609344,
'miles': 1609344,
'km': 1000000,
'kilometer': 1000000,
'kilometers': 1000000,
# international yard
'yard': 914.4,
'yards': 914.4,
}
distrepr = (
(1000000.0, 'km'),
(1000.0, 'm'),
(10.0, 'cm'),
)
arearepr = (
(1000000.0, 'sq.km'),
(1000.0, 'sq.m'),
(10.0, 'sq.cm'),
)
areaunits = {
'mm²': 1,
'sq.mm': 1,
'cm²': 10,
'sq.cm': 10,
# international foot
'foot²': 304.8,
'feet²': 304.8,
'sq.feet': 304.8,
'm²': 1000,
'sq.m': 1000,
'sq.meters': 1000,
# international mile
'mile²': 1609344,
'miles²': 1609344,
'sq.miles': 1609344,
'km²': 1000000,
'sq.km': 1000000,
# international yard
'yard²': 914.4,
'sq.yards': 914.4,
}
geojsonschema = {
'definitions': {
'BoundingBox': {'type': 'array', 'minItems': 4, 'items': {'type': 'number'}},
'PointCoordinates': {'type': 'array', 'minItems': 2, 'items': {'type': 'number'}},
'LineStringCoordinates': {'type': 'array', 'minItems': 2, 'items': {'$ref': '#/definitions/PointCoordinates'}},
'LinearRingCoordinates': {'type': 'array', 'minItems': 4, 'items': {'$ref': '#/definitions/PointCoordinates'}},
'PolygonCoordinates': {'type': 'array', 'items': {'$ref': '#/definitions/LinearRingCoordinates'}},
'Point': {
'title': 'GeoJSON Point',
'type': 'object',
'required': ['type', 'coordinates'],
'properties': {
'type': {'type': 'string', 'enum': ['Point']},
'coordinates': {'$ref': '#/definitions/PointCoordinates'},
'bbox': {'$ref': '#/definitions/BoundingBox'},
},
},
'LineString': {
'title': 'GeoJSON LineString',
'type': 'object',
'required': ['type', 'coordinates'],
'properties': {
'type': {'type': 'string', 'enum': ['LineString']},
'coordinates': {'$ref': '#/definitions/LineStringCoordinates'},
'bbox': {'$ref': '#/definitions/BoundingBox'},
},
},
'Polygon': {
'title': 'GeoJSON Polygon',
'type': 'object',
'required': ['type', 'coordinates'],
'properties': {
'type': {'type': 'string', 'enum': ['Polygon']},
'coordinates': {'$ref': '#/definitions/PolygonCoordinates'},
'bbox': {'$ref': '#/definitions/BoundingBox'},
},
},
'MultiPoint': {
'title': 'GeoJSON MultiPoint',
'type': 'object',
'required': ['type', 'coordinates'],
'properties': {
'type': {'type': 'string', 'enum': ['MultiPoint']},
'coordinates': {'type': 'array', 'items': {'$ref': '#/definitions/PointCoordinates'}},
'bbox': {'$ref': '#/definitions/BoundingBox'},
},
},
'MultiLineString': {
'title': 'GeoJSON MultiLineString',
'type': 'object',
'required': ['type', 'coordinates'],
'properties': {
'type': {'type': 'string', 'enum': ['MultiLineString']},
'coordinates': {'type': 'array', 'items': {'$ref': '#/definitions/LineStringCoordinates'}},
'bbox': {'$ref': '#/definitions/BoundingBox'},
},
},
'MultiPolygon': {
'title': 'GeoJSON MultiPolygon',
'type': 'object',
'required': ['type', 'coordinates'],
'properties': {
'type': {'type': 'string', 'enum': ['MultiPolygon']},
'coordinates': {'type': 'array', 'items': {'$ref': '#/definitions/PolygonCoordinates'}},
'bbox': {'$ref': '#/definitions/BoundingBox'},
},
},
'GeometryCollection': {
'title': 'GeoJSON GeometryCollection',
'type': 'object',
'required': ['type', 'geometries'],
'properties': {
'type': {'type': 'string', 'enum': ['GeometryCollection']},
'geometries': {'type': 'array', 'items': {'oneOf': [
{'$ref': '#/definitions/Point'},
{'$ref': '#/definitions/LineString'},
{'$ref': '#/definitions/Polygon'},
{'$ref': '#/definitions/MultiPoint'},
{'$ref': '#/definitions/MultiLineString'},
{'$ref': '#/definitions/MultiPolygon'},
]}},
'bbox': {'$ref': '#/definitions/BoundingBox'},
},
},
'Feature': {
'title': 'GeoJSON Feature',
'type': 'object',
'required': ['type', 'properties', 'geometry'],
'properties': {
'type': {'type': 'string', 'enum': ['Feature']},
'geometry': {'oneOf': [
{'type': 'null'},
{'$ref': '#/definitions/Point'},
{'$ref': '#/definitions/LineString'},
{'$ref': '#/definitions/Polygon'},
{'$ref': '#/definitions/MultiPoint'},
{'$ref': '#/definitions/MultiLineString'},
{'$ref': '#/definitions/MultiPolygon'},
{'$ref': '#/definitions/GeometryCollection'},
]},
'properties': {'oneOf': [{'type': 'null'}, {'type': 'object'}]},
'bbox': {'$ref': '#/definitions/BoundingBox'},
},
},
'FeatureCollection': {
'title': 'GeoJSON FeatureCollection',
'type': 'object',
'required': ['type', 'features'],
'properties': {
'type': {'type': 'string', 'enum': ['FeatureCollection']},
'features': {'type': 'array', 'items': {'$ref': '#/definitions/Feature'}},
'bbox': {'$ref': '#/definitions/BoundingBox'},
},
},
},
'oneOf': [
{'$ref': '#/definitions/Point'},
{'$ref': '#/definitions/LineString'},
{'$ref': '#/definitions/Polygon'},
{'$ref': '#/definitions/MultiPoint'},
{'$ref': '#/definitions/MultiLineString'},
{'$ref': '#/definitions/MultiPolygon'},
{'$ref': '#/definitions/GeometryCollection'},
{'$ref': '#/definitions/Feature'},
{'$ref': '#/definitions/FeatureCollection'},
],
}
[docs]
class Dist(s_types.Int):
[docs]
def postTypeInit(self):
s_types.Int.postTypeInit(self)
self.setNormFunc(int, self._normPyInt)
self.setNormFunc(str, self._normPyStr)
self.baseoff = self.opts.get('baseoff', 0)
def _normPyInt(self, valu):
return valu, {}
def _normPyStr(self, text):
try:
valu, off = s_grammar.parse_float(text, 0)
except Exception:
mesg = f'Distance requires a valid number and unit. No valid number found: {text}'
raise s_exc.BadTypeValu(mesg=mesg, name=self.name, valu=text) from None
unit, off = s_grammar.nom(text, off, s_grammar.alphaset)
mult = units.get(unit.lower())
if mult is None:
mesg = f'Unknown unit of distance: {text}'
raise s_exc.BadTypeValu(mesg=mesg, name=self.name, valu=text)
norm = int(valu * mult) + self.baseoff
if norm < 0:
mesg = f'A geo:dist may not be negative: {text}'
raise s_exc.BadTypeValu(mesg=mesg, name=self.name, valu=text)
return norm, {}
[docs]
def repr(self, norm):
valu = norm - self.baseoff
text = None
absv = abs(valu)
for base, unit in distrepr:
if absv >= base:
size = absv / base
text = '%s %s' % (size, unit)
break
if text is None:
text = '%d mm' % (absv,)
if valu < 0:
text = f'-{text}'
return text
areachars = {'.'}.union(s_grammar.alphaset)
[docs]
class Area(s_types.Int):
[docs]
def postTypeInit(self):
s_types.Int.postTypeInit(self)
self.setNormFunc(int, self._normPyInt)
self.setNormFunc(str, self._normPyStr)
def _normPyInt(self, valu):
return valu, {}
def _normPyStr(self, text):
try:
valu, off = s_grammar.parse_float(text, 0)
except Exception:
mesg = f'Area requires a valid number and unit, no valid number found: {text}'
raise s_exc.BadTypeValu(mesg=mesg, name=self.name, valu=text) from None
unit, off = s_grammar.nom(text, off, areachars)
mult = areaunits.get(unit.lower())
if mult is None:
mesg = f'Unknown unit of area: {text}'
raise s_exc.BadTypeValu(mesg=mesg, name=self.name, valu=text)
norm = int(valu * mult)
if norm < 0:
mesg = f'A geo:area may not be negative: {text}'
raise s_exc.BadTypeValu(mesg=mesg, name=self.name, valu=text)
return norm, {}
[docs]
def repr(self, norm):
text = None
for base, unit in arearepr:
if norm >= base:
size = norm / base
text = f'{size} {unit}'
break
if text is None:
text = f'{norm} sq.mm'
return text
[docs]
class LatLong(s_types.Type):
stortype = s_layer.STOR_TYPE_LATLONG
[docs]
def postTypeInit(self):
self.setNormFunc(str, self._normPyStr)
self.setNormFunc(list, self._normPyTuple)
self.setNormFunc(tuple, self._normPyTuple)
self.setCmprCtor('near=', self._cmprNear)
self.storlifts.update({
'near=': self._storLiftNear,
})
def _normCmprValu(self, valu):
latlong, dist = valu
rlatlong = self.modl.type('geo:latlong').norm(latlong)[0]
rdist = self.modl.type('geo:dist').norm(dist)[0]
return rlatlong, rdist
def _cmprNear(self, valu):
latlong, dist = self._normCmprValu(valu)
def cmpr(valu):
if s_gis.haversine(valu, latlong) <= dist:
return True
return False
return cmpr
def _storLiftNear(self, cmpr, valu):
latlong = self.norm(valu[0])[0]
dist = self.modl.type('geo:dist').norm(valu[1])[0]
return ((cmpr, (latlong, dist), self.stortype),)
def _normPyStr(self, valu):
valu = tuple(valu.strip().split(','))
return self._normPyTuple(valu)
def _normPyTuple(self, valu):
if len(valu) != 2:
raise s_exc.BadTypeValu(valu=valu, name=self.name,
mesg='Valu must contain valid latitude,longitude')
try:
latv = self.modl.type('geo:latitude').norm(valu[0])[0]
lonv = self.modl.type('geo:longitude').norm(valu[1])[0]
except Exception as e:
raise s_exc.BadTypeValu(valu=valu, name=self.name,
mesg=str(e)) from None
return (latv, lonv), {'subs': {'lat': latv, 'lon': lonv}}
[docs]
def repr(self, norm):
return f'{norm[0]},{norm[1]}'
[docs]
class GeoModule(s_module.CoreModule):
[docs]
def getModelDefs(self):
return (
('geo', {
'ctors': (
('geo:dist', 'synapse.models.geospace.Dist', {}, {
'doc': 'A geographic distance (base unit is mm).', 'ex': '10 km'
}),
('geo:area', 'synapse.models.geospace.Area', {}, {
'doc': 'A geographic area (base unit is square mm).', 'ex': '10 sq.km'
}),
('geo:latlong', 'synapse.models.geospace.LatLong', {}, {
'doc': 'A Lat/Long string specifying a point on Earth.',
'ex': '-12.45,56.78'
}),
),
'types': (
('geo:nloc', ('comp', {'fields': (('ndef', 'ndef'), ('latlong', 'geo:latlong'), ('time', 'time'))}), {
'deprecated': True,
'doc': 'Records a node latitude/longitude in space-time.'
}),
('geo:telem', ('guid', {}), {
'doc': 'A geospatial position of a node at a given time. The node should be linked via -(seenat)> edges.',
}),
('geo:json', ('data', {'schema': geojsonschema}), {
'doc': 'GeoJSON structured JSON data.'}),
('geo:name', ('str', {'lower': True, 'onespace': True}), {
'doc': 'An unstructured place name or address.'}),
('geo:place', ('guid', {}), {
'doc': 'A GUID for a geographic place.'}),
('geo:place:taxonomy', ('taxonomy', {}), {
'doc': 'A taxonomy of place types.',
'interfaces': ('meta:taxonomy',),
}),
('geo:address', ('str', {'lower': True, 'onespace': True}), {
'doc': 'A street/mailing address string.'}),
('geo:longitude', ('float', {'min': -180.0, 'max': 180.0,
'minisvalid': False, 'maxisvalid': True}), {
'ex': '31.337',
'doc': 'A longitude in floating point notation.',
}),
('geo:latitude', ('float', {'min': -90.0, 'max': 90.0,
'minisvalid': True, 'maxisvalid': True}), {
'ex': '31.337',
'doc': 'A latitude in floating point notation.',
}),
('geo:bbox', ('comp', {'sepr': ',', 'fields': (
('xmin', 'geo:longitude'),
('xmax', 'geo:longitude'),
('ymin', 'geo:latitude'),
('ymax', 'geo:latitude'))}), {
'doc': 'A geospatial bounding box in (xmin, xmax, ymin, ymax) format.',
}),
('geo:altitude', ('geo:dist', {'baseoff': 6371008800}), {
'doc': 'A negative or positive offset from Mean Sea Level (6,371.0088km from Earths core).'
}),
),
'edges': (
((None, 'seenat', 'geo:telem'), {
'deprecated': True,
'doc': 'Deprecated. Please use ``geo:telem:node``.'}),
(('geo:place', 'contains', 'geo:place'), {
'doc': 'The source place completely contains the target place.'}),
),
'forms': (
('geo:name', {}, ()),
('geo:nloc', {}, (
('ndef', ('ndef', {}), {'ro': True,
'doc': 'The node with location in geospace and time.'}),
('ndef:form', ('str', {}), {'ro': True,
'doc': 'The form of node referenced by the ndef.'}),
('latlong', ('geo:latlong', {}), {'ro': True,
'doc': 'The latitude/longitude the node was observed.'}),
('time', ('time', {}), {'ro': True,
'doc': 'The time the node was observed at location.'}),
('place', ('geo:place', {}), {
'doc': 'The place corresponding to the latlong property.'}),
('loc', ('loc', {}), {
'doc': 'The geo-political location string for the node.'}),
)),
('geo:telem', {}, (
('time', ('time', {}), {
'doc': 'The time that the node was at the position.'}),
('desc', ('str', {}), {
'doc': 'A description of the telemetry sample.'}),
('latlong', ('geo:latlong', {}), {
'doc': 'The latitude/longitude reading at the time.'}),
('accuracy', ('geo:dist', {}), {
'doc': 'The reported accuracy of the latlong telemetry reading.'}),
('place', ('geo:place', {}), {
'doc': 'The place which includes the latlong value.'}),
('place:name', ('geo:name', {}), {
'doc': 'The purported place name. Used for entity resolution.'}),
('node', ('ndef', {}), {
'doc': 'The node that was observed at the associated time and place.'}),
)),
('geo:place:taxonomy', {}, ()),
('geo:place', {}, (
('name', ('geo:name', {}), {
'doc': 'The name of the place.'}),
('type', ('geo:place:taxonomy', {}), {
'doc': 'The type of place.'}),
('names', ('array', {'type': 'geo:name', 'sorted': True, 'uniq': True}), {
'doc': 'An array of alternative place names.'}),
('parent', ('geo:place', {}), {
'deprecated': True,
'doc': 'Deprecated. Please use a -(contains)> edge.'}),
('desc', ('str', {}), {
'doc': 'A long form description of the place.'}),
('loc', ('loc', {}), {
'doc': 'The geo-political location string for the node.'}),
('address', ('geo:address', {}), {
'doc': 'The street/mailing address for the place.'}),
('geojson', ('geo:json', {}), {
'doc': 'A GeoJSON representation of the place.'}),
('latlong', ('geo:latlong', {}), {
'doc': 'The lat/long position for the place.'}),
('bbox', ('geo:bbox', {}), {
'doc': 'A bounding box which encompasses the place.'}),
('radius', ('geo:dist', {}), {
'doc': 'An approximate radius to use for bounding box calculation.'}),
('photo', ('file:bytes', {}), {
'doc': 'The image file to use as the primary image of the place.'}),
)),
)
}),
)