cache ArcGIS basemap tiles on disk for faster map loads
This commit is contained in:
parent
5f18e14631
commit
437100c433
5 changed files with 89 additions and 9 deletions
12
index.mjs
12
index.mjs
|
|
@ -5,7 +5,15 @@ import { readFile } from 'fs/promises';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import {
|
import {
|
||||||
weatherProxy, radarProxy, outlookProxy, mesonetProxy, forecastProxy, openMeteoProxy, rainViewerProxy,
|
weatherProxy,
|
||||||
|
radarProxy,
|
||||||
|
outlookProxy,
|
||||||
|
mesonetProxy,
|
||||||
|
forecastProxy,
|
||||||
|
openMeteoProxy,
|
||||||
|
rainViewerProxy,
|
||||||
|
arcGisServerProxy,
|
||||||
|
arcGisServicesProxy,
|
||||||
} from './proxy/handlers.mjs';
|
} from './proxy/handlers.mjs';
|
||||||
import playlist from './src/playlist.mjs';
|
import playlist from './src/playlist.mjs';
|
||||||
import OVERRIDES from './src/overrides.mjs';
|
import OVERRIDES from './src/overrides.mjs';
|
||||||
|
|
@ -256,6 +264,8 @@ if (!process.env?.STATIC) {
|
||||||
app.use('/forecast/', forecastProxy);
|
app.use('/forecast/', forecastProxy);
|
||||||
app.use('/open-meteo/', openMeteoProxy);
|
app.use('/open-meteo/', openMeteoProxy);
|
||||||
app.use('/rainviewer/', rainViewerProxy);
|
app.use('/rainviewer/', rainViewerProxy);
|
||||||
|
app.use('/arcgis-server/', arcGisServerProxy);
|
||||||
|
app.use('/arcgis-services/', arcGisServicesProxy);
|
||||||
|
|
||||||
// Playlist route is available in server mode (not in static mode)
|
// Playlist route is available in server mode (not in static mode)
|
||||||
app.get('/playlist.json', playlist);
|
app.get('/playlist.json', playlist);
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,18 @@ const STALE_TIME_LIMIT_MS = 3 * 60 * 60 * 1000;
|
||||||
const PERSISTED_HOSTS = new Set([
|
const PERSISTED_HOSTS = new Set([
|
||||||
'api.open-meteo.com',
|
'api.open-meteo.com',
|
||||||
'api.rainviewer.com',
|
'api.rainviewer.com',
|
||||||
|
'server.arcgisonline.com',
|
||||||
|
'services.arcgisonline.com',
|
||||||
|
]);
|
||||||
|
const BINARY_PERSISTED_HOSTS = new Set([
|
||||||
|
'server.arcgisonline.com',
|
||||||
|
'services.arcgisonline.com',
|
||||||
]);
|
]);
|
||||||
const HOST_FALLBACK_TTLS = {
|
const HOST_FALLBACK_TTLS = {
|
||||||
'api.open-meteo.com': 10 * 60,
|
'api.open-meteo.com': 10 * 60,
|
||||||
'api.rainviewer.com': 2 * 60,
|
'api.rainviewer.com': 2 * 60,
|
||||||
|
'server.arcgisonline.com': 7 * 24 * 60 * 60,
|
||||||
|
'services.arcgisonline.com': 7 * 24 * 60 * 60,
|
||||||
};
|
};
|
||||||
|
|
||||||
class HttpCache {
|
class HttpCache {
|
||||||
|
|
@ -55,16 +63,30 @@ class HttpCache {
|
||||||
return path.join(CACHE_DIR, `${HttpCache.hashKey(key)}.json`);
|
return path.join(CACHE_DIR, `${HttpCache.hashKey(key)}.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static shouldPersist(url, options, response) {
|
static getCacheBodyFilePath(key) {
|
||||||
if (options?.encoding === 'binary') return false;
|
return path.join(CACHE_DIR, `${HttpCache.hashKey(key)}.bin`);
|
||||||
if (typeof response?.data !== 'string') return false;
|
}
|
||||||
|
|
||||||
|
static getPersistedHost(url) {
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
return PERSISTED_HOSTS.has(parsedUrl.hostname);
|
return parsedUrl.hostname;
|
||||||
} catch {
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static shouldPersist(url, options, response) {
|
||||||
|
const host = HttpCache.getPersistedHost(url);
|
||||||
|
if (!host || !PERSISTED_HOSTS.has(host)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.encoding === 'binary') {
|
||||||
|
return BINARY_PERSISTED_HOSTS.has(host) && Buffer.isBuffer(response?.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof response?.data === 'string';
|
||||||
}
|
}
|
||||||
|
|
||||||
static getHostFallbackTtl(url) {
|
static getHostFallbackTtl(url) {
|
||||||
|
|
@ -96,10 +118,17 @@ class HttpCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (now > parsed.entry.expiry + STALE_TIME_LIMIT_MS) {
|
if (now > parsed.entry.expiry + STALE_TIME_LIMIT_MS) {
|
||||||
|
if (parsed.entry.binaryBody === true) {
|
||||||
|
await unlink(HttpCache.getCacheBodyFilePath(parsed.key)).catch(() => null);
|
||||||
|
}
|
||||||
await unlink(filePath);
|
await unlink(filePath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsed.entry.binaryBody === true) {
|
||||||
|
parsed.entry.data = await readFile(HttpCache.getCacheBodyFilePath(parsed.key));
|
||||||
|
}
|
||||||
|
|
||||||
this.cache.set(parsed.key, parsed.entry);
|
this.cache.set(parsed.key, parsed.entry);
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -110,6 +139,18 @@ class HttpCache {
|
||||||
static async persistEntry(key, entry) {
|
static async persistEntry(key, entry) {
|
||||||
try {
|
try {
|
||||||
await mkdir(CACHE_DIR, { recursive: true });
|
await mkdir(CACHE_DIR, { recursive: true });
|
||||||
|
|
||||||
|
if (Buffer.isBuffer(entry.data)) {
|
||||||
|
const metadata = {
|
||||||
|
...entry,
|
||||||
|
data: undefined,
|
||||||
|
binaryBody: true,
|
||||||
|
};
|
||||||
|
await writeFile(HttpCache.getCacheBodyFilePath(key), entry.data);
|
||||||
|
await writeFile(HttpCache.getCacheFilePath(key), JSON.stringify({ key, entry: metadata }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await writeFile(HttpCache.getCacheFilePath(key), JSON.stringify({ key, entry }));
|
await writeFile(HttpCache.getCacheFilePath(key), JSON.stringify({ key, entry }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`⚠️ Cache save | Failed to persist cache entry ${key}: ${error.message}`);
|
console.warn(`⚠️ Cache save | Failed to persist cache entry ${key}: ${error.message}`);
|
||||||
|
|
@ -118,6 +159,7 @@ class HttpCache {
|
||||||
|
|
||||||
static async deletePersistedEntry(key) {
|
static async deletePersistedEntry(key) {
|
||||||
try {
|
try {
|
||||||
|
await unlink(HttpCache.getCacheBodyFilePath(key)).catch(() => null);
|
||||||
await unlink(HttpCache.getCacheFilePath(key));
|
await unlink(HttpCache.getCacheFilePath(key));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== 'ENOENT') {
|
if (error.code !== 'ENOENT') {
|
||||||
|
|
|
||||||
|
|
@ -64,3 +64,19 @@ export const rainViewerProxy = async (req, res) => {
|
||||||
skipParams: ['u'],
|
skipParams: ['u'],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const arcGisServerProxy = async (req, res) => {
|
||||||
|
await cache.handleRequest(req, res, 'https://server.arcgisonline.com', {
|
||||||
|
serviceName: 'ArcGIS Server',
|
||||||
|
skipParams: ['u'],
|
||||||
|
encoding: 'binary',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const arcGisServicesProxy = async (req, res) => {
|
||||||
|
await cache.handleRequest(req, res, 'https://services.arcgisonline.com', {
|
||||||
|
serviceName: 'ArcGIS Services',
|
||||||
|
skipParams: ['u'],
|
||||||
|
encoding: 'binary',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,12 @@ import { getSmallIconFromWmoCode } from '../icons.mjs';
|
||||||
import { getOpenMeteoObservationSnapshot } from './weather.mjs';
|
import { getOpenMeteoObservationSnapshot } from './weather.mjs';
|
||||||
import { temperature } from './units.mjs';
|
import { temperature } from './units.mjs';
|
||||||
|
|
||||||
const BASE_MAP_URL = 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}';
|
const getBaseMapUrl = () => (window.WS4KP_SERVER_AVAILABLE
|
||||||
const BOUNDARY_MAP_URL = 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}';
|
? '/arcgis-server/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}'
|
||||||
|
: 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}');
|
||||||
|
const getBoundaryMapUrl = () => (window.WS4KP_SERVER_AVAILABLE
|
||||||
|
? '/arcgis-services/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}'
|
||||||
|
: 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}');
|
||||||
const DEFAULT_MAX_NEARBY_MARKERS = 7;
|
const DEFAULT_MAX_NEARBY_MARKERS = 7;
|
||||||
const MIN_CITY_DISTANCE_METERS = 25000;
|
const MIN_CITY_DISTANCE_METERS = 25000;
|
||||||
const MIN_MARKER_PIXEL_DISTANCE = 85;
|
const MIN_MARKER_PIXEL_DISTANCE = 85;
|
||||||
|
|
@ -26,14 +30,14 @@ const createMap = (mapElement) => window.L.map(mapElement, {
|
||||||
});
|
});
|
||||||
|
|
||||||
const addBaseLayers = (map) => {
|
const addBaseLayers = (map) => {
|
||||||
const baseLayer = window.L.tileLayer(BASE_MAP_URL, {
|
const baseLayer = window.L.tileLayer(getBaseMapUrl(), {
|
||||||
maxZoom: 10,
|
maxZoom: 10,
|
||||||
minZoom: 1,
|
minZoom: 1,
|
||||||
crossOrigin: true,
|
crossOrigin: true,
|
||||||
className: 'radar-base-layer',
|
className: 'radar-base-layer',
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
const boundaryLayer = window.L.tileLayer(BOUNDARY_MAP_URL, {
|
const boundaryLayer = window.L.tileLayer(getBoundaryMapUrl(), {
|
||||||
maxZoom: 10,
|
maxZoom: 10,
|
||||||
minZoom: 1,
|
minZoom: 1,
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,14 @@ const rewriteUrl = (_url) => {
|
||||||
url.protocol = window.location.protocol;
|
url.protocol = window.location.protocol;
|
||||||
url.host = window.location.host;
|
url.host = window.location.host;
|
||||||
url.pathname = `/rainviewer${url.pathname}`;
|
url.pathname = `/rainviewer${url.pathname}`;
|
||||||
|
} else if (url.origin === 'https://server.arcgisonline.com') {
|
||||||
|
url.protocol = window.location.protocol;
|
||||||
|
url.host = window.location.host;
|
||||||
|
url.pathname = `/arcgis-server${url.pathname}`;
|
||||||
|
} else if (url.origin === 'https://services.arcgisonline.com') {
|
||||||
|
url.protocol = window.location.protocol;
|
||||||
|
url.host = window.location.host;
|
||||||
|
url.pathname = `/arcgis-services${url.pathname}`;
|
||||||
} else if (typeof OVERRIDES !== 'undefined' && OVERRIDES?.RADAR_HOST && url.origin === `https://${OVERRIDES.RADAR_HOST}`) {
|
} else if (typeof OVERRIDES !== 'undefined' && OVERRIDES?.RADAR_HOST && url.origin === `https://${OVERRIDES.RADAR_HOST}`) {
|
||||||
// Handle override radar host
|
// Handle override radar host
|
||||||
url.protocol = window.location.protocol;
|
url.protocol = window.location.protocol;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue