diff options
| -rw-r--r-- | config.yml | 81 | ||||
| -rw-r--r-- | export_mvt.py | 110 | 
2 files changed, 174 insertions, 17 deletions
@@ -2709,7 +2709,10 @@ layers:          - AvvSasong          - AvvHa          - AvverkningsanmalanKlass -    publish: anmald +    publish: +      anmald: +        fields: +          age: Inkomdatum    'sks:avverk_utford':      # https://geodpags.skogsstyrelsen.se/geodataport/feeds/UtfordAvverk.xml @@ -2799,7 +2802,10 @@ layers:          Beteckn:            - replace: 'Visas ej'              with: null -    publish: utford +    publish: +      utford: +        fields: +          age: Avvdatum    'sametinget:betesomrade':      description: 'Samebyarnas betesområden: Renbetesområden' @@ -3217,30 +3223,44 @@ layers:          minzoom: 4          where: |            "Raderat" IS FALSE AND "Statuskod" = 4 +        fields: +          age: SenasteUppdaterat        station_processed:          minzoom: 4          where: |            "Raderat" IS FALSE AND "Statuskod" = 1 +        fields: +          age: SenasteUppdaterat        station_approved:          minzoom: 4          where: |            "Raderat" IS FALSE AND "Statuskod" = 3 +        fields: +          age: SenasteUppdaterat        station_revoked:          minzoom: 4          where: |            "Raderat" IS FALSE AND ("Statuskod" = 6 OR "Statuskod" = 9) +        fields: +          age: SenasteUppdaterat        station_rejected:          minzoom: 4          where: |            "Raderat" IS FALSE AND "Statuskod" = 7 +        fields: +          age: SenasteUppdaterat        station_dismounted:          minzoom: 4          where: |            "Raderat" IS FALSE AND "Statuskod" = 5 +        fields: +          age: SenasteUppdaterat        station_appealed:          minzoom: 4          where: |            "Raderat" IS FALSE AND "Statuskod" = 8 +        fields: +          age: SenasteUppdaterat    'vbk:projekteringsomraden':      description: Vindbrukskollen landbaserade projekteringsområden (Länsstyrelsen) @@ -3395,9 +3415,13 @@ layers:        area_current:          where: |            "Raderat" IS FALSE AND "EjAktuell" IS FALSE +        fields: +          age: SenasteUppdaterat        area_notcurrent:          where: |            "Raderat" IS FALSE AND "EjAktuell" IS NOT FALSE +        fields: +          age: SenasteUppdaterat    'vbk:havsbaserad_vindkraft':      description: Vindbrukskollen havsbaserad vindkraft (Länsstyrelsen) @@ -3613,30 +3637,48 @@ layers:        offshore_completed:          where: |            "Raderat" IS FALSE AND "Projektstatus" = 'Uppförd' +        fields: +          age: SenasteUppdaterat        offshore_approved:          where: |            "Raderat" IS FALSE AND "Projektstatus" = 'Tillståndsansökan beviljad' +        fields: +          age: SenasteUppdaterat        offshore_amended:          where: |            "Raderat" IS FALSE AND "Projektstatus" = 'Ändringsansökan' +        fields: +          age: SenasteUppdaterat        offshore_rejected:          where: |            "Raderat" IS FALSE AND "Projektstatus" = 'Tillståndsansökan avslagen' +        fields: +          age: SenasteUppdaterat        offshore_appealed:          where: |            "Raderat" IS FALSE AND "Projektstatus" = 'Överklagad' +        fields: +          age: SenasteUppdaterat        offshore_applied:          where: |            "Raderat" IS FALSE AND "Projektstatus" = 'Tillståndsansökan inlämnad' +        fields: +          age: SenasteUppdaterat        offshore_consultation:          where: |            "Raderat" IS FALSE AND "Projektstatus" = 'Samråd inför tillståndsansökan' +        fields: +          age: SenasteUppdaterat        offshore_investigation:          where: |            "Raderat" IS FALSE AND "Projektstatus" = 'Inledande undersökningar' +        fields: +          age: SenasteUppdaterat        offshore_revoked:          where: |            "Raderat" IS FALSE AND "Projektstatus" = 'Inte aktuell eller återkallad' +        fields: +          age: SenasteUppdaterat    'mrr:ut_metaller_industrimineral_ansokta':      # https://resource.sgu.se/dokument/produkter/mineralrattigheter-beskrivning.pdf @@ -3678,7 +3720,10 @@ layers:        path: mineralrattigheter.gpkg        format: GPKG        layername: ut_metaller_industrimineral_ansokta -    publish: appl_met +    publish: +      appl_met: +        fields: +          age: appl_date    'mrr:ut_diamant_ansokta':      # https://resource.sgu.se/dokument/produkter/mineralrattigheter-beskrivning.pdf @@ -3720,7 +3765,10 @@ layers:        path: mineralrattigheter.gpkg        format: GPKG        layername: ut_diamant_ansokta -    publish: appl_ogd +    publish: +      appl_ogd: +        fields: +          age: appl_date    'mrr:bearbetningskoncessioner_ansokta':      # https://resource.sgu.se/dokument/produkter/mineralrattigheter-beskrivning.pdf @@ -3762,7 +3810,10 @@ layers:        path: mineralrattigheter.gpkg        format: GPKG        layername: bearbetningskoncessioner_ansokta -    publish: appl_ec +    publish: +      appl_ec: +        fields: +          age: appl_date    'mrr:markanvisningar_bk_ansokta':      # https://resource.sgu.se/dokument/produkter/mineralrattigheter-beskrivning.pdf @@ -3861,7 +3912,10 @@ layers:          licenceid:            - replace: '-'              with: null -    publish: appr_met +    publish: +      appr_met: +        fields: +          age: dec_date    'mrr:ut_diamant_beviljade':      # https://resource.sgu.se/dokument/produkter/mineralrattigheter-beskrivning.pdf @@ -3924,7 +3978,10 @@ layers:          licenceid:            - replace: '-'              with: null -    publish: appr_ogd +    publish: +      appr_ogd: +        fields: +          age: dec_date    'mrr:bearbetningskoncessioner_beviljade':      # https://resource.sgu.se/dokument/produkter/mineralrattigheter-beskrivning.pdf @@ -3988,7 +4045,10 @@ layers:          licenceid:            - replace: '-'              with: null -    publish: appr_ec +    publish: +      appr_ec: +        fields: +          age: dec_date    'mrr:markanvisningar_bk_beviljade':      # https://resource.sgu.se/dokument/produkter/mineralrattigheter-beskrivning.pdf @@ -4038,7 +4098,10 @@ layers:          licenceid:            - replace: '-'              with: null -    publish: appr_dl +    publish: +      appr_dl: +        fields: +          age: dec_date    'mrr:ut_metaller_industrimineral_forbud':      # https://resource.sgu.se/dokument/produkter/mineralrattigheter-beskrivning.pdf diff --git a/export_mvt.py b/export_mvt.py index d19909c..31c7044 100644 --- a/export_mvt.py +++ b/export_mvt.py @@ -121,6 +121,7 @@ def exportSourceLayer(ds_src : gdal.Dataset,                        lyr_src : ogr.Layer,                        lyr_dst : ogr.Layer,                        layerdef : dict[str,Any], +                      fieldMap : tuple[list[str],list[int]],                        extent : ogr.Geometry|None = None) -> int:      """Export a source layer."""      count0 = -1 @@ -168,7 +169,7 @@ def exportSourceLayer(ds_src : gdal.Dataset,          spatialFilter = getSpatialFilterFromGeometry(extent, srs_src)      transform_geometry = layerdef.get('transform-geometry', None) -    columns = [ 'm.' + escape_identifier(lyr_src.GetFIDColumn()) ] +    columns = [ 'm.' + escape_identifier(lyr_src.GetFIDColumn()) ] + fieldMap[0]      geomFieldName_esc = escape_identifier(geomField.GetName())      if transform_geometry is None:          columns.append('m.' + geomFieldName_esc) @@ -194,16 +195,26 @@ def exportSourceLayer(ds_src : gdal.Dataset,              logging.debug('Source layer "%s" has %d features, of which %d are to be exported',                            layername, count0, count1) +        fieldMap = fieldMap[1] +        logging.debug('Field map: %s', str(fieldMap)) + +        geom_type = lyr_src.GetGeomType() +        bFlatten = geom_type == ogr.wkbUnknown or ogr.GT_HasM(geom_type) or ogr.GT_HasZ(geom_type) +        bTransform = bFlatten or ct is not None +          feature_count = 0          defn_dst = lyr_dst.GetLayerDefn()          feature = lyr_src.GetNextFeature()          while feature is not None: -            geom = feature.GetGeometryRef().Clone() -            if ct is not None and geom.Transform(ct) != ogr.OGRERR_NONE: -                raise RuntimeError('Could not apply coordinate transformation') -            geom.FlattenTo2D()              feature2 = ogr.Feature(defn_dst) -            feature2.SetGeometryDirectly(geom) +            feature2.SetFromWithMap(feature, False, fieldMap) +            if bTransform: +                geom = feature2.GetGeometryRef() +                if ct is not None and geom.Transform(ct) != ogr.OGRERR_NONE: +                    raise RuntimeError('Could not apply coordinate transformation') +                if bFlatten: +                    geom.FlattenTo2D() +                feature2.SetGeometryDirectly(geom)              feature2.SetFID(feature.GetFID())              if lyr_dst.CreateFeature(feature2) != ogr.OGRERR_NONE:                  raise RuntimeError(f'Could not transfer source feature #{feature.GetFID()}') @@ -351,7 +362,85 @@ def exportMetadata(basedir : Path, data : dict[str,Any],      finally:          os.close(fd) -# pylint: disable-next=too-many-branches, too-many-statements +def getFieldMap(lyr_dst : ogr.Layer, lyr_src : ogr.Layer, +                drv_src : gdal.Driver, +                fieldMap : dict[str,str]|None) -> tuple[list[str],list[int]]: +    """Create fields on the destination MVT layer, and return a list of +    column statements along with a field map for the MVT export.""" +    if fieldMap is None or len(fieldMap) == 0: +        return [], [] + +    if not lyr_dst.TestCapability(ogr.OLCCreateField): +        raise RuntimeError(f'Destination layer "{lyr_dst.GetName()}" lacks ' +                           'field creation capability') + +    columns = {} +    defn_src = lyr_src.GetLayerDefn() +    for fld_dst, fld_src in fieldMap.items(): +        idx_src = defn_src.GetFieldIndex(fld_src) +        if idx_src < 0: +            raise RuntimeError(f'Source layer "{lyr_src.GetName()}" has no field named "{fld_src}"') + +        defn_dst = ogr.FieldDefn() +        defn_src_fld = defn_src.GetFieldDefn(idx_src) +        if fld_dst == 'age': +            if defn_src_fld.GetType() not in (ogr.OFTDate, ogr.OFTDateTime): +                raise RuntimeError(f'Field "{fld_src}" of source layer "{lyr_src.GetName()}"' +                                   ' has type ' + ogr.GetFieldTypeName(defn_src_fld.GetType()) + +                                   ' (Date or DateTime expected)') +            defn_dst.SetType(ogr.OFTInteger) +            # signed int16 allows expressing dates from 1880-04-15 to 2059-09-18 +            # which should be more than enough (it's not clear if the MVT format takes +            # advantage of the reduced storage though) +            defn_dst.SetSubType(ogr.OFSTInt16) + +            # TODO[GDAL >=3.9] use lyr_src.GetDataset().GetDriver() +            if drv_src.ShortName == 'PostgreSQL': +                column = 'CAST(m.' + escape_identifier(fld_src) +                column += ' - date \'1970-01-01\' AS smallint)' +            elif drv_src.ShortName in ('SQLite', 'GPKG'): +                column = 'CAST(floor(julianday(m.' + escape_identifier(fld_src) + ')' +                column += ' - 2440587.5) AS smallint)' +            else: +                raise NotImplementedError(f'Unsupported source driver {drv_src.ShortName} for ' +                                          f'field "{fld_src}" (MVT field "{fld_dst}")') + +        else: +            raise NotImplementedError(f'Destination MVT field "{fld_dst}"') + +        columns[fld_dst] = column + +        defn_dst.SetName(fld_dst) +        defn_dst.SetNullable(defn_src_fld.IsNullable()) +        logging.debug('Create output field "%s" with type=%s, subtype=%s, nullable=%d', +                      defn_dst.GetName(), +                      ogr.GetFieldTypeName(defn_dst.GetType()), +                      ogr.GetFieldSubTypeName(defn_dst.GetSubType()), +                      defn_dst.IsNullable()) + +        if lyr_dst.CreateField(defn_dst, approx_ok=False) != gdal.CE_None: +            raise RuntimeError(f'Could not create field "{fld_dst}" ' +                               f'in destination MVT layer "{lyr_dst.GetName()}"') + +    indices = {} +    defn_dst = lyr_dst.GetLayerDefn() +    for i in range(defn_dst.GetFieldCount()): +        fld = defn_dst.GetFieldDefn(i) +        name = fld.GetName() +        if name in columns: +            indices[name] = i +        else: +            logging.warning('Destination layer has unknown field #%d "%s"', i, name) + +    ret = [None] * len(columns) +    fieldMap = [-1] * defn_dst.GetFieldCount() +    for idx, name in enumerate(columns.keys()): +        i = indices[name] # intentionally crash if we didn't create that field +        fieldMap[i] = idx +        ret[idx] = columns[name] + ' AS ' + escape_identifier(name) +    return (ret, fieldMap) + +# pylint: disable-next=too-many-branches, too-many-locals, too-many-statements  def exportMVT(ds : gdal.Dataset,                layers : dict[str,dict[str,Any]],                sources : dict[str,Any], @@ -436,10 +525,15 @@ def exportMVT(ds : gdal.Dataset,              if lyr_dst is None:                  raise RuntimeError(f'Could not create destination layer "{layername}"') +            fieldMap = getFieldMap(lyr_dst, lyr_src, drv_src=ds.GetDriver(), +                                   fieldMap=layerdef.get('fields', None)) +              # TODO: GDAL exports features to a temporary SQLite database even though the source              # is PostGIS hence is able to generate MVT with ST_AsMVT().  Letting PostGIS generate              # tiles is likely to speed up things. -            feature_count += exportSourceLayer(ds, lyr_src, lyr_dst, layerdef, extent=extent) +            feature_count += exportSourceLayer(ds, lyr_src, lyr_dst, layerdef, +                                               fieldMap=fieldMap, +                                               extent=extent)              layer_count += 1              lyr_dst = None              lyr_src = None  | 
