Add progressive web app companion for cross-platform access
Vite + TypeScript PWA that mirrors the Android app's core features: - Pre-processed shelter data (build-time UTM33N→WGS84 conversion) - Leaflet map with shelter markers, user location, and offline tiles - Canvas compass arrow (ported from DirectionArrowView.kt) - IndexedDB shelter cache with 7-day staleness check - Service worker with CacheFirst tiles and precached app shell - i18n for en, nb, nn (ported from Android strings.xml) - iOS/Android compass handling with low-pass filter - Respects user map interaction (no auto-snap on pan/zoom) - Build revision cache-breaker for reliable SW updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
46365b713b
commit
e8428de775
12051 changed files with 1799735 additions and 0 deletions
140
pwa/node_modules/leaflet.offline/test/TileManagerTest.ts
generated
vendored
Normal file
140
pwa/node_modules/leaflet.offline/test/TileManagerTest.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/* global describe, it, beforeEach */
|
||||
import { point, bounds, gridLayer } from 'leaflet';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { assert } from 'chai';
|
||||
|
||||
import {
|
||||
downloadTile,
|
||||
getStorageInfo,
|
||||
getStorageLength,
|
||||
getStoredTilesAsJson,
|
||||
getTileImageSource,
|
||||
getTilePoints,
|
||||
hasTile,
|
||||
removeTile,
|
||||
saveTile,
|
||||
truncate,
|
||||
} from '../src/TileManager';
|
||||
|
||||
const testTileInfo = {
|
||||
url: 'https://api.tiles.mapbox.com/v4/mapbox.streets/16/33677/21651.png?access_token=xyz',
|
||||
key: 'https://api.tiles.mapbox.com/v4/mapbox.streets/16/33677/21651.png?access_token=xyz',
|
||||
x: 33677,
|
||||
y: 21651,
|
||||
z: 16,
|
||||
urlTemplate:
|
||||
'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}',
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
describe('manage tile storage', () => {
|
||||
before(() => {
|
||||
fetchMock.mockGlobal();
|
||||
});
|
||||
after(() => {
|
||||
fetchMock.unmockGlobal();
|
||||
});
|
||||
beforeEach(() => truncate());
|
||||
|
||||
it('saves a tile', () =>
|
||||
saveTile(testTileInfo, new Blob()).then((r) => {
|
||||
assert.equal(
|
||||
r,
|
||||
'https://api.tiles.mapbox.com/v4/mapbox.streets/16/33677/21651.png?access_token=xyz',
|
||||
);
|
||||
}));
|
||||
|
||||
it('will return empty storageinfo when no tiles are stored', async () => {
|
||||
const info = await getStorageInfo(testTileInfo.urlTemplate);
|
||||
assert.lengthOf(info, 0);
|
||||
});
|
||||
|
||||
it('will return storageinfo with single saved tile', async () => {
|
||||
await saveTile(testTileInfo, new Blob());
|
||||
const info = await getStorageInfo(testTileInfo.urlTemplate);
|
||||
assert.lengthOf(info, 1);
|
||||
const { blob, ...expectedInfo } = info[0];
|
||||
assert.deepEqual(expectedInfo, testTileInfo);
|
||||
});
|
||||
|
||||
it('will return empty storageinfo for other url template', async () => {
|
||||
await saveTile(testTileInfo, new Blob());
|
||||
const info = await getStorageInfo(
|
||||
'http://someotherexample/{z}/{x}/{y}.png',
|
||||
);
|
||||
assert.lengthOf(info, 0);
|
||||
});
|
||||
|
||||
it('will return length 0 on an empty db', async () => {
|
||||
const length = await getStorageLength();
|
||||
assert.equal(length, 0);
|
||||
});
|
||||
|
||||
it('will calc tile points', () => {
|
||||
const minBound = point(0, 0);
|
||||
const maxBound = point(200, 200);
|
||||
const tilebounds = bounds(minBound, maxBound);
|
||||
const tilePoints = getTilePoints(tilebounds, point(256, 256));
|
||||
assert.lengthOf(tilePoints, 1);
|
||||
});
|
||||
|
||||
it('has tile finds tile by key', async () => {
|
||||
await saveTile(testTileInfo, new Blob());
|
||||
const result = await hasTile(testTileInfo.key);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
|
||||
it('deletes tile finds tile by key', async () => {
|
||||
await saveTile(testTileInfo, new Blob());
|
||||
await removeTile(testTileInfo.key);
|
||||
const result = await hasTile(testTileInfo.key);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
|
||||
it('Creates geojson with tiles', () => {
|
||||
const layer = gridLayer();
|
||||
const json = getStoredTilesAsJson(layer.getTileSize(), [testTileInfo]);
|
||||
assert.lengthOf(json.features, 1);
|
||||
const feature = json.features[0];
|
||||
assert.equal(feature.type, 'Feature');
|
||||
assert.equal(feature.geometry.type, 'Polygon');
|
||||
assert.lengthOf(feature.geometry.coordinates, 1);
|
||||
assert.lengthOf(feature.geometry.coordinates[0], 5);
|
||||
});
|
||||
|
||||
it('downloads a tile', async () => {
|
||||
const url = 'https://tile.openstreetmap.org/16/33700/21621.png';
|
||||
fetchMock.once(url, new Blob());
|
||||
const result = await downloadTile(url);
|
||||
assert.instanceOf(result, Blob);
|
||||
fetchMock.removeRoutes();
|
||||
});
|
||||
|
||||
it('downloading a tile throws if response is not successful', async () => {
|
||||
const url = 'https://tile.openstreetmap.org/16/33700/21621.png';
|
||||
let err;
|
||||
fetchMock.once(url, 400);
|
||||
try {
|
||||
await downloadTile(url);
|
||||
} catch (error) {
|
||||
err = error;
|
||||
}
|
||||
assert.instanceOf(err, Error);
|
||||
fetchMock.removeRoutes();
|
||||
});
|
||||
|
||||
it('get image src returns url if tile with key does not exist', async () => {
|
||||
const result = await getTileImageSource(testTileInfo.key, testTileInfo.url);
|
||||
assert.equal(result, testTileInfo.url);
|
||||
});
|
||||
|
||||
it('get image src returns dataSource url if tile key does exist', async () => {
|
||||
await saveTile(testTileInfo, new Blob());
|
||||
const result = await getTileImageSource(
|
||||
testTileInfo.key,
|
||||
'http://someurl/tile.png',
|
||||
);
|
||||
assert.isString(result);
|
||||
assert.isTrue(result.includes('blob:'));
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue