Skip to content

Implement native SQL Alchemy MVT #2022

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 28, 2025
Merged

Conversation

webb-ben
Copy link
Member

@webb-ben webb-ben commented May 30, 2025

Overview

This PR extends upon the work in #1979 to implement the full query using SQL alchemy functions instead of SQL text blocks.

I would like to implement better testing as well for tiles.

Related Issue / discussion

Additional information

Dependency policy (RFC2)

  • I have ensured that this PR meets RFC2 requirements

Updates to public demo

Contributions and licensing

(as per https://github.com/geopython/pygeoapi/blob/master/CONTRIBUTING.md#contributions-and-licensing)

  • I'd like to contribute [feature X|bugfix Y|docs|something else] to pygeoapi. I confirm that my contributions to pygeoapi will be compatible with the pygeoapi license guidelines at the time of contribution
  • I have already previously agreed to the pygeoapi Contributions and Licensing Guidelines

@webb-ben webb-ben requested a review from doublebyte1 May 30, 2025 03:12
@webb-ben webb-ben force-pushed the mvt-postgres branch 7 times, most recently from c813098 to 26b7ec8 Compare June 2, 2025 22:41
@webb-ben
Copy link
Member Author

webb-ben commented Jun 2, 2025

CI Failure unrelated to changes

@webb-ben webb-ben marked this pull request as ready for review June 2, 2025 23:23
@webb-ben webb-ben requested a review from a team June 2, 2025 23:23
@webb-ben webb-ben added OGC API - Tiles OGC API - Tiles ui User interface labels Jun 2, 2025
@tomkralidis tomkralidis self-requested a review June 3, 2025 10:57
@tomkralidis tomkralidis added this to the 0.21.0 milestone Jun 3, 2025
@webb-ben
Copy link
Member Author

webb-ben commented Jun 5, 2025

@ThorodanBrom I can't request your review but hoping you can validate this?

@ThorodanBrom
Copy link
Contributor

ThorodanBrom commented Jun 5, 2025 via email

@webb-ben webb-ben force-pushed the mvt-postgres branch 3 times, most recently from 1ab9ae2 to 690c0ed Compare June 10, 2025 15:54
webb-ben added 4 commits June 10, 2025 11:54
Fix typo

Rename base tile provider

Fix Tile HTML highlighting

Fix missing tile rename

Update CRS conversion

Change fallback for vector tile id

Undo rename tile provider

Reduce diff

Cleanup mvt-postgres provider

Re-add ST_CurveToLine

geopython@97ba024
@webb-ben
Copy link
Member Author

@tomkralidis @ThorodanBrom Added provider test and rebased. Should be ready for both of your reviews

Copy link
Contributor

@doublebyte1 doublebyte1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I hit the tile metadata endpoint:

http://localhost:5000/collections/ne_110m_populated_places_simple/tiles/WorldCRS84Quad/metadata

I am getting this error:

500 -
Traceback (most recent call last):
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 1536, in call
return self.wsgi_app(environ, start_response)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 1514, in wsgi_app
response = self.handle_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
return cors_after_request(app.make_response(f(*args, **kwargs)))
^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
return cors_after_request(app.make_response(f(*args, **kwargs)))
^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/pygeoapi/flask_app.py", line 357, in get_collection_tiles_metadata
return execute_from_flask(tiles_api.get_collection_tiles_metadata,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/pygeoapi/flask_app.py", line 153, in execute_from_flask
headers, status, content = api_function(actual_api, api_request, *args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/pygeoapi/api/tiles.py", line 317, in get_collection_tiles_metadata
tiles_metadata = p.get_metadata(
^^^^^^^^^^^^^^^
TypeError: BaseProvider.get_metadata() got an unexpected keyword argument 'dataset'

(you can reproduce my setup here: geopython/pygeoapi-examples#24)

@ThorodanBrom
Copy link
Contributor

ThorodanBrom commented Jun 15, 2025

If I hit the tile metadata endpoint:

http://localhost:5000/collections/ne_110m_populated_places_simple/tiles/WorldCRS84Quad/metadata

I am getting this error:

500 -
Traceback (most recent call last):
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 1536, in call
return self.wsgi_app(environ, start_response)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 1514, in wsgi_app
response = self.handle_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
return cors_after_request(app.make_response(f(*args, **kwargs)))
^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
return cors_after_request(app.make_response(f(*args, **kwargs)))
^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/pygeoapi/flask_app.py", line 357, in get_collection_tiles_metadata
return execute_from_flask(tiles_api.get_collection_tiles_metadata,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/pygeoapi/flask_app.py", line 153, in execute_from_flask
headers, status, content = api_function(actual_api, api_request, *args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/joana/git/tmp/pygeoapi/lib/python3.12/site-packages/pygeoapi/api/tiles.py", line 317, in get_collection_tiles_metadata
tiles_metadata = p.get_metadata(
^^^^^^^^^^^^^^^
TypeError: BaseProvider.get_metadata() got an unexpected keyword argument 'dataset'

(you can reproduce my setup here: geopython/pygeoapi-examples#24)

I think this is because the get_metadata method from BaseProvider is getting called instead of the one from BaseMVTProvider. This may be due to PostgreSQLProvider becoming a class field.

Changing the constructor order to MVTPostgreSQLProvider (BaseMVTProvider, PostgreSQLProvider) seems to fix the issue

@ThorodanBrom
Copy link
Contributor

A couple of bugs that I noticed:

  • Multiple features seem to get highlighted when clicked on the map on the pygeoapi UI. The current version does not seem to do this.
    Screenshot_2025-06-15_20-12-25
  • The fields/attributes are not returned as part of the MVT responses. This can't be tested using the pygeoapi UI, but on OpenLayers, only the layer name is returned.
    Updated version:
    Screenshot_2025-06-15_20-37-36
    Current version:
    Screenshot_2025-06-15_20-33-36

webb-ben added 6 commits June 15, 2025 18:40
- Fix get_metadata function
- Add unit test for get_metadata
- Fix MVT field selection
- Add test for vector tile properties
Bump GitHub Postgis engine to >3
@webb-ben webb-ben requested a review from doublebyte1 June 16, 2025 00:24
@webb-ben
Copy link
Member Author

@doublebyte1 @ThorodanBrom Believe I have addressed both of your comments and added corresponding unit tests

@webb-ben
Copy link
Member Author

I will note particularly long loading times to render the full table as MVT. There is not a strategy to drop features is there?

@ThorodanBrom
Copy link
Contributor

ThorodanBrom commented Jun 16, 2025

I will note particularly long loading times to render the full table as MVT. There is not a strategy to drop features is there?

I know that tippecanoe has logic for dropping features, so you can maybe check how they do it. I don't think pg_tileserv or Martin support dropping features. You can consider using LIMIT at lower zoom levels, but you then need to ensure that the tile output looks OK at those lower zoom levels.

Also, I've noticed that Leaflet is a bit slow for visualizing MVTs (which is what pygeoapi UI uses). OpenLayers is a bit faster when I've used it.

@ThorodanBrom
Copy link
Contributor

@webb-ben, I think WorldCRS84Quad tiles don't work properly. This is an issue from the original PR #1979. I've made a PR #2042 to fix it on master.

@webb-ben
Copy link
Member Author

webb-ben commented Jun 16, 2025

@webb-ben, I think WorldCRS84Quad tiles don't work properly. This is an issue from the original PR #1979. I've made a PR #2042 to fix it on master.

Can you confirm you have the correct configuration for storage_crs? The default storage_crs config option is http://www.opengis.net/def/crs/OGC/1.3/CRS84 but often Postgis geometry SRID is http://www.opengis.net/def/crs/EPSG/0/4326 with lat/lon switched.

With this configuration:

          - type: tile
            name: MVT-postgresql
            data:
                host: localhost
                dbname: test
                user: postgres
                password: postgres
                search_path: [osm, public]
            id_field: osm_id
            table: hotosm_bdi_waterways
            geom_field: foo_geom
            storage_crs: http://www.opengis.net/def/crs/EPSG/0/4326
            options:
                zoom:
                    min: 5
                    max: 15
            format:
                name: pbf
                mimetype: application/vnd.mapbox-vector-tile

I see:
image
image

@webb-ben
Copy link
Member Author

webb-ben commented Jun 16, 2025

I will note particularly long loading times to render the full table as MVT. There is not a strategy to drop features is there?

I know that tippecanoe has logic for dropping features, so you can maybe check how they do it. I don't think pg_tileserv or Martin support dropping features. You can consider using LIMIT at lower zoom levels, but you then need to ensure that the tile output looks OK at those lower zoom levels.

Also, I've noticed that Leaflet is a bit slow for visualizing MVTs (which is what pygeoapi UI uses). OpenLayers is a bit faster when I've used it.

@tomkralidis / @doublebyte1 / @ThorodanBrom I have a change (webb-ben@d32c6f9) that models the configuration for pygeoapi limit to control the maximum number of features in a single tile, ordered by the size of the feature. I will wait to include this as a separate PR unless directed otherwise. It could be valuable to combine spatial functions and property based ranking to better preserve only the most prominent features at low zoom.

          - type: tile
            name: MVT-postgresql
            ...
            options:
                limits:
                    max_items: 1000

@ThorodanBrom
Copy link
Contributor

ThorodanBrom commented Jun 16, 2025 via email

webb-ben added 3 commits June 16, 2025 21:02
Prevent passing dict to SQL connection which throws an error because it cannot be cached
Align with  WorldCRS84Quad spec using geopython#2042
@webb-ben
Copy link
Member Author

@ThorodanBrom @doublebyte1 based on my understanding of WorldCRS84Quad, this PR should now implement the envelope logic in #2042

Add minimal support stretching EPSG3857 TileLayer  to EPSG4326. Really should be a CRS84 based layer like:

L.tileLayer.wms("https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi", {
    layers: "BlueMarble_ShadedRelief_Bathymetry",
    attribution: "NASA GIBS",
  })
  .addTo(map);
@ThorodanBrom
Copy link
Contributor

Commit 3384f69 now allows for WorldCRS84Quad tiles to be visible on the pygeoapi UI. The basemap does not align nicely with the tiles, but I think this is what the warning is for.

Screenshot_2025-06-27_13-57-39

I'd suggest that OpenLayers be used for tile display instead of the current Leaflet implementation, because OpenLayers supports both WebMercatorQuad and WorldCRS84Quad MVTs seamlessly. It also supports raster tiles in both TMSs as well.

If this is something desirable, I can work on it.

@tomkralidis
Copy link
Member

@ThorodanBrom the HTML/UI mapping library has a few considerations (example: CoverageJSON support, packaging/dependencies for Debian/Ubuntu/etc.). Suggest a separate issue for discussion.

@tomkralidis tomkralidis merged commit 350aec1 into geopython:master Jun 28, 2025
4 checks passed
@webb-ben webb-ben deleted the mvt-postgres branch June 30, 2025 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
OGC API - Tiles OGC API - Tiles ui User interface
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants