protomaps / PMTiles

Cloud-optimized + compressed single-file tile archives for vector and raster maps
https://protomaps.com/docs/pmtiles/
BSD 3-Clause "New" or "Revised" License
2.07k stars 121 forks source link

ol-pmtiles and FileSource #443

Closed prusswan closed 1 month ago

prusswan commented 1 month ago

While trying to load tiles from FileSource in OpenLayers using the existing examples, I had to modify PMTilesVectorSource (which assumes FetchSource). Below is how I fixed it, but maybe Source should be passed in instead?


      const TileState = {
        "LOADED": 2,
        "ERROR": 3,
        "EMPTY": 4,
      }

      var mvtFormat = new ol.format.MVT({
        featureClass: ol.Feature,
        geometryName: 'geom', // for overture tiles + ol.Feature inspection
      });

      const response = await fetch(base64content);
      const blob = await response.blob();

      const file = new File(
          [blob],
          "fileName.pmtiles",
          {
            type: "application/vnd.pmtiles",
            lastModified: new Date()
          }
      );
      var fs = new pmtiles.FileSource(file);

      class PMTilesFileSource extends ol.source.VectorTile {
        tileLoadFunction = (tile, url) => {
          // the URL construction is done internally by OL, so we need to parse it
          // back out here using a hacky regex
          const re = new RegExp(/pmtiles:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/);
          const result = url.match(re);
          const z = +result[2];
          const x = +result[3];
          const y = +result[4];

          tile.setLoader((extent, resolution, projection) => {
            this.pmtiles_
              .getZxy(z, x, y)
              .then((tile_result) => {
                //console.log([z,x,y], tile_result);
                if (tile_result) {
                  const format = tile.getFormat();
                  //console.log("format", format);

                  var features = format.readFeatures(tile_result.data, {
                      extent: extent,
                      featureProjection: projection,
                  });

                  //var features = format.readFeatures(tile_result.data);

                  tile.setFeatures(features
                    /*
                    format.readFeatures(tile_result.data, {
                      extent: extent,
                      featureProjection: projection,
                    }),*/
                  );
                  console.log([z,x,y], "loaded!", tile_result, features);
                  tile.setState(TileState.LOADED);
                } else {
                  console.log([z,x,y],"cleared!");
                  tile.setFeatures([]);
                  tile.setState(TileState.EMPTY);
                }
              })
              .catch((err) => {
                tile.setFeatures([]);
                tile.setState(TileState.ERROR);
                console.log("err", TileState.ERROR, err);
              });
          });
        };

        constructor(options) {
          console.log("options", options.format);
          super({
            ...options,
            ...{
              state: "loading",
              url: "pmtiles://" + options.url + "/{z}/{x}/{y}",
              format: options.format || new ol.format.MVT()
            },
          });

          /* todo: should the Source be passed in as an option instead? since it could be FileSource or FetchSource
          const fetchSource = new pmtiles.FetchSource(
            options.url,
            new Headers(options.headers),
          );
          */
          if ("fileSource" in options) {
            console.log("fileSource found");
            // https://pmtiles.io/typedoc/classes/PMTiles.html
            this.pmtiles_ = new pmtiles.PMTiles(options.fileSource);
          }
          else {
            this.pmtiles_ = new pmtiles.PMTiles(fetchSource);
          }
          this.pmtiles_.getHeader().then((h) => {
            this.tileGrid.minZoom = h.minZoom;
            this.tileGrid.maxZoom = h.maxZoom;
            this.setTileLoadFunction(this.tileLoadFunction);
            this.setState("ready");
          });
        }
      }

      const localVectorLayer = new ol.layer.VectorTile({
        title: "local/inline PMTiles (from FileSource)",
        declutter: false, //true,
        renderMode: 'vector',

        //source: new olpmtiles.PMTilesVectorSource({
        source: new PMTilesFileSource({
          format: mvtFormat, 
          fileSource: fs,
        }),
        style: new ol.style.Style({
          /* ... omitted */
        })
     });

Related: #407

bdon commented 1 month ago

Yeah it ought to take source as a parameter.

I would like to improve the OL library, it's ugly to work on as-is because I am maintaining the IIFE (script-includes) for OL as a separate file. Are you using a bundler with OpenLayers? I would prefer to kill the IIFE build and then we can put out a new version that uses TypeScript, etc.

prusswan commented 1 month ago

Yeah it ought to take source as a parameter.

I would like to improve the OL library, it's ugly to work on as-is because I am maintaining the IIFE (script-includes) for OL as a separate file. Are you using a bundler with OpenLayers? I would prefer to kill the IIFE build and then we can put out a new version that uses TypeScript, etc.

It depends, if I am using it as part of a NodeJS/web application, then bundler is an option. But if it is more like some HTML or static content that is embedded into something that isn't NodeJS (e.g. folium, Jupyter, or even DuckDB), something that can be used easily without bundler (e.g. like plain Leaflet.js) is much more convenient.

Maybe the ol-pmtiles functionality can be folded into pmtiles.js + examples?

bdon commented 1 month ago

the url option for ol-pmtiles 1.0.0 can now either be a string or a pmtiles.Source.

https://www.npmjs.com/package/ol-pmtiles

Let me know if that resolve this.