Package gavo :: Package formats :: Module geojson
[frames] | no frames]

Source Code for Module gavo.formats.geojson

  1  """ 
  2  Generating geojson (RFC 7946) files. 
  3   
  4  This requires annotation with the geojson (DaCHS-private) data model. 
  5   
  6  See separate documentation in the reference documentation. 
  7   
  8  No streaming is forseen for this format for now; whatever a web browser 
  9  can cope with, we can, too.  I hope. 
 10   
 11  To add more geometry types, pick a type name (typically different 
 12  from what geojson calls the thing because it also depends on the input), 
 13  add it to _FEATURE_MAKERS and write the Factory, taking _getSepcooFactory as 
 14  a model. 
 15  """ 
 16   
 17  #c Copyright 2008-2019, the GAVO project 
 18  #c 
 19  #c This program is free software, covered by the GNU GPL.  See the 
 20  #c COPYING file in the source distribution. 
 21   
 22   
 23  import json 
 24   
 25  from gavo import base 
 26  from gavo.formats import common 
 27  from gavo.utils import serializers 
 28   
 29   
 30  # we need to protect some of our columns from being mapped (by giving 
 31  # them a magic attribute), hence a special MFRegistry 
 32   
 33  JSON_MF_REGISTRY = serializers.defaultMFRegistry.clone() 
 34  registerMF = JSON_MF_REGISTRY.registerFactory 
 35   
36 -def _rawMapperFactory(colDesc):
37 if hasattr(colDesc.original, "geojson_do_not_touch"): 38 return lambda val: val
39 registerMF(_rawMapperFactory) 40 41
42 -def _makeCRS(gjAnnotation):
43 """returns a dictionary to add a geoJSON CRS structure from a DaCHS 44 annotation to a dictionary. 45 """ 46 try: 47 rawCRS = gjAnnotation["crs"] 48 if rawCRS["type"]=="name": 49 return {"crs": { 50 "type": "name", 51 "properties": { 52 "name": rawCRS["properties"]["name"], 53 } 54 } 55 } 56 57 elif rawCRS["type"]=="link": 58 return {"crs": { 59 "type": "url", 60 "properties": { 61 "href": rawCRS["properties"]["href"], 62 "type": rawCRS["properties"]["type"], 63 } 64 } 65 } 66 67 else: 68 raise base.DataError("Unknown GeoJSON CRS type: %s"%rawCRS["type"]) 69 70 except KeyError: 71 return {}
72 73
74 -def _makeFeatureFactory(tableDef, skippedFields, geoFactory):
75 """returns a factory function for building geoJson features from 76 rowdicts. 77 78 skippedFields are not included in the properties (i.e., they're 79 what geometry is built from),, geoFactory is a function returning 80 the geometry itself, given a row. 81 """ 82 propertiesFields = [f.name for f in tableDef.columns 83 if f.name not in skippedFields] 84 85 def buildFeature(row): 86 feature = geoFactory(row) 87 feature["properties"] = dict((n, row[n]) for n in propertiesFields) 88 return feature
89 90 return buildFeature 91 92
93 -def _getGeometryFactory(tableDef, geometryAnnotation):
94 """returns a row factory for a geometry-valued column. 95 96 This expects a value key referencing a column typed either 97 spoint or spoly. 98 """ 99 geoCol = geometryAnnotation["value"].value 100 101 if geoCol.type=="spoint": 102 def annMaker(row): 103 return { 104 "type": "Point", 105 "coordinates": list(row[geoCol.name].asCooPair())}
106 geoCol.geojson_do_not_touch = True 107 108 elif geoCol.type=="spoly": 109 def annMaker(row): 110 return { 111 "type": "Polygon", 112 "coordinates": [list(p) for p in 113 row[geoCol.name].asCooPairs()]} 114 geoCol.geojson_do_not_touch = True 115 116 else: 117 raise base.DataError("Cannot serialise %s-valued columns" 118 " with a 'geometry' geometry type (only spoint and spoly)") 119 120 return _makeFeatureFactory(tableDef, [geoCol.name], annMaker) 121 122
123 -def _getSepsimplexFactory(tableDef, geometryAnnotation):
124 """returns a row factory for polygons specified with a min/max coordinate 125 range. 126 127 This expects c1min/max, c2min/max keys. It does not do anything special 128 if the simplex spans the stitching line. 129 """ 130 c1min = geometryAnnotation["c1min"].value.name 131 c1max = geometryAnnotation["c1max"].value.name 132 c2min = geometryAnnotation["c2min"].value.name 133 c2max = geometryAnnotation["c2max"].value.name 134 135 return _makeFeatureFactory(tableDef, 136 [c1min, c2min, c1max, c2max], 137 lambda row: { 138 "type": "Polygon", 139 "coordinates": [ 140 [row[c1min], row[c2min]], 141 [row[c1min], row[c2max]], 142 [row[c1max], row[c2max]], 143 [row[c1max], row[c2min]], 144 [row[c1min], row[c2min]]]})
145 146
147 -def _getSeppolyFactory(tableDef, geometryAnnotation):
148 """returns a features factory for polygons made of separate 149 coordinates. 150 151 This expects cn_m keys; it will gooble them up until the first is not 152 found. m is either 1 or 2. 153 """ 154 # hard code the assumption that these are column annotations for now 155 # -- make a type check if we may put in literals 156 cooIndex = 1 157 polyCoos, ignoredNames = [], set() 158 while True: 159 try: 160 polyCoos.append(( 161 geometryAnnotation["c%d_1"%cooIndex].value.name, 162 geometryAnnotation["c%d_2"%cooIndex].value.name)) 163 ignoredNames |= set(polyCoos[-1]) 164 except KeyError: 165 break 166 cooIndex += 1 167 polyCoos.append(polyCoos[0]) 168 169 return _makeFeatureFactory(tableDef, 170 ignoredNames, 171 lambda row: { 172 "type": "Polygon", 173 "coordinates": [[row[name1], row[name2]] 174 for name1, name2 in polyCoos]})
175 176
177 -def _getSepcooFactory(tableDef, geometryAnnotation):
178 """returns a features factory for points made up of separate coordinates. 179 180 This expects latitude and longitude keys. 181 """ 182 # hard code the assumption that these are column annotations for now 183 # -- make a type check if we may put in literals 184 latCoo = geometryAnnotation["latitude"].value.name 185 longCoo = geometryAnnotation["longitude"].value.name 186 return _makeFeatureFactory(tableDef, 187 [latCoo, longCoo], 188 lambda row: { 189 "type": "Point", 190 "coordinates": [row[longCoo], row[latCoo]]})
191 192 193 # a dict mapping feature.geometry.type names to row factories dealing 194 # with them 195 _FEATURE_MAKERS = { 196 "sepcoo": _getSepcooFactory, 197 "seppoly": _getSeppolyFactory, 198 "sepsimplex": _getSepsimplexFactory, 199 "geometry": _getGeometryFactory, 200 } 201
202 -def _makeFeatures(table, gjAnnotation):
203 """returns a list of geoJSON features from the (annotated) table. 204 """ 205 geometryAnnotation = gjAnnotation["feature"]["geometry"] 206 try: 207 makeFeature = _FEATURE_MAKERS[ 208 geometryAnnotation["type"]]( 209 table.tableDef, geometryAnnotation) 210 except KeyError as msg: 211 raise base.ui.logOldExc( 212 base.DataError("Invalid geoJSON annotation on table %s: %s missing"%( 213 table.tableDef.id, msg))) 214 215 sm = base.SerManager(table, acquireSamples=False, 216 mfRegistry=JSON_MF_REGISTRY) 217 218 # let geo builders manually ignore rows they can't do anything with 219 features = [] 220 for r in sm.getMappedValues(): 221 try: 222 features.append(makeFeature(r)) 223 except base.SkipThis: 224 pass 225 return features
226
227 -def writeTableAsGeoJSON(table, target, acquireSamples=False):
228 """writes a table as geojson. 229 230 This requires an annotation with geojson:FeatureCollection. 231 """ 232 # for now, don't bother with complete data items, just serialise the 233 # primary table. 234 if hasattr(table, "getPrimaryTable"): 235 table = table.getPrimaryTable() 236 237 try: 238 ann = table.tableDef.iterAnnotationsOfType("geojson:FeatureCollection" 239 ).next() 240 except IndexError: 241 raise base.DataError("Table has no geojson:FeatureCollection annotation." 242 " Cannot serialise to GeoJSON.") 243 244 result = { 245 "type": "FeatureCollection", 246 "features": _makeFeatures(table, ann), 247 } 248 result.update(_makeCRS(ann)) 249 250 return json.dump(result, target, encoding="utf-8")
251 252 253 # NOTE: while json could easily serialize full data elements, 254 # right now we're only writing single tables. 255 common.registerDataWriter("geojson", 256 writeTableAsGeoJSON, "application/geo+json", "GeoJSON", ".geojson") 257