@osm-editor-kit/osm-coverage 0.1.0-alpha.0 → 0.1.0-alpha.2
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/README.md +12 -5
- package/dist/create-osm-coverage-api.d.ts +20 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.js +107 -12
- package/dist/osm-coverage-storage.d.ts +20 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
# `@osm-editor-kit/osm-coverage`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> This package is an **alpha** release and still under active development. APIs may change; install with the npm `alpha` dist-tag.
|
|
4
5
|
|
|
5
6
|
## What it does
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
- Download OSM data for the current map viewport (and only the parts you have not covered yet)
|
|
9
|
+
- Keep a session graph of ways/nodes/relations as the map moves and pans
|
|
10
|
+
- Track covered areas as polygons so refetches skip what you already have
|
|
11
|
+
- Record fetch history as GeoJSON for debugging and UI
|
|
12
|
+
- Build Overpass query URLs for common interpreters
|
|
13
|
+
- Optional durable session storage (`storage` adapter) for graph + coverage across reloads
|
|
14
|
+
- Optional editor-dev fixture loader via `@osm-editor-kit/osm-coverage/dev-osm-map-fixture` (not for production apps; not on the main export)
|
|
10
15
|
|
|
11
16
|
## Usage
|
|
12
17
|
|
|
@@ -19,11 +24,13 @@ const api = createOsmCoverageApi<{ source: OsmDataSource }>({
|
|
|
19
24
|
minZoom: 14,
|
|
20
25
|
getDownloadUrl: (bounds, { source }) =>
|
|
21
26
|
/* build Overpass or Map API URL */ '',
|
|
27
|
+
// optional: storage: { load, save, clear }
|
|
22
28
|
})
|
|
23
29
|
|
|
24
30
|
const queryClient = new QueryClient()
|
|
25
31
|
const params = { source: OsmDataSource.OverpassVk }
|
|
26
32
|
|
|
33
|
+
await api.restoreSession(queryClient, params)
|
|
27
34
|
await api.ensureCoverage(queryClient, {
|
|
28
35
|
bounds: { south: 52.47, west: 13.44, north: 52.48, east: 13.45 },
|
|
29
36
|
zoom: 18,
|
|
@@ -32,5 +39,5 @@ await api.ensureCoverage(queryClient, {
|
|
|
32
39
|
})
|
|
33
40
|
|
|
34
41
|
const data = queryClient.getQueryData(api.sessionKey(params))
|
|
35
|
-
// { graph, coverage, fetchHistory }
|
|
42
|
+
// { graph, coverage, fetchHistory, savedAt }
|
|
36
43
|
```
|
|
@@ -2,11 +2,16 @@ import { type MapBounds, type ParsedOsmData } from '@osm-editor-kit/osm-data';
|
|
|
2
2
|
import { type QueryClient } from '@tanstack/react-query';
|
|
3
3
|
import type { Feature, FeatureCollection, MultiPolygon, Polygon } from 'geojson';
|
|
4
4
|
import { type CoverageFetchProps, type MapSizePx } from './coverage-geometry';
|
|
5
|
+
import type { OsmCoverageStorage } from './osm-coverage-storage';
|
|
5
6
|
export type { CoverageFetchProps, MapSizePx } from './coverage-geometry';
|
|
7
|
+
export type { OsmCoveragePersisted, OsmCoverageStorage } from './osm-coverage-storage';
|
|
8
|
+
export { formatCoverageAgeHour, getCoverageSavedAt } from './osm-coverage-storage';
|
|
6
9
|
export type OsmCoverageQueryData = {
|
|
7
10
|
graph: ParsedOsmData;
|
|
8
11
|
coverage: Feature<Polygon | MultiPolygon> | null;
|
|
9
12
|
fetchHistory: FeatureCollection<Polygon, CoverageFetchProps>;
|
|
13
|
+
/** ISO timestamp of last durable save / successful network merge; null if never saved. */
|
|
14
|
+
savedAt: string | null;
|
|
10
15
|
};
|
|
11
16
|
export type CreateOsmCoverageApiOptions<TSessionParams> = {
|
|
12
17
|
getSessionKey: (params: TSessionParams) => readonly unknown[];
|
|
@@ -15,22 +20,34 @@ export type CreateOsmCoverageApiOptions<TSessionParams> = {
|
|
|
15
20
|
download?: (url: string) => Promise<ParsedOsmData>;
|
|
16
21
|
/** When false, session queries stay enabled=false and ensureCoverage skips network. */
|
|
17
22
|
isNetworkEnabled?: () => boolean;
|
|
23
|
+
/** Optional durable store for graph + coverage across reloads. */
|
|
24
|
+
storage?: OsmCoverageStorage;
|
|
25
|
+
/** Debounce durable writes after merges (ms). Default 500. */
|
|
26
|
+
storageSaveDebounceMs?: number;
|
|
18
27
|
};
|
|
19
|
-
export declare function createOsmCoverageApi<TSessionParams>({ getSessionKey, minZoom, getDownloadUrl, download, isNetworkEnabled, }: CreateOsmCoverageApiOptions<TSessionParams>): {
|
|
28
|
+
export declare function createOsmCoverageApi<TSessionParams>({ getSessionKey, minZoom, getDownloadUrl, download, isNetworkEnabled, storage, storageSaveDebounceMs, }: CreateOsmCoverageApiOptions<TSessionParams>): {
|
|
20
29
|
emptyData: () => OsmCoverageQueryData;
|
|
21
30
|
sessionKey: (params: TSessionParams) => readonly unknown[];
|
|
22
31
|
coverageKey: (params: TSessionParams) => readonly [...unknown[], "coverage"];
|
|
23
|
-
ensureCoverage: (queryClient: QueryClient, { bounds, zoom, mapSizePx, force, ...sessionParams }: {
|
|
32
|
+
ensureCoverage: (queryClient: QueryClient, { bounds, zoom, mapSizePx, force, skipRestore, clearPersistedOnForce, ...sessionParams }: {
|
|
24
33
|
bounds: MapBounds;
|
|
25
34
|
zoom: number;
|
|
26
35
|
mapSizePx: MapSizePx;
|
|
27
36
|
force?: boolean;
|
|
37
|
+
/** Skip durable hydrate for this call (e.g. prefer-fresh). */
|
|
38
|
+
skipRestore?: boolean;
|
|
39
|
+
/** When force=true, also wipe durable storage before refetch. */
|
|
40
|
+
clearPersistedOnForce?: boolean;
|
|
28
41
|
} & TSessionParams) => Promise<{
|
|
29
42
|
skipped: boolean;
|
|
30
43
|
}>;
|
|
44
|
+
restoreSession: (queryClient: QueryClient, params: TSessionParams, options?: {
|
|
45
|
+
forceReload?: boolean;
|
|
46
|
+
}) => Promise<boolean>;
|
|
47
|
+
clearPersisted: (params: TSessionParams) => Promise<void>;
|
|
31
48
|
createUseQuery: (useSessionParams: () => TSessionParams) => <TData = OsmCoverageQueryData>(options?: {
|
|
32
49
|
select?: (data: OsmCoverageQueryData) => TData;
|
|
33
|
-
}) => import("@tanstack/react-query").
|
|
50
|
+
}) => import("@tanstack/react-query").UseQueryResult<NoInfer<TData>, Error>;
|
|
34
51
|
createUseIsFetching: (useSessionParams: () => TSessionParams) => () => boolean;
|
|
35
52
|
createUseResetOnSessionChange: (useSessionParams: () => TSessionParams) => () => void;
|
|
36
53
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { createOsmCoverageApi, type OsmCoverageQueryData } from './create-osm-coverage-api';
|
|
1
|
+
export { createOsmCoverageApi, type OsmCoverageQueryData, type OsmCoveragePersisted, type OsmCoverageStorage, formatCoverageAgeHour, getCoverageSavedAt, } from './create-osm-coverage-api';
|
|
3
2
|
export { type CoverageFetchProps, type CoverageFetchKind, type MapSizePx, boundsToPolygon, bufferPxAtZoom, computeMissingFetchRequests, unionIntoCoverage, } from './coverage-geometry';
|
|
4
3
|
export { OsmDataSource } from './osm-data-source';
|
|
5
4
|
export { boundsToOverpassBbox, buildOverpassInterpreterUrl } from './overpass-url';
|
package/dist/index.js
CHANGED
|
@@ -10,13 +10,7 @@ import { featureCollection } from '@turf/helpers';
|
|
|
10
10
|
import intersect from '@turf/intersect';
|
|
11
11
|
import union from '@turf/union';
|
|
12
12
|
|
|
13
|
-
// src/
|
|
14
|
-
var DEV_OSM_MAP_FIXTURE_ROUTE = "dev-osm-map-fixture";
|
|
15
|
-
function devOsmMapFixtureMapUrl(baseUrl, bounds) {
|
|
16
|
-
const base = baseUrl.replace(/\/$/, "");
|
|
17
|
-
const bbox2 = [bounds.west, bounds.south, bounds.east, bounds.north].join(",");
|
|
18
|
-
return `${base}/${DEV_OSM_MAP_FIXTURE_ROUTE}/api/0.6/map?bbox=${bbox2}`;
|
|
19
|
-
}
|
|
13
|
+
// src/create-osm-coverage-api.ts
|
|
20
14
|
var BUFFER_PX_AT_Z13 = 200;
|
|
21
15
|
var MIN_MISSING_AREA_RATIO = 5e-3;
|
|
22
16
|
var FULL_FETCH_RATIO = 0.85;
|
|
@@ -209,29 +203,112 @@ function appendFetchHistory(history, groupId, fetchedAt, requests) {
|
|
|
209
203
|
});
|
|
210
204
|
}
|
|
211
205
|
|
|
206
|
+
// src/osm-coverage-storage.ts
|
|
207
|
+
function getCoverageSavedAt(savedAt) {
|
|
208
|
+
if (!savedAt) return null;
|
|
209
|
+
const parsed = Date.parse(savedAt);
|
|
210
|
+
return Number.isNaN(parsed) ? null : savedAt;
|
|
211
|
+
}
|
|
212
|
+
function formatCoverageAgeHour(savedAt, now = /* @__PURE__ */ new Date()) {
|
|
213
|
+
const iso = getCoverageSavedAt(savedAt);
|
|
214
|
+
if (!iso) return null;
|
|
215
|
+
const saved = new Date(iso);
|
|
216
|
+
saved.setMinutes(0, 0, 0);
|
|
217
|
+
const hour = String(saved.getHours()).padStart(2, "0");
|
|
218
|
+
const sameDay = saved.getFullYear() === now.getFullYear() && saved.getMonth() === now.getMonth() && saved.getDate() === now.getDate();
|
|
219
|
+
if (sameDay) {
|
|
220
|
+
return `${hour}:00`;
|
|
221
|
+
}
|
|
222
|
+
const y = saved.getFullYear();
|
|
223
|
+
const m = String(saved.getMonth() + 1).padStart(2, "0");
|
|
224
|
+
const d = String(saved.getDate()).padStart(2, "0");
|
|
225
|
+
return `${y}-${m}-${d} ${hour}:00`;
|
|
226
|
+
}
|
|
227
|
+
|
|
212
228
|
// src/create-osm-coverage-api.ts
|
|
229
|
+
var DEFAULT_STORAGE_SAVE_DEBOUNCE_MS = 500;
|
|
213
230
|
function createOsmCoverageApi({
|
|
214
231
|
getSessionKey,
|
|
215
232
|
minZoom,
|
|
216
233
|
getDownloadUrl,
|
|
217
234
|
download = downloadOsmData,
|
|
218
|
-
isNetworkEnabled
|
|
235
|
+
isNetworkEnabled,
|
|
236
|
+
storage,
|
|
237
|
+
storageSaveDebounceMs = DEFAULT_STORAGE_SAVE_DEBOUNCE_MS
|
|
219
238
|
}) {
|
|
220
239
|
const getCoverageKey = (params) => [...getSessionKey(params), "coverage"];
|
|
221
240
|
const networkEnabled = () => isNetworkEnabled?.() ?? true;
|
|
222
241
|
const coverageChains = /* @__PURE__ */ new Map();
|
|
242
|
+
const hydratedSessions = /* @__PURE__ */ new Set();
|
|
243
|
+
const saveTimers = /* @__PURE__ */ new Map();
|
|
223
244
|
function emptyData() {
|
|
224
245
|
return {
|
|
225
246
|
graph: emptyParsedOsmData(),
|
|
226
247
|
coverage: null,
|
|
227
|
-
fetchHistory: emptyFetchHistory()
|
|
248
|
+
fetchHistory: emptyFetchHistory(),
|
|
249
|
+
savedAt: null
|
|
228
250
|
};
|
|
229
251
|
}
|
|
252
|
+
function scheduleSave(sessionKey, data) {
|
|
253
|
+
if (!storage || !data.coverage) return;
|
|
254
|
+
const chainKey = JSON.stringify(sessionKey);
|
|
255
|
+
const previous = saveTimers.get(chainKey);
|
|
256
|
+
if (previous) clearTimeout(previous);
|
|
257
|
+
const persisted = {
|
|
258
|
+
graph: data.graph,
|
|
259
|
+
coverage: data.coverage,
|
|
260
|
+
savedAt: data.savedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
261
|
+
};
|
|
262
|
+
saveTimers.set(
|
|
263
|
+
chainKey,
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
saveTimers.delete(chainKey);
|
|
266
|
+
void storage.save(sessionKey, persisted).catch((error) => {
|
|
267
|
+
console.error("Failed to persist OSM coverage session", error);
|
|
268
|
+
});
|
|
269
|
+
}, storageSaveDebounceMs)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
async function restoreSession(queryClient, params, options) {
|
|
273
|
+
if (!storage) return false;
|
|
274
|
+
const sessionKey = getSessionKey(params);
|
|
275
|
+
const chainKey = JSON.stringify(sessionKey);
|
|
276
|
+
if (options?.forceReload) {
|
|
277
|
+
hydratedSessions.delete(chainKey);
|
|
278
|
+
}
|
|
279
|
+
if (hydratedSessions.has(chainKey)) return false;
|
|
280
|
+
hydratedSessions.add(chainKey);
|
|
281
|
+
const loaded = await storage.load(sessionKey);
|
|
282
|
+
if (!loaded?.coverage) return false;
|
|
283
|
+
const current = queryClient.getQueryData(sessionKey);
|
|
284
|
+
if (current?.coverage) return false;
|
|
285
|
+
queryClient.setQueryData(sessionKey, {
|
|
286
|
+
graph: loaded.graph,
|
|
287
|
+
coverage: loaded.coverage,
|
|
288
|
+
fetchHistory: emptyFetchHistory(),
|
|
289
|
+
savedAt: loaded.savedAt
|
|
290
|
+
});
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
async function clearPersisted(params) {
|
|
294
|
+
if (!storage) return;
|
|
295
|
+
const sessionKey = getSessionKey(params);
|
|
296
|
+
const chainKey = JSON.stringify(sessionKey);
|
|
297
|
+
const pending = saveTimers.get(chainKey);
|
|
298
|
+
if (pending) {
|
|
299
|
+
clearTimeout(pending);
|
|
300
|
+
saveTimers.delete(chainKey);
|
|
301
|
+
}
|
|
302
|
+
hydratedSessions.delete(chainKey);
|
|
303
|
+
await storage.clear(sessionKey);
|
|
304
|
+
}
|
|
230
305
|
async function ensureCoverage(queryClient, {
|
|
231
306
|
bounds,
|
|
232
307
|
zoom,
|
|
233
308
|
mapSizePx,
|
|
234
309
|
force = false,
|
|
310
|
+
skipRestore = false,
|
|
311
|
+
clearPersistedOnForce = false,
|
|
235
312
|
...sessionParams
|
|
236
313
|
}) {
|
|
237
314
|
if (!networkEnabled()) {
|
|
@@ -242,6 +319,12 @@ function createOsmCoverageApi({
|
|
|
242
319
|
const coverageKey = getCoverageKey(params);
|
|
243
320
|
const chainKey = JSON.stringify(sessionKey);
|
|
244
321
|
const run = async () => {
|
|
322
|
+
if (!skipRestore && !force) {
|
|
323
|
+
await restoreSession(queryClient, params);
|
|
324
|
+
}
|
|
325
|
+
if (force && clearPersistedOnForce) {
|
|
326
|
+
await clearPersisted(params);
|
|
327
|
+
}
|
|
245
328
|
const requestKey = [...coverageKey, crypto.randomUUID()];
|
|
246
329
|
return queryClient.fetchQuery({
|
|
247
330
|
queryKey: requestKey,
|
|
@@ -274,7 +357,14 @@ function createOsmCoverageApi({
|
|
|
274
357
|
for (const newGraph of fetchedGraphs) {
|
|
275
358
|
graph = mergeParsedOsm(graph, newGraph);
|
|
276
359
|
}
|
|
277
|
-
|
|
360
|
+
const next2 = {
|
|
361
|
+
graph,
|
|
362
|
+
coverage,
|
|
363
|
+
fetchHistory,
|
|
364
|
+
savedAt: fetchedAt
|
|
365
|
+
};
|
|
366
|
+
scheduleSave(sessionKey, next2);
|
|
367
|
+
return next2;
|
|
278
368
|
});
|
|
279
369
|
return { skipped: false };
|
|
280
370
|
},
|
|
@@ -299,8 +389,11 @@ function createOsmCoverageApi({
|
|
|
299
389
|
return useQuery({
|
|
300
390
|
queryKey: getSessionKey(params),
|
|
301
391
|
queryFn: () => emptyData(),
|
|
302
|
-
|
|
392
|
+
// Placeholder (not initialData) so durable restore via setQueryData is not fought
|
|
393
|
+
// by a permanently seeded empty session.
|
|
394
|
+
placeholderData: emptyData(),
|
|
303
395
|
staleTime: Number.POSITIVE_INFINITY,
|
|
396
|
+
gcTime: Number.POSITIVE_INFINITY,
|
|
304
397
|
enabled: networkEnabled(),
|
|
305
398
|
select: options?.select
|
|
306
399
|
});
|
|
@@ -332,6 +425,8 @@ function createOsmCoverageApi({
|
|
|
332
425
|
sessionKey: getSessionKey,
|
|
333
426
|
coverageKey: getCoverageKey,
|
|
334
427
|
ensureCoverage,
|
|
428
|
+
restoreSession,
|
|
429
|
+
clearPersisted,
|
|
335
430
|
createUseQuery,
|
|
336
431
|
createUseIsFetching,
|
|
337
432
|
createUseResetOnSessionChange
|
|
@@ -358,4 +453,4 @@ function buildOverpassInterpreterUrl(server, query) {
|
|
|
358
453
|
var overpassDeUrl = "https://overpass-api.de/api/interpreter?data=";
|
|
359
454
|
var overpassVkUrl = "https://maps.mail.ru/osm/tools/overpass/api/interpreter?data=";
|
|
360
455
|
|
|
361
|
-
export {
|
|
456
|
+
export { OsmDataSource, boundsToOverpassBbox, boundsToPolygon, bufferPxAtZoom, buildOverpassInterpreterUrl, computeMissingFetchRequests, createOsmCoverageApi, formatCoverageAgeHour, getCoverageSavedAt, overpassDeUrl, overpassVkUrl, unionIntoCoverage };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ParsedOsmData } from '@osm-editor-kit/osm-data';
|
|
2
|
+
import type { Feature, MultiPolygon, Polygon } from 'geojson';
|
|
3
|
+
/** Durable session blob — fetchHistory stays memory-only. */
|
|
4
|
+
export type OsmCoveragePersisted = {
|
|
5
|
+
graph: ParsedOsmData;
|
|
6
|
+
coverage: Feature<Polygon | MultiPolygon> | null;
|
|
7
|
+
savedAt: string;
|
|
8
|
+
};
|
|
9
|
+
export type OsmCoverageStorage = {
|
|
10
|
+
load(sessionKey: readonly unknown[]): Promise<OsmCoveragePersisted | null>;
|
|
11
|
+
save(sessionKey: readonly unknown[], data: OsmCoveragePersisted): Promise<void>;
|
|
12
|
+
clear(sessionKey: readonly unknown[]): Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
/** ISO `savedAt` from session query data, or null when never persisted/fetched. */
|
|
15
|
+
export declare function getCoverageSavedAt(savedAt: string | null | undefined): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Round `savedAt` down to the hour for UI (“OSM data from 14:00”).
|
|
18
|
+
* Returns null when missing/invalid.
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatCoverageAgeHour(savedAt: string | null | undefined, now?: Date): string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osm-editor-kit/osm-coverage",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"description": "Incremental OSM coverage fetching for map viewports with TanStack Query session state.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tobias Jordans",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"url": "git+https://github.com/osmberlin/street-space-editor.git",
|
|
10
10
|
"directory": "packages/osm-coverage"
|
|
11
11
|
},
|
|
12
|
+
"homepage": "https://github.com/osmberlin/street-space-editor/tree/main/packages/osm-coverage",
|
|
12
13
|
"type": "module",
|
|
13
14
|
"sideEffects": false,
|
|
14
15
|
"files": [
|