1

I have an ionic angular app that uses online maps (OSM and Spanish IGN maps). It runs on Android (at the moment there is not an ios versión). Now, I need to develop the possibility of using offline maps. To do it I generated an .mbtiles file with OSM vector tiles of my área of interest (plus a number of low-zoom tiles of the rest of the world, just in case). The file size is around 200 MB and I host it in the internal storage of my Android device.

I can generate vector tiles from the .mbtiles file using SQLite. Then, as far as I know, I need a web server that can serve dynamic contents to the maps component of my app. And this is the point, I suspect that I must do it outside of my ionic app, maybe through nodejs.

So, I can have a web server and a separate ionic app. How can I start both together: I suspect I can not launch the web server from ionic: I suppose (I am not experienced with backend developent) I have to do it from nodejs.

I would appreciate any help on it.

1 Answer 1

0

Obviously it is possible to set up offline maps using a node.js web server to provide tiles from an .mbtiles file. However, I managed to set it up without any server. I used @capacitor-community/sqlite to extract tiles and serve them to openLayers. My code is

--- map.page.ts ----

async createMap() {
(...)
case 'offline':
  credits = '© MapTiler © OpenStreetMap contributors'
  await this.server.openMbtiles('offline.mbtiles');
  const olSource = await this.createSource();
  if (!olSource) return;
  olLayer = new VectorTileLayer({ source: olSource, style: vectorTileStyle });
break;
(...)
// Create map
this.map = new Map({
  target: 'map',
  layers: [olLayer, this.currentLayer, this.archivedLayer, this.multiLayer],
  view: new View({ center: currentPosition, zoom: 9 }),
  controls: [new Zoom(), new ScaleLine(), new Rotate(), new CustomControl(this.fs)],
});
(...)

createSource() {
  try {
    // Create vector tile source
    return new VectorTileSource({
      format: new MVT(),
      tileClass: VectorTile,
      tileGrid: new TileGrid({
        extent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34],
        resolutions: Array.from({ length: 20 }, (_, z) => 156543.03392804097 / Math.pow(2, z)),
        tileSize: [256, 256],
      }),
      // Tile load function
      tileLoadFunction: async (tile) => {
        const vectorTile = tile as VectorTile;
        const [z, x, y] = vectorTile.getTileCoord();
        try {
          // Get vector tile
          const rawData = await this.server.getVectorTile(z, x, y);
          if (!rawData?.byteLength) {
            vectorTile.setLoader(() => {});
            vectorTile.setState(TileState.EMPTY);
            return;
          }
          // Decompress
          const decompressed = pako.inflate(new Uint8Array(rawData));
          // Read features
          const features = new MVT().readFeatures(decompressed, {
            extent: vectorTile.extent ?? [-20037508.34, -20037508.34, 20037508.34, 20037508.34],
            featureProjection: 'EPSG:3857',
          });
          // Set features to vector tile
          vectorTile.setFeatures(features);
        } catch (error) {
          vectorTile.setState(TileState.ERROR);
        }
      },
      tileUrlFunction: ([z, x, y]) => `${z}/${x}/${y}`,
    });
  } catch (e) {
    console.error('Error in createSource:', e);
    return null;
  }
}

---- server.service.ts -----

async getVectorTile(zoom: number, x: number, y: number): Promise<ArrayBuffer | null> {
  console.log(`🔍 Trying to get vector tile z=${zoom}, x=${x}, y=${y}`);
  if (!this.db) {
    console.error('❌ Database connection is not open.');
    return null;
  }
  // Query the database for the tile using XYZ coordinates
  const resultXYZ = await this.db.query(
    `SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?;`,
    [zoom, x, y]  
  );
  if (resultXYZ?.values?.length) {
    console.log(`✅ Tile found: z=${zoom}, x=${x}, y=${y}`);
    const tileData = resultXYZ.values[0].tile_data;
    // Ensure tileData is returned as an ArrayBuffer
    if (tileData instanceof ArrayBuffer) {
      return tileData;
    } else if (Array.isArray(tileData)) {
      return new Uint8Array(tileData).buffer; // Convert array to ArrayBuffer
    } else {
      console.error(`❌ Unexpected tile_data format for ${zoom}/${x}/${y}`, tileData);
      return null;
    }
  } else {
    console.log(`❌ No tile found: z=${zoom}, x=${x}, y=${y}`);
    return null;
  }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.