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.
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;
}
}