@signalk/charts-plugin 3.7.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +1 -0
- package/CHANGELOG.md +109 -0
- package/README.md +72 -0
- package/docs/screenshots/1_overview.png +0 -0
- package/docs/screenshots/2_region.png +0 -0
- package/docs/screenshots/3_jobs.png +0 -0
- package/package.json +13 -8
- package/plugin/cacheLayout.d.ts +72 -0
- package/plugin/cacheLayout.d.ts.map +1 -0
- package/plugin/cacheLayout.js +243 -0
- package/plugin/cacheLayout.js.map +1 -0
- package/plugin/chartDownloader.d.ts +122 -29
- package/plugin/chartDownloader.d.ts.map +1 -1
- package/plugin/chartDownloader.js +488 -313
- package/plugin/chartDownloader.js.map +1 -1
- package/plugin/chartDownloaderMBTilesHelpers.d.ts +10 -0
- package/plugin/chartDownloaderMBTilesHelpers.d.ts.map +1 -0
- package/plugin/chartDownloaderMBTilesHelpers.js +235 -0
- package/plugin/chartDownloaderMBTilesHelpers.js.map +1 -0
- package/plugin/chartDownloaderTileHelpers.d.ts +30 -0
- package/plugin/chartDownloaderTileHelpers.d.ts.map +1 -0
- package/plugin/chartDownloaderTileHelpers.js +277 -0
- package/plugin/chartDownloaderTileHelpers.js.map +1 -0
- package/plugin/charts.d.ts.map +1 -1
- package/plugin/charts.js +23 -5
- package/plugin/charts.js.map +1 -1
- package/plugin/constants.d.ts.map +1 -1
- package/plugin/index.d.ts.map +1 -1
- package/plugin/index.js +560 -47
- package/plugin/index.js.map +1 -1
- package/plugin/pluginStatus.js.map +1 -1
- package/plugin/projection.js.map +1 -1
- package/plugin/tileServer.d.ts +4 -2
- package/plugin/tileServer.d.ts.map +1 -1
- package/plugin/tileServer.js +37 -8
- package/plugin/tileServer.js.map +1 -1
- package/plugin/tokenProvider.d.ts +63 -0
- package/plugin/tokenProvider.d.ts.map +1 -0
- package/plugin/tokenProvider.js +258 -0
- package/plugin/tokenProvider.js.map +1 -0
- package/plugin/types.d.ts +37 -2
- package/plugin/types.d.ts.map +1 -1
- package/public/images/jobs.png +0 -0
- package/public/images/overview.png +0 -0
- package/public/images/region.png +0 -0
- package/public/index.html +392 -113
- package/public/js/index.js +620 -0
- package/public/leaflet/images/downloads.png +0 -0
- package/public/leaflet/images/layers-2x.png +0 -0
- package/public/leaflet/images/layers.png +0 -0
- package/public/leaflet/images/lifebuoy.png +0 -0
- package/public/leaflet/images/spritesheet-2x.png +0 -0
- package/public/leaflet/images/spritesheet.svg +156 -0
- package/public/leaflet/leaflet.css +677 -0
- package/public/leaflet/leaflet.draw.css +253 -0
- package/public/leaflet/leaflet.draw.js +2957 -0
- package/public/leaflet/leaflet.js +7467 -0
- package/public/style.css +351 -37
- package/docs/screenshots/1_placeholder.png +0 -0
- package/public/index.js +0 -349
|
@@ -3,38 +3,98 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ChartDownloader = exports.ChartSeedingManager = exports.
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
6
|
+
exports.ChartDownloader = exports.ChartSeedingManager = exports.FailureKind = exports.TileSource = exports.State = void 0;
|
|
8
7
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const p_limit_1 = __importDefault(require("p-limit"));
|
|
10
|
-
const geojson_antimeridian_cut_1 = __importDefault(require("geojson-antimeridian-cut"));
|
|
11
|
-
const boolean_intersects_1 = __importDefault(require("@turf/boolean-intersects"));
|
|
12
|
-
const bbox_1 = require("@turf/bbox");
|
|
13
|
-
const helpers_1 = require("@turf/helpers");
|
|
14
8
|
const check_disk_space_1 = __importDefault(require("check-disk-space"));
|
|
15
9
|
const projection_1 = require("./projection");
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
const cacheLayout_1 = require("./cacheLayout");
|
|
11
|
+
// All DB-touching helpers (delete / purge / vacuum / region-tile listing) need
|
|
12
|
+
// the raw node:sqlite handle exposed by @signalk/mbtiles. Reaching through
|
|
13
|
+
// `provider._mbtilesHandle!._db!` worked but lied about the contract: a proxy
|
|
14
|
+
// provider whose mbtiles file failed to open at startup has neither, and the
|
|
15
|
+
// non-null assertions papered over the resulting TypeError. Centralising here
|
|
16
|
+
// gives a single descriptive failure surface and removes seven `!` operators
|
|
17
|
+
// from call sites.
|
|
18
|
+
function requireMbtilesDb(provider) {
|
|
19
|
+
const db = provider._mbtilesHandle?._db;
|
|
20
|
+
if (!db) {
|
|
21
|
+
throw new Error(`Provider "${provider.identifier}": mbtiles cache not open. ` +
|
|
22
|
+
`Configure the provider with proxy:true and ensure the cache path ` +
|
|
23
|
+
`is writable; this operation needs the raw SQLite handle.`);
|
|
24
|
+
}
|
|
25
|
+
return db;
|
|
26
|
+
}
|
|
27
|
+
const chartDownloaderTileHelpers_1 = require("./chartDownloaderTileHelpers");
|
|
28
|
+
const chartDownloaderMBTilesHelpers_1 = require("./chartDownloaderMBTilesHelpers");
|
|
29
|
+
var State;
|
|
30
|
+
(function (State) {
|
|
31
|
+
State[State["Stopped"] = 0] = "Stopped";
|
|
32
|
+
State[State["Running"] = 1] = "Running";
|
|
33
|
+
})(State || (exports.State = State = {}));
|
|
34
|
+
var JobType;
|
|
35
|
+
(function (JobType) {
|
|
36
|
+
JobType[JobType["None"] = 0] = "None";
|
|
37
|
+
JobType[JobType["Seed"] = 1] = "Seed";
|
|
38
|
+
JobType[JobType["Delete"] = 2] = "Delete";
|
|
39
|
+
})(JobType || (JobType = {}));
|
|
40
|
+
var TileSource;
|
|
41
|
+
(function (TileSource) {
|
|
42
|
+
TileSource[TileSource["None"] = 0] = "None";
|
|
43
|
+
TileSource[TileSource["FetchedFromCache"] = 1] = "FetchedFromCache";
|
|
44
|
+
TileSource[TileSource["FetchedFromRemote"] = 2] = "FetchedFromRemote";
|
|
45
|
+
})(TileSource || (exports.TileSource = TileSource = {}));
|
|
46
|
+
// Categorised failure kinds for tile fetches. Lets the worker keep
|
|
47
|
+
// per-job counts so an admin staring at "500000 failed" can tell whether
|
|
48
|
+
// the upstream is timing out, the auth is wrong, or the network is gone.
|
|
49
|
+
// Used in TileFetchResult.failure and surfaced via job.info().failures.
|
|
50
|
+
var FailureKind;
|
|
51
|
+
(function (FailureKind) {
|
|
52
|
+
FailureKind["None"] = "none";
|
|
53
|
+
FailureKind["Timeout"] = "timeout";
|
|
54
|
+
FailureKind["HttpClient"] = "http_4xx";
|
|
55
|
+
FailureKind["HttpServer"] = "http_5xx";
|
|
56
|
+
FailureKind["Network"] = "network";
|
|
57
|
+
FailureKind["NoRemoteUrl"] = "no_remote_url";
|
|
58
|
+
})(FailureKind || (exports.FailureKind = FailureKind = {}));
|
|
59
|
+
// Default tile-fetch timeout. Bumped from 5s to 10s — some legitimate
|
|
60
|
+
// tile providers (NOAA WMS, custom inland-water sources) take 5-15s on
|
|
61
|
+
// a cold-cache backend tile and a 5s default marked them as failures.
|
|
62
|
+
// Per-provider override available via OnlineChartProvider.timeoutMs and
|
|
63
|
+
// TokenProviderConfig.tile.timeoutMs (added on the same change).
|
|
64
|
+
const TILE_FETCH_TIMEOUT_MS = 10000;
|
|
65
|
+
// Default backoff before a single retry on a transient 5xx (502/503).
|
|
66
|
+
// Picked to be long enough that an upstream momentary blip clears, short
|
|
67
|
+
// enough that 32 workers don't all stall on a hard outage. Configurable
|
|
68
|
+
// at runtime via ChartDownloader.tileRetryBackoffMs (test harness sets
|
|
69
|
+
// it to 0 to keep mocked-503 tests fast).
|
|
70
|
+
const DEFAULT_TILE_RETRY_BACKOFF_MS = 1500;
|
|
22
71
|
class ChartSeedingManager {
|
|
23
72
|
static ActiveJobs = {};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Reset all module-level static state owned by ChartSeedingManager and
|
|
75
|
+
* ChartDownloader so a test harness can run from a known baseline.
|
|
76
|
+
*
|
|
77
|
+
* The plugin's static fields outlive a `stop()` / `start()` cycle in
|
|
78
|
+
* production (which is intended — process-global gates and ID sequences),
|
|
79
|
+
* but in a test process running 200+ tests in sequence the residue
|
|
80
|
+
* shows up as cross-test interference: nextJobId drifts upward, an
|
|
81
|
+
* earlier low-disk-space probe leaves CachingEnabled=false for 2s,
|
|
82
|
+
* stale CacheStatistics from one provider get seen by an unrelated
|
|
83
|
+
* test asserting "stats are an object".
|
|
84
|
+
*
|
|
85
|
+
* Production code never calls this. The test app's beforeEach does.
|
|
86
|
+
*/
|
|
87
|
+
static resetForTests() {
|
|
88
|
+
this.ActiveJobs = {};
|
|
89
|
+
ChartDownloader.resetForTests();
|
|
90
|
+
}
|
|
91
|
+
static async createJob(cachePath, provider, options, minZoom, maxZoom, feature = undefined, bbox = undefined) {
|
|
92
|
+
const downloader = new ChartDownloader(cachePath, provider, options);
|
|
37
93
|
this.ActiveJobs[downloader.ID] = downloader;
|
|
94
|
+
if (feature)
|
|
95
|
+
downloader.initalizeJobFromFeature(feature, minZoom, maxZoom);
|
|
96
|
+
else if (bbox)
|
|
97
|
+
downloader.initializeJobFromBBox(bbox, minZoom, maxZoom);
|
|
38
98
|
return downloader;
|
|
39
99
|
}
|
|
40
100
|
// Cancel all running jobs and clear the registry. Used when the plugin stops
|
|
@@ -48,206 +108,389 @@ class ChartSeedingManager {
|
|
|
48
108
|
}
|
|
49
109
|
exports.ChartSeedingManager = ChartSeedingManager;
|
|
50
110
|
class ChartDownloader {
|
|
51
|
-
|
|
52
|
-
chartsPath;
|
|
111
|
+
cachePath;
|
|
53
112
|
provider;
|
|
113
|
+
options;
|
|
54
114
|
static MINIMUM_FREE_DISK_SPACE = 1024 * 1024 * 1024; // 1 GB
|
|
55
115
|
static nextJobId = 1;
|
|
116
|
+
static CachingEnabled = true;
|
|
117
|
+
static CacheStatistics = new Map();
|
|
118
|
+
static TilesCached = 0;
|
|
119
|
+
static lastDiskSpaceCheck = 0;
|
|
120
|
+
static lastDiskSpaceResult = true;
|
|
121
|
+
// See DEFAULT_TILE_RETRY_BACKOFF_MS. Public so the test harness can drop
|
|
122
|
+
// it to 0; production code should treat it as read-only.
|
|
123
|
+
static tileRetryBackoffMs = DEFAULT_TILE_RETRY_BACKOFF_MS;
|
|
56
124
|
id = ChartDownloader.nextJobId++;
|
|
57
|
-
|
|
125
|
+
type = JobType.None;
|
|
126
|
+
state = State.Stopped;
|
|
127
|
+
status = 'Idle';
|
|
58
128
|
totalTiles = 0;
|
|
59
129
|
downloadedTiles = 0;
|
|
60
130
|
failedTiles = 0;
|
|
61
131
|
cachedTiles = 0;
|
|
62
|
-
|
|
132
|
+
deletedTiles = 0;
|
|
133
|
+
// Per-job failure histogram, keyed by FailureKind. Surfaced via
|
|
134
|
+
// info().failures so the UI / admin can tell timeouts apart from
|
|
135
|
+
// 5xx apart from auth failures without grepping logs. Only the
|
|
136
|
+
// first failure of each kind is logged in detail (sampleLogged)
|
|
137
|
+
// so a job with 500k failed tiles doesn't drown the log buffer.
|
|
138
|
+
failureCounts = {};
|
|
139
|
+
sampleLogged = new Set();
|
|
63
140
|
areaDescription = '';
|
|
64
141
|
cancelRequested = false;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.
|
|
142
|
+
regionName = 'Unnamed';
|
|
143
|
+
tiles;
|
|
144
|
+
tilesInDB;
|
|
145
|
+
constructor(cachePath, provider, options) {
|
|
146
|
+
this.cachePath = cachePath;
|
|
70
147
|
this.provider = provider;
|
|
148
|
+
this.options = options;
|
|
71
149
|
}
|
|
72
150
|
get ID() {
|
|
73
151
|
return this.id;
|
|
74
152
|
}
|
|
75
|
-
async
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
this.tilesToDownload = await this.filterCachedTiles(this.tiles);
|
|
88
|
-
this.status = Status.Stopped;
|
|
89
|
-
this.totalTiles = this.tiles.length;
|
|
90
|
-
this.cachedTiles = this.totalTiles - this.tilesToDownload.length;
|
|
91
|
-
this.areaDescription = `BBox: [${bbox.join(', ')}]`;
|
|
153
|
+
async initalizeJobFromFeature(feature, minZoom, maxZoom) {
|
|
154
|
+
const geojson = (0, chartDownloaderTileHelpers_1.convertFeatureToGeoJSON)(feature);
|
|
155
|
+
this.tiles = () => (0, chartDownloaderTileHelpers_1.getTilesForGeoJSON)(geojson, minZoom, maxZoom);
|
|
156
|
+
// Defer the requireMbtilesDb call to factory invocation: the handle may
|
|
157
|
+
// not be open yet when initialize* runs (e.g. createJob before the
|
|
158
|
+
// proxy mbtiles is reconciled), but it must be open by the time the
|
|
159
|
+
// generator is iterated.
|
|
160
|
+
this.tilesInDB = () => (0, chartDownloaderMBTilesHelpers_1.getMBTilesForPolygon)(requireMbtilesDb(this.provider), geojson, minZoom, maxZoom);
|
|
161
|
+
this.totalTiles = (0, chartDownloaderTileHelpers_1.countTilesAdaptiveIterative)(geojson, minZoom, maxZoom);
|
|
162
|
+
this.state = State.Stopped;
|
|
163
|
+
this.regionName = feature?.properties?.name ?? 'Unnamed';
|
|
164
|
+
this.areaDescription = `Region: ${this.regionName}`;
|
|
92
165
|
}
|
|
93
|
-
async
|
|
94
|
-
|
|
95
|
-
this.
|
|
96
|
-
this.
|
|
97
|
-
this.totalTiles =
|
|
98
|
-
this.
|
|
99
|
-
this.areaDescription = `
|
|
166
|
+
async initializeJobFromBBox(bbox, minZoom, maxZoom) {
|
|
167
|
+
const geojson = (0, chartDownloaderTileHelpers_1.convertBboxToGeoJSON)(bbox);
|
|
168
|
+
this.tiles = () => (0, chartDownloaderTileHelpers_1.getTilesForGeoJSON)(geojson, minZoom, maxZoom);
|
|
169
|
+
this.tilesInDB = () => (0, chartDownloaderMBTilesHelpers_1.getMBTilesForPolygon)(requireMbtilesDb(this.provider), geojson, minZoom, maxZoom);
|
|
170
|
+
this.totalTiles = (0, chartDownloaderTileHelpers_1.countTilesAdaptiveIterative)(geojson, minZoom, maxZoom);
|
|
171
|
+
this.state = State.Stopped;
|
|
172
|
+
this.areaDescription = `BBox: [${bbox
|
|
173
|
+
.map((v) => Number(v).toFixed(3))
|
|
174
|
+
.join(', ')}]`;
|
|
100
175
|
}
|
|
101
|
-
static DISK_CHECK_INTERVAL_MS = 30_000;
|
|
102
176
|
/**
|
|
103
177
|
* Download map tiles for a specific area.
|
|
104
178
|
*/
|
|
105
179
|
async seedCache() {
|
|
106
180
|
// Guard against double-start: a second call while Running would share
|
|
107
181
|
// counters and concurrency slots with the first and corrupt progress.
|
|
108
|
-
if (this.
|
|
182
|
+
if (this.state === State.Running)
|
|
109
183
|
return;
|
|
184
|
+
const concurrency = 32;
|
|
110
185
|
this.cancelRequested = false;
|
|
111
|
-
this.
|
|
112
|
-
this.tilesToDownload = await this.filterCachedTiles(this.tiles);
|
|
186
|
+
this.state = State.Running;
|
|
113
187
|
this.downloadedTiles = 0;
|
|
188
|
+
this.cachedTiles = 0;
|
|
114
189
|
this.failedTiles = 0;
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
190
|
+
this.failureCounts = {};
|
|
191
|
+
this.sampleLogged = new Set();
|
|
192
|
+
this.type = JobType.Seed;
|
|
193
|
+
console.log(`[charts-plugin] job ${this.id} started seeding ` +
|
|
194
|
+
`provider=${this.provider.identifier} ` +
|
|
195
|
+
`area="${this.areaDescription}" totalTiles=${this.totalTiles}`);
|
|
196
|
+
const tileIterator = this.tiles();
|
|
197
|
+
let generatorDone = false;
|
|
198
|
+
const nextTile = () => {
|
|
199
|
+
if (generatorDone || this.cancelRequested)
|
|
200
|
+
return null;
|
|
201
|
+
const { value, done } = tileIterator.next();
|
|
202
|
+
if (done) {
|
|
203
|
+
generatorDone = true;
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
};
|
|
208
|
+
const worker = async () => {
|
|
209
|
+
while (!this.cancelRequested) {
|
|
210
|
+
const tile = nextTile();
|
|
211
|
+
if (!tile)
|
|
212
|
+
return;
|
|
213
|
+
if (!ChartDownloader.CachingEnabled) {
|
|
214
|
+
this.cancelRequested = true;
|
|
215
|
+
this.status = 'Insufficient disk space, canceling job';
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
127
218
|
try {
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
219
|
+
const result = await ChartDownloader.getTileFromCacheOrRemote(this.cachePath, this.provider, tile, this.options.refetch);
|
|
220
|
+
if (result.source === TileSource.None) {
|
|
221
|
+
this.failedTiles++;
|
|
222
|
+
const kind = result.failure?.kind ?? FailureKind.Network;
|
|
223
|
+
this.failureCounts[kind] = (this.failureCounts[kind] ?? 0) + 1;
|
|
224
|
+
// Log the first instance of each failure kind in this job —
|
|
225
|
+
// gives an operator something to grep for ("job 42 timeout
|
|
226
|
+
// first at z=12 x=2057 y=1364 status=undefined") without
|
|
227
|
+
// 500k log lines if every tile fails.
|
|
228
|
+
if (!this.sampleLogged.has(kind)) {
|
|
229
|
+
this.sampleLogged.add(kind);
|
|
230
|
+
console.warn(`[charts-plugin] job ${this.id} ` +
|
|
231
|
+
`provider=${this.provider.identifier} ` +
|
|
232
|
+
`first ${kind} failure at z=${tile.z} x=${tile.x} y=${tile.y}` +
|
|
233
|
+
(result.failure?.status
|
|
234
|
+
? ` status=${result.failure.status}`
|
|
235
|
+
: ''));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (result.source === TileSource.FetchedFromCache) {
|
|
239
|
+
this.cachedTiles++;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
this.downloadedTiles++;
|
|
133
243
|
}
|
|
244
|
+
this.totalTiles = Math.max(this.totalTiles, this.downloadedTiles + this.failedTiles + this.cachedTiles);
|
|
134
245
|
}
|
|
135
246
|
catch (err) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
247
|
+
this.failedTiles++;
|
|
248
|
+
const msg = err.message ?? String(err);
|
|
249
|
+
this.failureCounts[FailureKind.Network] =
|
|
250
|
+
(this.failureCounts[FailureKind.Network] ?? 0) + 1;
|
|
251
|
+
if (!this.sampleLogged.has(FailureKind.Network)) {
|
|
252
|
+
this.sampleLogged.add(FailureKind.Network);
|
|
253
|
+
console.warn(`[charts-plugin] job ${this.id} ` +
|
|
254
|
+
`provider=${this.provider.identifier} ` +
|
|
255
|
+
`first uncaught failure at z=${tile.z} x=${tile.x} y=${tile.y}: ${msg}`);
|
|
256
|
+
}
|
|
139
257
|
}
|
|
140
258
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
259
|
+
};
|
|
260
|
+
this.status = 'Seeding';
|
|
261
|
+
const workers = Array.from({ length: concurrency }, () => worker());
|
|
262
|
+
await Promise.all(workers);
|
|
263
|
+
this.state = State.Stopped;
|
|
264
|
+
if (this.options.mbtiles) {
|
|
265
|
+
try {
|
|
266
|
+
this.status = 'Creating MBTiles';
|
|
267
|
+
const safeRegionName = this.regionName
|
|
268
|
+
.normalize('NFKC') // normalize unicode
|
|
269
|
+
.replace(/[^a-zA-Z0-9_-]/g, '_') // allow only safe chars
|
|
270
|
+
.replace(/_+/g, '_') // collapse repeats
|
|
271
|
+
.replace(/^_+|_+$/g, '') // trim underscores
|
|
272
|
+
.slice(0, 100); // limit length
|
|
273
|
+
// Snapshot exports go to ${cachePath}/exports/, which is excluded from
|
|
274
|
+
// the chart scanner — the file is a build artifact for offline transfer
|
|
275
|
+
// (USB stick to another SK server), not a chart the running plugin
|
|
276
|
+
// serves. Re-running the job overwrites the file freely; no scanner
|
|
277
|
+
// handle holds it open.
|
|
278
|
+
const baseDir = path_1.default.resolve(this.cachePath, cacheLayout_1.EXPORTS_DIR);
|
|
279
|
+
const filePath = path_1.default.resolve((0, cacheLayout_1.exportMbtilesPath)(this.cachePath, safeRegionName, this.provider.identifier));
|
|
280
|
+
// Same separator-aware check as the migrate endpoint: a naive
|
|
281
|
+
// startsWith would miss sibling-directory prefix collisions.
|
|
282
|
+
if (filePath !== baseDir && !filePath.startsWith(baseDir + path_1.default.sep)) {
|
|
283
|
+
throw new Error('Invalid path detected');
|
|
284
|
+
}
|
|
285
|
+
// The export copies the whole seeded set into a second mbtiles, so it
|
|
286
|
+
// needs roughly as much free space again. Bail before opening the file
|
|
287
|
+
// rather than filling the disk and corrupting the working cache.
|
|
288
|
+
if (!(await ChartDownloader.hasDiskSpace(baseDir))) {
|
|
289
|
+
throw new Error('Insufficient disk space for mbtiles export');
|
|
290
|
+
}
|
|
291
|
+
const mbtiles = await (0, chartDownloaderMBTilesHelpers_1.openOrCreateMbtiles)(filePath, this.provider);
|
|
292
|
+
const iterator = this.tiles();
|
|
293
|
+
for (const tile of iterator) {
|
|
294
|
+
const buffer = await ChartDownloader.getTileFromMbTiles(this.provider._mbtilesHandle, tile);
|
|
295
|
+
if (buffer) {
|
|
296
|
+
await ChartDownloader.cacheTileToMbTiles(mbtiles, tile, buffer);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
mbtiles._db?.exec('PRAGMA wal_checkpoint(TRUNCATE);');
|
|
152
300
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
301
|
+
catch (err) {
|
|
302
|
+
// The tile seed itself already succeeded (state is Stopped); only the
|
|
303
|
+
// optional export failed. Record it and rethrow so the HTTP handler's
|
|
304
|
+
// .catch() can log it without an unhandled rejection crashing the
|
|
305
|
+
// server.
|
|
306
|
+
this.status = `Failed: ${err.message}`;
|
|
307
|
+
console.error(`[charts-plugin] job ${this.id} mbtiles export failed: ${err.message}`);
|
|
308
|
+
throw err;
|
|
161
309
|
}
|
|
162
310
|
}
|
|
163
|
-
this.status =
|
|
311
|
+
this.status = 'Completed';
|
|
312
|
+
console.log(`[charts-plugin] job ${this.id} completed ` +
|
|
313
|
+
`provider=${this.provider.identifier} ` +
|
|
314
|
+
`downloaded=${this.downloadedTiles} cached=${this.cachedTiles} ` +
|
|
315
|
+
`failed=${this.failedTiles} ` +
|
|
316
|
+
`failures=${JSON.stringify(this.failureCounts)}`);
|
|
164
317
|
}
|
|
165
318
|
async deleteCache() {
|
|
166
|
-
this.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
319
|
+
this.state = State.Running;
|
|
320
|
+
this.deletedTiles = 0;
|
|
321
|
+
this.type = JobType.Delete;
|
|
322
|
+
this.status = 'Deleting tiles';
|
|
323
|
+
console.log(`[charts-plugin] job ${this.id} delete started ` +
|
|
324
|
+
`provider=${this.provider.identifier} totalTiles=${this.totalTiles}`);
|
|
325
|
+
try {
|
|
326
|
+
const db = requireMbtilesDb(this.provider);
|
|
327
|
+
await (0, chartDownloaderMBTilesHelpers_1.deleteTilesInChunks)(db, this.tilesInDB(), 1000, (deleted) => {
|
|
328
|
+
console.log(`[charts-plugin] job ${this.id} deleted ${deleted} / ${this.totalTiles}`);
|
|
329
|
+
});
|
|
330
|
+
this.status = 'Purging orphaned images';
|
|
331
|
+
await (0, chartDownloaderMBTilesHelpers_1.purgeAllOrphanImages)(db, 1000, (deleted, totalDeleted) => {
|
|
332
|
+
this.deletedTiles += deleted;
|
|
333
|
+
console.log(`[charts-plugin] job ${this.id} purged ${totalDeleted} orphans ` +
|
|
334
|
+
`(last chunk ${deleted})`);
|
|
335
|
+
});
|
|
336
|
+
if (this.options.vacuum) {
|
|
337
|
+
this.status = 'Vacuuming MBTiles database';
|
|
338
|
+
(0, chartDownloaderMBTilesHelpers_1.vacuumMbtiles)(db);
|
|
179
339
|
}
|
|
340
|
+
this.status = 'Completed';
|
|
341
|
+
this.state = State.Stopped;
|
|
342
|
+
console.log(`[charts-plugin] job ${this.id} delete completed ` +
|
|
343
|
+
`provider=${this.provider.identifier} ` +
|
|
344
|
+
`deletedTiles=${this.deletedTiles}`);
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
// Record the failure on the job and reset state so the job doesn't sit
|
|
348
|
+
// wedged in 'Running' forever. Rethrow so callers / tests still observe
|
|
349
|
+
// it — the HTTP handler attaches a .catch() to keep the rejection from
|
|
350
|
+
// becoming an unhandled rejection that crashes the server.
|
|
351
|
+
this.state = State.Stopped;
|
|
352
|
+
this.status = `Failed: ${err.message}`;
|
|
353
|
+
console.error(`[charts-plugin] job ${this.id} delete failed: ${err.message}`);
|
|
354
|
+
throw err;
|
|
180
355
|
}
|
|
181
|
-
this.status = Status.Stopped;
|
|
182
356
|
}
|
|
183
357
|
cancelJob() {
|
|
184
358
|
this.cancelRequested = true;
|
|
185
359
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return null; // filter out cached tile
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
if (err.code === 'ENOENT') {
|
|
199
|
-
return tile; // file does not exist → uncached
|
|
360
|
+
info() {
|
|
361
|
+
const progress = () => {
|
|
362
|
+
if (this.totalTiles > 0) {
|
|
363
|
+
if (this.type === JobType.Seed) {
|
|
364
|
+
return ((this.downloadedTiles + this.cachedTiles + this.failedTiles) /
|
|
365
|
+
this.totalTiles);
|
|
366
|
+
}
|
|
367
|
+
else if (this.type === JobType.Delete) {
|
|
368
|
+
return this.deletedTiles / this.totalTiles;
|
|
200
369
|
}
|
|
201
|
-
console.error('Unexpected fs error:', err);
|
|
202
|
-
return tile; // treat unknown errors as uncached
|
|
203
370
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return results.filter((t) => t !== null);
|
|
207
|
-
}
|
|
208
|
-
info() {
|
|
371
|
+
return 0;
|
|
372
|
+
};
|
|
209
373
|
return {
|
|
210
374
|
id: this.id,
|
|
375
|
+
type: this.type,
|
|
211
376
|
chartName: this.provider.name,
|
|
212
377
|
regionName: this.areaDescription,
|
|
213
378
|
totalTiles: this.totalTiles,
|
|
214
379
|
downloadedTiles: this.downloadedTiles,
|
|
215
380
|
cachedTiles: this.cachedTiles,
|
|
216
381
|
failedTiles: this.failedTiles,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
382
|
+
// Per-failure-kind histogram. Empty {} for jobs with no failures.
|
|
383
|
+
// Surfaced so the UI / admin can see "1234 timeouts, 5 auth, 200 5xx"
|
|
384
|
+
// instead of an undifferentiated failedTiles count.
|
|
385
|
+
failures: { ...this.failureCounts },
|
|
386
|
+
deletedTiles: this.deletedTiles,
|
|
387
|
+
progress: progress(),
|
|
388
|
+
status: this.status,
|
|
389
|
+
state: this.state
|
|
222
390
|
};
|
|
223
391
|
}
|
|
224
|
-
static async getTileFromCacheOrRemote(chartsPath, provider, tile) {
|
|
225
|
-
|
|
392
|
+
static async getTileFromCacheOrRemote(chartsPath, provider, tile, overwrite = false) {
|
|
393
|
+
let stats = ChartDownloader.CacheStatistics.get(provider.identifier);
|
|
394
|
+
if (!stats) {
|
|
395
|
+
stats = { requests: 0, hits: 0, misses: 0, failures: 0 };
|
|
396
|
+
ChartDownloader.CacheStatistics.set(provider.identifier, stats);
|
|
397
|
+
}
|
|
398
|
+
stats.requests++;
|
|
399
|
+
// Try MBTiles cache first
|
|
400
|
+
if (!overwrite) {
|
|
401
|
+
const mbtilesBuffer = await ChartDownloader.getTileFromMbTiles(provider._mbtilesHandle, tile);
|
|
402
|
+
if (mbtilesBuffer) {
|
|
403
|
+
stats.hits++;
|
|
404
|
+
return { buffer: mbtilesBuffer, source: TileSource.FetchedFromCache };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Cache miss: fetch from remote
|
|
408
|
+
const { buffer, failure } = await ChartDownloader.fetchTileFromRemoteDetailed(provider, tile);
|
|
409
|
+
ChartDownloader.CachingEnabled =
|
|
410
|
+
await ChartDownloader.hasDiskSpace(chartsPath);
|
|
411
|
+
if (ChartDownloader.CachingEnabled && buffer) {
|
|
412
|
+
stats.misses++;
|
|
413
|
+
//Writing to mbtiles must be awaited
|
|
414
|
+
await ChartDownloader.cacheTileToMbTiles(provider._mbtilesHandle, tile, buffer);
|
|
415
|
+
return {
|
|
416
|
+
buffer,
|
|
417
|
+
source: TileSource.FetchedFromRemote,
|
|
418
|
+
failure: { kind: FailureKind.None }
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
stats.failures++;
|
|
422
|
+
return { buffer: null, source: TileSource.None, failure };
|
|
423
|
+
}
|
|
424
|
+
static async getTileFromMbTiles(mbtilesHandle, tile) {
|
|
425
|
+
if (!mbtilesHandle) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
226
428
|
try {
|
|
227
|
-
const
|
|
228
|
-
|
|
429
|
+
const cachedBuffer = await new Promise((resolve, reject) => {
|
|
430
|
+
mbtilesHandle.getTile(tile.z, tile.x, tile.y, (err, data) => {
|
|
431
|
+
if (err) {
|
|
432
|
+
// MBTiles returns an error when tile does not exist
|
|
433
|
+
if (err.message?.includes('Tile does not exist')) {
|
|
434
|
+
return resolve(null);
|
|
435
|
+
}
|
|
436
|
+
return reject(err);
|
|
437
|
+
}
|
|
438
|
+
resolve(data);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
if (cachedBuffer) {
|
|
442
|
+
return cachedBuffer;
|
|
443
|
+
}
|
|
229
444
|
}
|
|
230
445
|
catch (err) {
|
|
231
|
-
|
|
446
|
+
console.error('Failed to read tile from MBTiles cache:', err);
|
|
232
447
|
}
|
|
233
|
-
|
|
234
|
-
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
static async cacheTileToMbTiles(mbtilesHandle, tile, buffer) {
|
|
451
|
+
if (buffer && ChartDownloader.CachingEnabled) {
|
|
235
452
|
try {
|
|
236
|
-
await
|
|
237
|
-
|
|
453
|
+
await new Promise((resolve, reject) => {
|
|
454
|
+
mbtilesHandle.putTile(tile.z, tile.x, tile.y, buffer, (err) => {
|
|
455
|
+
if (err)
|
|
456
|
+
reject(err);
|
|
457
|
+
else
|
|
458
|
+
resolve();
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
ChartDownloader.TilesCached++;
|
|
238
462
|
}
|
|
239
463
|
catch (err) {
|
|
240
|
-
console.error(
|
|
464
|
+
console.error('Failed to write tile to mbtiles cache:', err);
|
|
241
465
|
}
|
|
242
466
|
}
|
|
243
|
-
return buffer;
|
|
244
467
|
}
|
|
245
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Detailed-result variant of fetchTileFromRemote. Returns the buffer on
|
|
470
|
+
* success and a `failure` discriminator on every other path so the
|
|
471
|
+
* caller can attribute "failed" tiles to a specific cause (timeout vs
|
|
472
|
+
* 5xx vs network) for diagnostic logs and per-job counters.
|
|
473
|
+
*
|
|
474
|
+
* Retries once on 502/503 with TILE_RETRY_BACKOFF_MS backoff: those are
|
|
475
|
+
* the response codes that genuinely benefit from one more try (LB hiccup,
|
|
476
|
+
* backend cold-start). 504 is excluded because it means the upstream
|
|
477
|
+
* already waited; another wait makes the per-tile latency intolerable.
|
|
478
|
+
* 4xx are not retried (auth or "tile genuinely doesn't exist" — neither
|
|
479
|
+
* gets better by trying again).
|
|
480
|
+
*/
|
|
481
|
+
static async fetchTileFromRemoteDetailed(provider, tile, timeoutMs = TILE_FETCH_TIMEOUT_MS) {
|
|
482
|
+
// Token providers fetch their token lazily; awaiting here ensures
|
|
483
|
+
// remoteUrl/headers below template against a non-stale token. Fast-path
|
|
484
|
+
// when the cached token is still inside its TTL: the call returns
|
|
485
|
+
// synchronously without I/O.
|
|
486
|
+
if (provider._tokenProvider) {
|
|
487
|
+
await provider._tokenProvider.ensureFreshToken();
|
|
488
|
+
}
|
|
246
489
|
// Local (non-proxy) providers have no remoteUrl; the POST /cache endpoint
|
|
247
490
|
// is open to any provider, so callers can still land here and should get
|
|
248
491
|
// a well-defined null rather than a crash.
|
|
249
492
|
if (!provider.remoteUrl) {
|
|
250
|
-
return null;
|
|
493
|
+
return { buffer: null, failure: { kind: FailureKind.NoRemoteUrl } };
|
|
251
494
|
}
|
|
252
495
|
let url = provider.remoteUrl
|
|
253
496
|
.replace('{z}', tile.z.toString())
|
|
@@ -272,175 +515,107 @@ class ChartDownloader {
|
|
|
272
515
|
url = url.replace('{bbox_3857}', `${mx1},${my1},${mx2},${my2}`);
|
|
273
516
|
}
|
|
274
517
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
headers: provider.headers,
|
|
280
|
-
signal: controller.signal
|
|
281
|
-
});
|
|
282
|
-
// Clear the abort timer as soon as the response head is in. A long body
|
|
283
|
-
// read was otherwise racing the timeout and could be aborted mid-stream
|
|
284
|
-
// while the caller waited on arrayBuffer().
|
|
285
|
-
clearTimeout(timeoutId);
|
|
286
|
-
if (!response.ok) {
|
|
287
|
-
return null;
|
|
518
|
+
let lastFailure = { kind: FailureKind.Network };
|
|
519
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
520
|
+
if (attempt > 0) {
|
|
521
|
+
await new Promise((r) => setTimeout(r, ChartDownloader.tileRetryBackoffMs));
|
|
288
522
|
}
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
523
|
+
const controller = new AbortController();
|
|
524
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
525
|
+
try {
|
|
526
|
+
const response = await fetch(url, {
|
|
527
|
+
headers: provider.headers,
|
|
528
|
+
signal: controller.signal
|
|
529
|
+
});
|
|
530
|
+
// Clear the abort timer as soon as the response head is in. A long
|
|
531
|
+
// body read was otherwise racing the timeout and could be aborted
|
|
532
|
+
// mid-stream while the caller waited on arrayBuffer().
|
|
533
|
+
clearTimeout(timeoutId);
|
|
534
|
+
if (response.ok) {
|
|
535
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
536
|
+
return {
|
|
537
|
+
buffer: Buffer.from(arrayBuffer),
|
|
538
|
+
failure: { kind: FailureKind.None }
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
// For token providers, an upstream 401/403 most likely means the
|
|
542
|
+
// cached token has expired or been revoked. Invalidate so the next
|
|
543
|
+
// fetch refreshes it. 5xx and other non-auth errors don't trigger
|
|
544
|
+
// this — they're not about credentials.
|
|
545
|
+
if ((response.status === 401 || response.status === 403) &&
|
|
546
|
+
provider._tokenProvider) {
|
|
547
|
+
provider._tokenProvider.invalidateToken();
|
|
307
548
|
}
|
|
549
|
+
const kind = response.status >= 500
|
|
550
|
+
? FailureKind.HttpServer
|
|
551
|
+
: FailureKind.HttpClient;
|
|
552
|
+
lastFailure = { kind, status: response.status };
|
|
553
|
+
// Retry only on 502/503 — those are the LB/backend-blip codes
|
|
554
|
+
// worth a second try. 504 already implies a long wait upstream;
|
|
555
|
+
// 4xx won't change on retry.
|
|
556
|
+
if (response.status === 502 || response.status === 503)
|
|
557
|
+
continue;
|
|
558
|
+
return { buffer: null, failure: lastFailure };
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
clearTimeout(timeoutId);
|
|
562
|
+
// AbortError vs other network errors: the timeout's controller.abort
|
|
563
|
+
// throws an AbortError. Treat any other thrown error as Network.
|
|
564
|
+
const isAbort = err.name === 'AbortError' ||
|
|
565
|
+
controller.signal.aborted;
|
|
566
|
+
lastFailure = {
|
|
567
|
+
kind: isAbort ? FailureKind.Timeout : FailureKind.Network
|
|
568
|
+
};
|
|
569
|
+
// Don't retry on timeout (already slow) or generic network error
|
|
570
|
+
// (likely DNS/route failure — won't recover in 1.5s).
|
|
571
|
+
return { buffer: null, failure: lastFailure };
|
|
308
572
|
}
|
|
309
573
|
}
|
|
310
|
-
return
|
|
574
|
+
return { buffer: null, failure: lastFailure };
|
|
311
575
|
}
|
|
312
576
|
/**
|
|
313
|
-
*
|
|
314
|
-
*
|
|
577
|
+
* Backwards-compatible wrapper for callers that only need the buffer.
|
|
578
|
+
* Internal callers should prefer fetchTileFromRemoteDetailed for the
|
|
579
|
+
* categorised failure information.
|
|
315
580
|
*/
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
const crossesAntiMeridian = minLon > maxLon;
|
|
320
|
-
// Respect the provider's minzoom: low zooms outside the provider's range
|
|
321
|
-
// would 404 from the remote and just inflate totalTiles.
|
|
322
|
-
const minZoom = Math.max(tileServer_1.MIN_ZOOM, this.provider.minzoom ?? tileServer_1.MIN_ZOOM);
|
|
323
|
-
// Helper to process a lon/lat box normally. lonLatToTileXY returns
|
|
324
|
-
// tile-Y increasing southward, so for a box with minLat < maxLat the
|
|
325
|
-
// south edge yields the larger tile-Y.
|
|
326
|
-
const processBBox = (lo1, la1, lo2, la2) => {
|
|
327
|
-
for (let z = minZoom; z <= maxZoom; z++) {
|
|
328
|
-
const [minX, maxY] = (0, projection_1.lonLatToTile)(lo1, la1, z); // SW corner
|
|
329
|
-
const [maxX, minY] = (0, projection_1.lonLatToTile)(lo2, la2, z); // NE corner
|
|
330
|
-
for (let x = minX; x <= maxX; x++) {
|
|
331
|
-
for (let y = minY; y <= maxY; y++) {
|
|
332
|
-
tiles.push({ x, y, z });
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
if (!crossesAntiMeridian) {
|
|
338
|
-
// normal
|
|
339
|
-
processBBox(minLon, minLat, maxLon, maxLat);
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
// crosses antimeridian — split into two boxes:
|
|
343
|
-
// [minLon -> 180] and [-180 -> maxLon]
|
|
344
|
-
processBBox(minLon, minLat, 180, maxLat);
|
|
345
|
-
processBBox(-180, minLat, maxLon, maxLat);
|
|
346
|
-
}
|
|
347
|
-
return tiles;
|
|
348
|
-
}
|
|
349
|
-
getTilesForGeoJSON(geojson, zoomMin = 1, zoomMax = 14) {
|
|
350
|
-
const tiles = [];
|
|
351
|
-
for (const feature of geojson.features) {
|
|
352
|
-
if (feature.geometry.type !== 'Polygon' &&
|
|
353
|
-
feature.geometry.type !== 'MultiPolygon') {
|
|
354
|
-
console.warn('Skipping non-polygon feature');
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
const boundingBox = (0, bbox_1.bbox)(feature.geometry); // [minX, minY, maxX, maxY]
|
|
358
|
-
for (let z = zoomMin; z <= zoomMax; z++) {
|
|
359
|
-
const [minX, minY] = (0, projection_1.lonLatToTile)(boundingBox[0], boundingBox[3], z); // top-left
|
|
360
|
-
const [maxX, maxY] = (0, projection_1.lonLatToTile)(boundingBox[2], boundingBox[1], z); // bottom-right
|
|
361
|
-
for (let x = minX; x <= maxX; x++) {
|
|
362
|
-
for (let y = minY; y <= maxY; y++) {
|
|
363
|
-
// Cheap AABB pre-filter avoids allocating a turf polygon and
|
|
364
|
-
// running booleanIntersects for tiles that can't possibly
|
|
365
|
-
// overlap the feature's bbox. Saves 90%+ of the turf work on
|
|
366
|
-
// concave regions.
|
|
367
|
-
const [tMinLon, tMinLat, tMaxLon, tMaxLat] = (0, projection_1.tileToBBox)(x, y, z);
|
|
368
|
-
if (tMaxLon < boundingBox[0] ||
|
|
369
|
-
tMinLon > boundingBox[2] ||
|
|
370
|
-
tMaxLat < boundingBox[1] ||
|
|
371
|
-
tMinLat > boundingBox[3]) {
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
const tilePoly = this.bboxPolygon([
|
|
375
|
-
tMinLon,
|
|
376
|
-
tMinLat,
|
|
377
|
-
tMaxLon,
|
|
378
|
-
tMaxLat
|
|
379
|
-
]);
|
|
380
|
-
if ((0, boolean_intersects_1.default)(feature, tilePoly)) {
|
|
381
|
-
tiles.push({ x, y, z });
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return tiles;
|
|
581
|
+
static async fetchTileFromRemote(provider, tile, timeoutMs = TILE_FETCH_TIMEOUT_MS) {
|
|
582
|
+
const { buffer } = await ChartDownloader.fetchTileFromRemoteDetailed(provider, tile, timeoutMs);
|
|
583
|
+
return buffer;
|
|
388
584
|
}
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
585
|
+
static async hasDiskSpace(path) {
|
|
586
|
+
const now = Date.now();
|
|
587
|
+
// cache for 2 seconds
|
|
588
|
+
if (now - ChartDownloader.lastDiskSpaceCheck < 2000) {
|
|
589
|
+
return ChartDownloader.lastDiskSpaceResult;
|
|
393
590
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
properties: {
|
|
399
|
-
name: region.name || '',
|
|
400
|
-
description: region.description || '',
|
|
401
|
-
timestamp: region.timestamp || '',
|
|
402
|
-
source: region.$source || '',
|
|
403
|
-
...(feature.properties || {})
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
const splitGeoFeature = (0, geojson_antimeridian_cut_1.default)(geoFeature);
|
|
407
|
-
const features = [];
|
|
408
|
-
const pushFeaturePolygon = (orig, coords, idx) => {
|
|
409
|
-
const poly = {
|
|
410
|
-
type: 'Feature',
|
|
411
|
-
id: idx != null && orig.id ? `${orig.id}-${idx}` : orig.id,
|
|
412
|
-
geometry: {
|
|
413
|
-
type: 'Polygon',
|
|
414
|
-
coordinates: coords
|
|
415
|
-
},
|
|
416
|
-
properties: orig.properties || {}
|
|
417
|
-
};
|
|
418
|
-
features.push(poly);
|
|
419
|
-
};
|
|
420
|
-
const f = splitGeoFeature;
|
|
421
|
-
if (f.geometry && f.geometry.type === 'MultiPolygon') {
|
|
422
|
-
const coords = f.geometry.coordinates;
|
|
423
|
-
coords.forEach((ring, i) => pushFeaturePolygon(f, ring, i));
|
|
591
|
+
try {
|
|
592
|
+
const { free } = await (0, check_disk_space_1.default)(path);
|
|
593
|
+
ChartDownloader.lastDiskSpaceResult =
|
|
594
|
+
free >= ChartDownloader.MINIMUM_FREE_DISK_SPACE;
|
|
424
595
|
}
|
|
425
|
-
|
|
426
|
-
|
|
596
|
+
catch (err) {
|
|
597
|
+
ChartDownloader.lastDiskSpaceResult = false;
|
|
427
598
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
features
|
|
431
|
-
};
|
|
599
|
+
ChartDownloader.lastDiskSpaceCheck = now;
|
|
600
|
+
return ChartDownloader.lastDiskSpaceResult;
|
|
432
601
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
602
|
+
static getStatistics() {
|
|
603
|
+
return Object.fromEntries(ChartDownloader.CacheStatistics);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Reset the static fields owned by ChartDownloader. See
|
|
607
|
+
* ChartSeedingManager.resetForTests for why this exists.
|
|
608
|
+
*/
|
|
609
|
+
static resetForTests() {
|
|
610
|
+
ChartDownloader.nextJobId = 1;
|
|
611
|
+
ChartDownloader.CachingEnabled = true;
|
|
612
|
+
ChartDownloader.CacheStatistics = new Map();
|
|
613
|
+
ChartDownloader.TilesCached = 0;
|
|
614
|
+
ChartDownloader.lastDiskSpaceCheck = 0;
|
|
615
|
+
ChartDownloader.lastDiskSpaceResult = true;
|
|
616
|
+
// Skip the production 1.5s backoff for mocked-503 tests; production
|
|
617
|
+
// value is restored per the constant on plugin reload.
|
|
618
|
+
ChartDownloader.tileRetryBackoffMs = 0;
|
|
444
619
|
}
|
|
445
620
|
}
|
|
446
621
|
exports.ChartDownloader = ChartDownloader;
|