From 297fc425448f8a980eca22a76088aeee10f54857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20der=20Kaap?= Date: Tue, 11 Feb 2025 17:16:31 +0100 Subject: [PATCH 1/2] Started on CRS testcase --- testdata/ETRS89andRDNAP/README.md | 2 - testdata/crs/README.md | 50 ++++++++++ testdata/crs/natpark.conf | 9 ++ testdata/crs/natpark.map | 146 ++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 testdata/crs/README.md create mode 100644 testdata/crs/natpark.conf create mode 100644 testdata/crs/natpark.map diff --git a/testdata/ETRS89andRDNAP/README.md b/testdata/ETRS89andRDNAP/README.md index 5ec2425..16ab29e 100644 --- a/testdata/ETRS89andRDNAP/README.md +++ b/testdata/ETRS89andRDNAP/README.md @@ -3,8 +3,6 @@ This is to test if RDNAPTRANS transformations are used properly. The test source data originates from NSGI. -TODO automate this test in the build - ## Run mapserver ### existing 7.6.4-patch5-2-buster-lighttpd diff --git a/testdata/crs/README.md b/testdata/crs/README.md new file mode 100644 index 0000000..31e73e0 --- /dev/null +++ b/testdata/crs/README.md @@ -0,0 +1,50 @@ +# ERTS89andRDNAP test + +This tests projecting WFS features in different coordinate systems, in particular: +- EPSG:3034 +- EPSG:3035 +- EPSG:4258 +- EPSG:4326 +- CRS:84 + +TODO automate this test in the build + +## Run mapserver + +### existing 7.6.4-patch5-2-buster-lighttpd + +serving etrs89 source + +```docker +docker run --rm -p 80:80 -v `pwd`/testdata/crs:/srv/data -e MAPSERVER_CONFIG_FILE=/srv/data/natpark.conf -e SERVICE_TYPE=wfs -e MS_MAPFILE=/srv/data/natpark.map pdok/mapserver:7.6.4-patch5-2-buster-lighttpd + +``` + +### local built 8 + +Warning: This docker build compiles dependencies and will take a long time when running for the first time +```docker +docker build --target NL -t pdok/mapserver:8-local-NL . +``` + +serving natpark source + +```docker +docker run --rm -p 80:80 -v `pwd`/testdata/crs:/srv/data -e MAPSERVER_CONFIG_FILE=/srv/data/natpark.conf -e SERVICE_TYPE=wfs -e MS_MAPFILE=/srv/data/natpark.map pdok/mapserver:8-local-NL +``` + +## Verify the output + + +```shell +IMAGE=pdok/mapserver:8-local-NL && \ +SOURCE_NAME=rd && \ +OUT_NAME=etrs89 && \ +OUT_EPSG=4258 && \ +docker run --rm -p 80:80 -v `pwd`/ETRS89andRDNAP:/srv/data \ + -e MAPSERVER_CONFIG_FILE=/srv/data/${SOURCE_NAME}.conf -e SERVICE_TYPE=wfs -e MS_MAPFILE=/srv/data/${SOURCE_NAME}.map --entrypoint=mapserv \ + "${IMAGE}" \ + -nh "QUERY_STRING=service=WFS&version=2.0.0&request=GetFeature&typeName=${SOURCE_NAME}&outputFormat=geojson&srsName=EPSG:${OUT_EPSG}" | \ + jq --arg crs "${OUT_NAME}" '.features | .[] | { id, x_dev: (.geometry.coordinates[0] - (.properties[$crs+"_x"]|tonumber)), y_dev: (.geometry.coordinates[1] - (.properties[$crs+"_y"]|tonumber)) } | {error: ((.x_dev|abs) > 0.001 or (.y_dev|abs) > 0.001 )} + .' | \ + jq -s 'group_by (.error)[] | {error: .[0].error, count: length}' +``` diff --git a/testdata/crs/natpark.conf b/testdata/crs/natpark.conf new file mode 100644 index 0000000..d0b4f62 --- /dev/null +++ b/testdata/crs/natpark.conf @@ -0,0 +1,9 @@ +CONFIG + ENV + MS_MAP_NO_PATH "true" + END + MAPS + MAP "/srv/data/natpark.map" + END + +END \ No newline at end of file diff --git a/testdata/crs/natpark.map b/testdata/crs/natpark.map new file mode 100644 index 0000000..a2626d7 --- /dev/null +++ b/testdata/crs/natpark.map @@ -0,0 +1,146 @@ +MAP +NAME "" # empty so ETF geonovum test doesn't try to test it +CONFIG "MS_ERRORFILE" "stderr" +EXTENT -25000 250000 280000 860000 +UNITS meters +STATUS ON +SIZE 1 1 # filler value, to prevent mapserver complaining in logs no width or height are set +#DEBUG 5 +PROJECTION +"init=epsg:28992" +END + +WEB +METADATA +"ows_enable_request" "*" +"ows_fees" "NONE" +"ows_contactorganization" "PDOK" +"ows_schemas_location" "http://schemas.opengis.net" +"ows_service_onlineresource" "https://service.pdok.nl/" +"ows_contactperson" "KlantContactCenter PDOK" +"ows_contactposition" "pointOfContact" +"ows_contactvoicetelephone" "" +"ows_contactfacsimiletelephone" "" +"ows_addresstype" "Work" +"ows_address" "" +"ows_city" "Apeldoorn" +"ows_stateorprovince" "" +"ows_postcode" "" +"ows_country" "Nederland" +"ows_contactelectronicmailaddress" "BeheerPDOK@kadaster.nl" +"ows_hoursofservice" "" +"ows_contactinstructions" "https://www.pdok.nl/contact" +"ows_role" "" +"ows_srs" "EPSG:28992 EPSG:25831 EPSG:25832 EPSG:3034 EPSG:3035 EPSG:3857 EPSG:4258 EPSG:4326" +"ows_accessconstraints" "otherRestrictions;http://creativecommons.org/publicdomain/mark/1.0/deed.nl;Geen beperkingen" +END +END + +OUTPUTFORMAT +NAME "GEOJSON" # format name (visible as format in the 1.0.0 capabilities) +DRIVER "OGR/GEOJSON" +MIMETYPE "application/json; subtype=geojson" +FORMATOPTION "STORAGE=stream" +FORMATOPTION "FORM=SIMPLE" +FORMATOPTION "USE_FEATUREID=true" +FORMATOPTION "LCO:ID_FIELD=fuuid" +FORMATOPTION "LCO:ID_TYPE=STRING" +FORMATOPTION "LCO:WRITE_BBOX=YES" +END + +OUTPUTFORMAT +NAME "JSON" +DRIVER "OGR/GEOJSON" +MIMETYPE "application/json" +FORMATOPTION "STORAGE=stream" +FORMATOPTION "FORM=SIMPLE" +FORMATOPTION "USE_FEATUREID=true" +FORMATOPTION "LCO:ID_FIELD=fuuid" +FORMATOPTION "LCO:ID_TYPE=STRING" +FORMATOPTION "LCO:WRITE_BBOX=YES" +END + +OUTPUTFORMAT +NAME "XML" +DRIVER "OGR/GML" +MIMETYPE "text/xml" +FORMATOPTION "STORAGE=stream" +FORMATOPTION "FORM=SIMPLE" +FORMATOPTION "USE_FEATUREID=true" +END + +OUTPUTFORMAT +NAME "GML3" +DRIVER "OGR/GML" +MIMETYPE "text/xml; subtype=gml/3.1.1" +FORMATOPTION "STORAGE=stream" +FORMATOPTION "FORM=SIMPLE" +FORMATOPTION "USE_FEATUREID=true" +END + +WEB +METADATA +"ows_title" "Nationale parken WFS" +"ows_abstract" "Dit bestand geeft de grenzen van de Nationale Parken weer, zoals die door de Secretarissen van de Parken zijn aangegeven in de periode augustus 2005 - november 2005, met een update in januari 2007 en in augustus 2007. Op de grenzen van NP Drents-Friese Wold volgt nog een correctie. Het bestand bevat in totaal 21 Nationale Parken: 18 door de Minister van LNV vastgestelde Parken (waarvan 1 in oprichting), 2 Particuliere Parken en 1 grensoverschrijdend Park, vastgesteld door het Commité van Ministers van de Benelux. Daarnaast komen in het GIS-bestand "uitwerkingsgebieden" voor. Dit zijn gebieden die bij voorkeur op termijn onderdeel moeten gaan uitmaken van een Nationaal Park, maar nu nog niet zijn aangewezen. Ze maken dan ook GEEN deel uit van het betreffende Park en hebben geen enkele status! De grenzen in het GIS-bestand kunnen afwijken van de grenzen in het BIP (Beheers- en Inrichtingsplan). Elk Nationaal Park heeft een BIP, dat ter goedkeuring wordt voorgelegd aan de Minister van LNV. Elk BIP bevat een kaart met de begrenzing van het Park. Die grens is een momentopname. In de loop der tijd kunnen de grenzen van een Park licht wijzigen door kleine aankopen of uitruil van gronden. Die wijzigingen worden bij periodieke updates doorgevoerd in het BIP. Een BIP kan daardoor achter- of juist vooruitlopen op de grenzen in dit bestand." +"ows_keywordlist" "Protected sites,Beschermde gebieden,HVD,Aardobservatie en milieu,infoFeatureAccessService" +"wfs_languages" "eng" #first default, values according ISO 639-2/B +"wfs_extent" "-25000 250000 280000 860000" +"wfs_namespace_prefix" "nationaleparken" +"wfs_namespace_uri" "http://nationaleparken.geonovum.nl" +"wfs_onlineresource" "https://service.pdok.nl/rvo/nationaleparken/wfs/v2_0" +"wfs_getfeature_formatlist" "GEOJSON,JSON,XML,GML3" # List of earlier defined outputformat names +"wfs_maxfeatures" "1000" +"wfs_maxfeatures_ignore_for_resulttype_hits" "true" +"wfs_storedqueries" "urn:x-inspire:storedQuery:nationaleparken:FullDataset" +"wfs_urn:x-inspire:storedQuery:nationaleparken:FullDataset_filedef" "/srv/data/config/storedquery_fulldataset.xml" +"wfs_inspire_metadataurl_href" "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=b87a0095-2ad7-4dbb-81a1-fed060df79e1" +"wfs_inspire_metadataurl_format" "application/vnd.ogc.csw.GetRecordByIdResponse_xml" +"wfs_inspire_capabilities" "url" +"wfs_inspire_dsid_code" "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=#MD_DataIdentification" +END +END + +LAYER +STATUS ON +NAME "nationaleparken" +CONNECTIONTYPE OGR +CONNECTION "/srv/data/natpark.gpkg" +DATA "natpark" +TYPE POLYGON +PROJECTION +"init=epsg:28992" # Define the source projection to enable reprojection +END +TEMPLATE void +METADATA +"wfs_title" "Nationale Parken" +"wfs_abstract" "Nationaleparken Beschermde gebieden" +"wfs_keywordlist" "Beschermde gebieden" +"wfs_srs" "EPSG:28992 EPSG:25831 EPSG:25832 EPSG:3034 EPSG:3035 EPSG:3857 EPSG:4258 EPSG:4326" +"wfs_extent" "-25000 250000 280000 860000" #DEFAULT !!! belangrijk, anders is performance slecht +"wfs_include_items" "all" # required for getfeatureinfo +"wfs_bbox_extended" "true" +"wfs_enable_request" "*" +"wfs_featureid" "puuid" +"wfs_geomtype" "MultiPolygon" +"wfs_use_default_extent_for_getfeature" "false" +"gml_include_items" "fuuid,objectid,naam,instrument,nr,datum,bron,fiat_secr,hectares" # required for getfeatureinfo +"gml_fuuid_alias" "fuuid" +"gml_objectid_alias" "objectid" +"gml_naam_alias" "naam" +"gml_instrument_alias" "instrument" +"gml_nr_alias" "nr" +"gml_datum_alias" "datum" +"gml_bron_alias" "bron" +"gml_fiat_secr_alias" "fiatSecr" +"gml_hectares_alias" "hectares" +"gml_featureid" "puuid" +"gml_exclude_items" "puuid" +"gml_geometries" "geom" +"gml_types" "auto" +"ows_metadataurl_type" "TC211" +"ows_metadataurl_format" "text/plain" +"ows_metadataurl_href" "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=4961d305-fbb5-426a-9ba3-53e1ca5f3b18" +END + +END # LAYER +END # MAP \ No newline at end of file From fde867d1edf1f57f48b653b6b11c4b9a3cdd5d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20van=20der=20Kaap?= Date: Wed, 12 Feb 2025 14:36:04 +0100 Subject: [PATCH 2/2] Added testcase and fixed warning from lighttpd --- .github/workflows/regression-test.yml | 24 +++++++++++++++++++++++ config/lighttpd.conf | 2 +- testdata/crs/README.md | 27 ++++++++++++++++---------- testdata/crs/natpark.gpkg | Bin 0 -> 98304 bytes 4 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 testdata/crs/natpark.gpkg diff --git a/.github/workflows/regression-test.yml b/.github/workflows/regression-test.yml index 6cb9e2f..bb8fc53 100644 --- a/.github/workflows/regression-test.yml +++ b/.github/workflows/regression-test.yml @@ -151,3 +151,27 @@ jobs: docker stop mapserver-rdnap-wfs exit $exit_code + - name: Regression test => Different CRS (RDNAPTRANS -> EPSG:4258) + run: | + # start mapserver + docker run --rm -d -p 8181:80 --name mapserver-crs -v `pwd`/testdata/crs:/srv/data -e MAPSERVER_CONFIG_FILE=/srv/data/natpark.conf -e SERVICE_TYPE=wfs -e MS_MAPFILE=/srv/data/natpark.map pdok/mapserver:local-nl + + # execute request + mkdir -p `pwd`/testdata/crs/actual + curl "http://localhost:8181/mapserver?service=WFS&request=GetFeature&count=1&version=2.0.0&outputFormat=application/json&typeName=nationaleparken&srsName=EPSG:4258" -sL > `pwd`/testdata/crs/actual/output.json + + # assert results are as expected + exit_code=0 + [ $(cat `pwd`/testdata/crs/actual/output.json | jq -r '.crs.properties.name') == "urn:ogc:def:crs:EPSG::4258" ] || exit_code=1; + [ $(cat `pwd`/testdata/crs/actual/output.json | jq -r '.bbox[0]' | xargs -I '{}' echo "scale=5;" "({}-4.3646379084)/1 == 0" | bc) ] || exit_code=1; + [ $(cat `pwd`/testdata/crs/actual/output.json | jq -r '.bbox[1]' | xargs -I '{}' echo "scale=5;" "({}-51.3620482342678)/1 == 0" | bc) ] || exit_code=1; + [ $(cat `pwd`/testdata/crs/actual/output.json | jq -r '.bbox[2]' | xargs -I '{}' echo "scale=5;" "({}-4.46528581228022)/1 == 0" | bc) ] || exit_code=1; + [ $(cat `pwd`/testdata/crs/actual/output.json | jq -r '.bbox[3]' | xargs -I '{}' echo "scale=5;" "({}-51.4268875774673)/1 == 0" | bc) ] || exit_code=1; + + # cleanup + rm -rf `pwd`/testdata/crs/actual + + # stop mapserver + docker stop mapserver-crs + + exit $exit_code \ No newline at end of file diff --git a/config/lighttpd.conf b/config/lighttpd.conf index e40710e..3f4cad4 100644 --- a/config/lighttpd.conf +++ b/config/lighttpd.conf @@ -1,8 +1,8 @@ server.modules += ( "mod_setenv" ) +server.modules += ( "mod_indexfile" ) server.modules += ( "mod_fastcgi" ) server.modules += ( "mod_rewrite" ) server.modules += ( "mod_magnet" ) -server.modules += ( "mod_indexfile" ) index-file.names = ( "index.html" ) server.document-root = "/var/www/" diff --git a/testdata/crs/README.md b/testdata/crs/README.md index 31e73e0..6eb5113 100644 --- a/testdata/crs/README.md +++ b/testdata/crs/README.md @@ -7,19 +7,19 @@ This tests projecting WFS features in different coordinate systems, in particula - EPSG:4326 - CRS:84 -TODO automate this test in the build +This dataset has only 1 feature to have a reduced size in the Git repository. ## Run mapserver ### existing 7.6.4-patch5-2-buster-lighttpd -serving etrs89 source - ```docker docker run --rm -p 80:80 -v `pwd`/testdata/crs:/srv/data -e MAPSERVER_CONFIG_FILE=/srv/data/natpark.conf -e SERVICE_TYPE=wfs -e MS_MAPFILE=/srv/data/natpark.map pdok/mapserver:7.6.4-patch5-2-buster-lighttpd ``` +The server then can be contact at `http://localhost:80/mapserver?request=GetCapabilities&service=WFS` + ### local built 8 Warning: This docker build compiles dependencies and will take a long time when running for the first time @@ -27,24 +27,31 @@ Warning: This docker build compiles dependencies and will take a long time when docker build --target NL -t pdok/mapserver:8-local-NL . ``` -serving natpark source +Serving Nationale Parken source ```docker docker run --rm -p 80:80 -v `pwd`/testdata/crs:/srv/data -e MAPSERVER_CONFIG_FILE=/srv/data/natpark.conf -e SERVICE_TYPE=wfs -e MS_MAPFILE=/srv/data/natpark.map pdok/mapserver:8-local-NL ``` +The server then can be contact at `http://localhost:80/mapserver?request=GetCapabilities&service=WFS` + ## Verify the output ```shell +exit_code=0 +mkdir -p `pwd`/testdata/crs/actual/; IMAGE=pdok/mapserver:8-local-NL && \ -SOURCE_NAME=rd && \ +SOURCE_NAME=natpark && \ OUT_NAME=etrs89 && \ OUT_EPSG=4258 && \ -docker run --rm -p 80:80 -v `pwd`/ETRS89andRDNAP:/srv/data \ - -e MAPSERVER_CONFIG_FILE=/srv/data/${SOURCE_NAME}.conf -e SERVICE_TYPE=wfs -e MS_MAPFILE=/srv/data/${SOURCE_NAME}.map --entrypoint=mapserv \ +docker run --rm -p 80:80 -v `pwd`/testdata/crs:/srv/data -e MAPSERVER_CONFIG_FILE=/srv/data/natpark.conf -e SERVICE_TYPE=wfs -e MS_MAPFILE=/srv/data/natpark.map --entrypoint=mapserv \ "${IMAGE}" \ - -nh "QUERY_STRING=service=WFS&version=2.0.0&request=GetFeature&typeName=${SOURCE_NAME}&outputFormat=geojson&srsName=EPSG:${OUT_EPSG}" | \ - jq --arg crs "${OUT_NAME}" '.features | .[] | { id, x_dev: (.geometry.coordinates[0] - (.properties[$crs+"_x"]|tonumber)), y_dev: (.geometry.coordinates[1] - (.properties[$crs+"_y"]|tonumber)) } | {error: ((.x_dev|abs) > 0.001 or (.y_dev|abs) > 0.001 )} + .' | \ - jq -s 'group_by (.error)[] | {error: .[0].error, count: length}' + -nh "QUERY_STRING=service=WFS&request=GetFeature&count=1&version=2.0.0&outputFormat=application/json&typeName=nationaleparken&srsName=EPSG:${OUT_EPSG}" > `pwd`/testdata/crs/actual/output.json; +[ $(cat `pwd`/testdata/crs/actual/output.json | jq -r '.crs.properties.name') == "urn:ogc:def:crs:EPSG::4258" ] || exit_code=1; +[ $(cat `pwd`/testdata/crs/actual/output.json | jq -r '.bbox[0]' | xargs -I '{}' echo "scale=5;" "({}-4.3646379084)/1 == 0" | bc) ] || exit_code=1; +[ $(cat `pwd`/testdata/crs/actual/output.json | jq -r '.bbox[1]' | xargs -I '{}' echo "scale=5;" "({}-51.3620482342678)/1 == 0" | bc) ] || exit_code=1; +[ $(cat `pwd`/testdata/crs/actual/output.json | jq -r '.bbox[2]' | xargs -I '{}' echo "scale=5;" "({}-4.46528581228022)/1 == 0" | bc) ] || exit_code=1; +[ $(cat `pwd`/testdata/crs/actual/output.json | jq -r '.bbox[3]' | xargs -I '{}' echo "scale=5;" "({}-51.4268875774673)/1 == 0" | bc) ] || exit_code=1; +echo $exit_code ``` diff --git a/testdata/crs/natpark.gpkg b/testdata/crs/natpark.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..1afe07d3e9478d881a3286562b23b9afd6897a0f GIT binary patch literal 98304 zcmeI53w%`7oyYIw{SJsB$RIfkVlvGn%;X&qnIRb_BgssdOh8O=GR)j0BlAdRZcJbm z69O&Ts%@!{x~qUdw-2FGU1jaVZ0(D+4?kKhwpvmbbX!}i2(+kt*#9}_PBJr@KzM8g zKL7g(On&$OKj-|<_q^`8N$zb~-sq7;(h&&yoDwM)rU(j!peIBSgiQE11OA1N+3-zL zSiq-35DE`cZE9X-iZ~znZKhCq``yWKCL#9(JwGG&WByH~6Ml~bkN^@u0!RP}AOR$R z1dsp{Kmter3H(11m_oPqxw+Z!e^SOjgq+0e-8o(G4IW4U2_OL^fCP{L5z)|={D+Ej5IR2q}fYKk_h zH1!61Yg44ZO4HKZV6<7y^=+ypVkjhfiLRzXr_xlFS67$UR+s83D>Y@cweVS1qbsY? z7+UQOR-4(rs!c^>P-#@vT5W}D_3Aj5s-7!(7YL1%ZD$EAWe4K_oQ(QdT0sk~0fBlWsPN1($I2zoj_ewC)O3@B9T zs2*L;dE6c+buTTisje!ok&oIa_mIo!6&)Q;mlO!nI@ZFWt&aBT zaC$?cLv)5DkH1r;ftJu}wNbfn4?lE_F6L5ERnA;EL7ylE0iZ4(FD*xHxY-Qd#~3cc zXlSuRL$Z^W(OfN7o4p}k`vTfpiv6=f>LMX8t-#mncZ(e!zvw2Nqg+Xgt;J%s7<%G<5a&7$HTUB0GMJ@lt5yTq5QSSaKO}O#->43kJ z4GWbTX(s3Rl0Y8ztL?W{fb$5xufX7Yu z|BBq50({|t1dsp{Kmter2_OL^fCP{L5X#|8Mp0|_7jB!C2v01`j~NB{{S0VIF~kN^_6k_coZD%0gV z2e|*gk{TB4f&`EN5Iy2_OL^fCP{L501`j~NB{{S0VIF~kN^@u0!RP}oF@VN`Tu!>!(>PR2_OL^ zfCP{L5r^NFk4r1pGvDuJMYba9liv zh2(Zmzza1A5m&$;l7dc;Us@0ug3Iam2PCpaBwjHTB2t&rPqc+qSy;ZKV|&4T&4q#W&VjEiHB!LKZveUh9w~A8EFkn+ WSMbQBm+&v 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 uXh=i=8WLT&THSn23a$7mR&N