Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Support Esri JSON and server-side filters in EsriRESTConverterMixin (rest_format, rest_params["where"])
- Converter for Bavaria, Germany LPIS field blocks (de_by_block)
- Converter for Hesse, Germany LPIS reference parcels
- Converter for Saarland, Germany LPIS field blocks (de_sl_block)
- Fix parcel sizes written in scientific notation being read 10,000x too large (de_sl parser)
- Update vecorel-cli to v0.2.16:
- Converter output is sorted by Hilbert distance
- Commands exit with a non-zero exit code when they report a failure
Expand Down
4 changes: 3 additions & 1 deletion fiboa_cli/datasets/de_sl.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ def parse_flik(x):


def parse_size(x):
match = re.search(r"Size in ha: (\d+(\.\d+)?)+", x, re.I)
# Small parcels are written in scientific notation, e.g. "Size in ha: 1.0999999999999999E-4".
Comment thread
saidy-moregeo marked this conversation as resolved.
# Without the exponent the value is read as 1.1 ha instead of 1.1 m², a factor of 10,000.
match = re.search(r"Size in ha: (\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)", x, re.I)
return float(match.group(1)) if match else None


Expand Down
86 changes: 86 additions & 0 deletions fiboa_cli/datasets/de_sl_block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import re
from urllib.parse import urlencode

import requests
from vecorel_cli.conversion.admin import AdminConverterMixin

from ..conversion.convert_gml import gml_assure_columns
from ..conversion.fiboa_converter import FiboaBaseConverter
from .commons.de_iacs import DEIACSMixin
from .de_sl import parse_flik, parse_size

BASE_URL = "https://geoportal.saarland.de/gdi-sl/inspirewfs_Bodenbedeckung_LPIS"
PARAMS = {
"service": "WFS",
"version": "2.0.0",
"request": "GetFeature",
"typeNames": "lcv:LandCoverUnit",
}
# The WFS accepts larger pages, but 2500 keeps each response around 7 MB. It is also the ceiling
# of the limit allowlist on the OGC API - Features endpoint for the same data.
PAGE_SIZE = 2500


class DESLBlockConverter(AdminConverterMixin, DEIACSMixin, FiboaBaseConverter):
id = "de_sl_block"
admin_subdivision_code = "SL"
short_name = "Germany, Saarland (LPIS)"
title = "Field blocks for Saarland, Germany"
description = """
The reference parcels ("Referenzschläge") of the Saarland Land Parcel Identification System (LPIS),
the reference system for area-based agricultural payments. The data is transformed into the INSPIRE
"Land Cover" data model, so each parcel carries a land cover class from the national IACS code list
alongside its FLIK. The area given is the eligible parcel size recorded in InVeKoS, not the
geometric area of the polygon. The complementary application parcels ("Antragsschläge") are
published separately.
"""

provider = "Ministerium für Umwelt, Klima, Mobilität, Agrar und Verbraucherschutz <https://geoportal.saarland.de/spatial-objects/384>"
attribution = "© GDI-SL 2026, CC BY 4.0"
license = "CC-BY-4.0"

extensions = {"https://fiboa.org/flik-extension/v0.2.0/schema.yaml"}

columns = {
"geometry": "geometry",
"flik": ("flik", "id"), # derived in migrate(); unique
"area_type": "crop:code", # added in file_migration(), trimmed in migrate()
Comment thread
ivorbosloper marked this conversation as resolved.
"area": "metrics:area", # derived in migrate(); in hectares
}

def get_urls(self):
# numberReturned is always reported as 0 by this server, so the page count has to come
# from a hits request rather than from the responses themselves.
hits = requests.get(BASE_URL, params={**PARAMS, "resultType": "hits"})
hits.raise_for_status()
total = int(re.search(r'numberMatched="(\d+)"', hits.text).group(1))

query = urlencode({**PARAMS, "count": PAGE_SIZE})
return {
f"{BASE_URL}?{query}&startIndex={start}": f"de_sl_block_{start}.gml"
for start in range(0, total, PAGE_SIZE)
}

def file_migration(self, gdf, path, uri, layer=None):
# The land cover class is a nested xlink attribute, which the GML driver does not guess
# into its generated schema. GDAL separates the steps of an element path with "|".
return gml_assure_columns(
gdf,
path,
uri,
layer,
area_type={
"ElementPath": "landCoverObservation|LandCoverObservation|class@href",
"Type": "String",
"Width": 255,
},
)

def migrate(self, gdf):
# The FLIK and the size are both encoded in the INSPIRE description, e.g.
# "Size in ha: 0.11206, flik: DESLLI0000248744"
gdf["flik"] = gdf["description"].apply(parse_flik)
gdf["area"] = gdf["description"].apply(parse_size)
# …/codelist/de.iacs/AgriculturalAreaTypeValue/GL -> GL
gdf["area_type"] = gdf["area_type"].str.rsplit("/", n=1).str[-1]
return super().migrate(gdf)
Loading
Loading