@osm-editor-kit/osm-coverage 0.1.0-alpha.0 → 0.1.0-alpha.3
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 +13 -7
- package/dist/coverage-geometry.d.ts +5 -0
- package/dist/create-osm-coverage-api.d.ts +20 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.js +130 -29
- package/dist/osm-coverage-storage.d.ts +20 -0
- package/package.json +5 -4
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
|
|
|
@@ -17,13 +22,14 @@ import { createOsmCoverageApi, OsmDataSource } from '@osm-editor-kit/osm-coverag
|
|
|
17
22
|
const api = createOsmCoverageApi<{ source: OsmDataSource }>({
|
|
18
23
|
getSessionKey: ({ source }) => ['osm', source] as const,
|
|
19
24
|
minZoom: 14,
|
|
20
|
-
getDownloadUrl: (bounds, { source }) =>
|
|
21
|
-
|
|
25
|
+
getDownloadUrl: (bounds, { source }) => /* build Overpass or Map API URL */ '',
|
|
26
|
+
// optional: storage: { load, save, clear }
|
|
22
27
|
})
|
|
23
28
|
|
|
24
29
|
const queryClient = new QueryClient()
|
|
25
30
|
const params = { source: OsmDataSource.OverpassVk }
|
|
26
31
|
|
|
32
|
+
await api.restoreSession(queryClient, params)
|
|
27
33
|
await api.ensureCoverage(queryClient, {
|
|
28
34
|
bounds: { south: 52.47, west: 13.44, north: 52.48, east: 13.45 },
|
|
29
35
|
zoom: 18,
|
|
@@ -32,5 +38,5 @@ await api.ensureCoverage(queryClient, {
|
|
|
32
38
|
})
|
|
33
39
|
|
|
34
40
|
const data = queryClient.getQueryData(api.sessionKey(params))
|
|
35
|
-
// { graph, coverage, fetchHistory }
|
|
41
|
+
// { graph, coverage, fetchHistory, savedAt }
|
|
36
42
|
```
|
|
@@ -31,6 +31,11 @@ export declare function isValidBounds(bounds: MapBounds): boolean;
|
|
|
31
31
|
export declare function boundsArea(bounds: MapBounds): number;
|
|
32
32
|
export declare function bufferBoundsInViewport(request: MapBounds, viewport: MapBounds, zoom: number, mapSizePx: MapSizePx): MapBounds;
|
|
33
33
|
export declare function clampBoundsToViewport(request: MapBounds, viewport: MapBounds): MapBounds;
|
|
34
|
+
/** Lon/lat length of the zoom-aware seam overlap (same magnitude as fetch buffer). */
|
|
35
|
+
export declare function overlapDeltaAtZoom(viewport: MapBounds, zoom: number, mapSizePx: MapSizePx): {
|
|
36
|
+
lon: number;
|
|
37
|
+
lat: number;
|
|
38
|
+
};
|
|
34
39
|
export declare function createViewportFetchRequest(viewport: MapBounds, zoom: number, mapSizePx: MapSizePx, kind: CoverageFetchKind): CoverageFetchRequest | null;
|
|
35
40
|
export declare function computeMissingFetchRequests(viewport: MapBounds, coverage: Feature<Polygon | MultiPolygon> | null, zoom: number, mapSizePx: MapSizePx): CoverageFetchRequest[];
|
|
36
41
|
export declare function unionIntoCoverage(coverage: Feature<Polygon | MultiPolygon> | null, requestBounds: MapBounds): Feature<Polygon | MultiPolygon>;
|
|
@@ -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;
|
|
@@ -54,19 +48,15 @@ function bufferBoundsInViewport(request, viewport, zoom, mapSizePx) {
|
|
|
54
48
|
north: request.north + latBuffer
|
|
55
49
|
};
|
|
56
50
|
}
|
|
57
|
-
function
|
|
51
|
+
function overlapDeltaAtZoom(viewport, zoom, mapSizePx) {
|
|
52
|
+
const px = bufferPxAtZoom(zoom);
|
|
58
53
|
return {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
east: Math.min(request.east, viewport.east),
|
|
62
|
-
north: Math.min(request.north, viewport.north)
|
|
54
|
+
lon: px * (viewport.east - viewport.west) / mapSizePx.width,
|
|
55
|
+
lat: px * (viewport.north - viewport.south) / mapSizePx.height
|
|
63
56
|
};
|
|
64
57
|
}
|
|
65
58
|
function prepareFetchBounds(request, viewport, zoom, mapSizePx) {
|
|
66
|
-
const buffered =
|
|
67
|
-
bufferBoundsInViewport(request, viewport, zoom, mapSizePx),
|
|
68
|
-
viewport
|
|
69
|
-
);
|
|
59
|
+
const buffered = bufferBoundsInViewport(request, viewport, zoom, mapSizePx);
|
|
70
60
|
if (!isValidBounds(buffered)) return null;
|
|
71
61
|
const viewportArea = boundsArea(viewport);
|
|
72
62
|
if (boundsArea(buffered) < MIN_MISSING_AREA_RATIO * viewportArea) return null;
|
|
@@ -80,27 +70,28 @@ function missingParts(viewport, coverage) {
|
|
|
80
70
|
(feature) => feature.geometry.type === "Polygon"
|
|
81
71
|
);
|
|
82
72
|
}
|
|
83
|
-
function stripCandidates(viewport, coverage, missingPart) {
|
|
73
|
+
function stripCandidates(viewport, coverage, missingPart, zoom, mapSizePx) {
|
|
84
74
|
const viewportPoly = boundsToPolygon(viewport);
|
|
85
75
|
const covered = intersect(featureCollection([viewportPoly, coverage]));
|
|
86
76
|
if (!covered) return [];
|
|
87
77
|
const coveredBbox = polygonToBounds(covered);
|
|
78
|
+
const { lon, lat } = overlapDeltaAtZoom(viewport, zoom, mapSizePx);
|
|
88
79
|
const candidates = [
|
|
89
80
|
{
|
|
90
81
|
west: viewport.west,
|
|
91
82
|
south: viewport.south,
|
|
92
|
-
east: coveredBbox.west,
|
|
83
|
+
east: Math.min(viewport.east, coveredBbox.west + lon),
|
|
93
84
|
north: viewport.north
|
|
94
85
|
},
|
|
95
86
|
{
|
|
96
|
-
west: coveredBbox.east,
|
|
87
|
+
west: Math.max(viewport.west, coveredBbox.east - lon),
|
|
97
88
|
south: viewport.south,
|
|
98
89
|
east: viewport.east,
|
|
99
90
|
north: viewport.north
|
|
100
91
|
},
|
|
101
92
|
{
|
|
102
93
|
west: viewport.west,
|
|
103
|
-
south: coveredBbox.north,
|
|
94
|
+
south: Math.max(viewport.south, coveredBbox.north - lat),
|
|
104
95
|
east: viewport.east,
|
|
105
96
|
north: viewport.north
|
|
106
97
|
},
|
|
@@ -108,7 +99,7 @@ function stripCandidates(viewport, coverage, missingPart) {
|
|
|
108
99
|
west: viewport.west,
|
|
109
100
|
south: viewport.south,
|
|
110
101
|
east: viewport.east,
|
|
111
|
-
north: coveredBbox.south
|
|
102
|
+
north: Math.min(viewport.north, coveredBbox.south + lat)
|
|
112
103
|
}
|
|
113
104
|
];
|
|
114
105
|
return candidates.filter((strip) => {
|
|
@@ -117,14 +108,14 @@ function stripCandidates(viewport, coverage, missingPart) {
|
|
|
117
108
|
return overlap !== null && area(overlap) > 0;
|
|
118
109
|
});
|
|
119
110
|
}
|
|
120
|
-
function chooseBoundsForMissingPart(viewport, coverage, missingPart) {
|
|
111
|
+
function chooseBoundsForMissingPart(viewport, coverage, missingPart, zoom, mapSizePx) {
|
|
121
112
|
const partArea = area(missingPart);
|
|
122
113
|
const partBbox = polygonToBounds(missingPart);
|
|
123
114
|
const fillRatio = partArea / area(boundsToPolygon(partBbox));
|
|
124
115
|
if (fillRatio >= HIGH_FILL_RATIO) {
|
|
125
116
|
return [partBbox];
|
|
126
117
|
}
|
|
127
|
-
const strips = stripCandidates(viewport, coverage, missingPart);
|
|
118
|
+
const strips = stripCandidates(viewport, coverage, missingPart, zoom, mapSizePx);
|
|
128
119
|
const stripAreas = strips.map((strip) => ({ bounds: strip, area: boundsArea(strip) })).sort((a, b) => b.area - a.area);
|
|
129
120
|
const stripTotalArea = stripAreas.reduce((sum, strip) => sum + strip.area, 0);
|
|
130
121
|
const bboxArea = boundsArea(partBbox);
|
|
@@ -149,7 +140,7 @@ function computeMissingFetchRequests(viewport, coverage, zoom, mapSizePx) {
|
|
|
149
140
|
);
|
|
150
141
|
if (significantParts.length === 0) return [];
|
|
151
142
|
const rawBounds = significantParts.flatMap(
|
|
152
|
-
(part) => chooseBoundsForMissingPart(viewport, coverage, part)
|
|
143
|
+
(part) => chooseBoundsForMissingPart(viewport, coverage, part, zoom, mapSizePx)
|
|
153
144
|
);
|
|
154
145
|
let requests = rawBounds.map((request) => {
|
|
155
146
|
const prepared = prepareFetchBounds(request, viewport, zoom, mapSizePx);
|
|
@@ -166,6 +157,15 @@ function computeMissingFetchRequests(viewport, coverage, zoom, mapSizePx) {
|
|
|
166
157
|
return full ? [full] : [];
|
|
167
158
|
}
|
|
168
159
|
requests = requests.slice(0, MAX_STRIP_REQUESTS);
|
|
160
|
+
let projected = coverage;
|
|
161
|
+
for (const request of requests) {
|
|
162
|
+
projected = unionIntoCoverage(projected, request.bounds);
|
|
163
|
+
}
|
|
164
|
+
const leftover = missingParts(viewport, projected).reduce((sum, part) => sum + area(part), 0);
|
|
165
|
+
if (leftover >= MIN_MISSING_AREA_RATIO * viewportArea) {
|
|
166
|
+
const full = createViewportFetchRequest(viewport, zoom, mapSizePx, "full");
|
|
167
|
+
return full ? [full] : [];
|
|
168
|
+
}
|
|
169
169
|
return requests.map((request) => ({
|
|
170
170
|
bounds: request.bounds,
|
|
171
171
|
kind: request.kind
|
|
@@ -209,29 +209,112 @@ function appendFetchHistory(history, groupId, fetchedAt, requests) {
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// src/osm-coverage-storage.ts
|
|
213
|
+
function getCoverageSavedAt(savedAt) {
|
|
214
|
+
if (!savedAt) return null;
|
|
215
|
+
const parsed = Date.parse(savedAt);
|
|
216
|
+
return Number.isNaN(parsed) ? null : savedAt;
|
|
217
|
+
}
|
|
218
|
+
function formatCoverageAgeHour(savedAt, now = /* @__PURE__ */ new Date()) {
|
|
219
|
+
const iso = getCoverageSavedAt(savedAt);
|
|
220
|
+
if (!iso) return null;
|
|
221
|
+
const saved = new Date(iso);
|
|
222
|
+
saved.setMinutes(0, 0, 0);
|
|
223
|
+
const hour = String(saved.getHours()).padStart(2, "0");
|
|
224
|
+
const sameDay = saved.getFullYear() === now.getFullYear() && saved.getMonth() === now.getMonth() && saved.getDate() === now.getDate();
|
|
225
|
+
if (sameDay) {
|
|
226
|
+
return `${hour}:00`;
|
|
227
|
+
}
|
|
228
|
+
const y = saved.getFullYear();
|
|
229
|
+
const m = String(saved.getMonth() + 1).padStart(2, "0");
|
|
230
|
+
const d = String(saved.getDate()).padStart(2, "0");
|
|
231
|
+
return `${y}-${m}-${d} ${hour}:00`;
|
|
232
|
+
}
|
|
233
|
+
|
|
212
234
|
// src/create-osm-coverage-api.ts
|
|
235
|
+
var DEFAULT_STORAGE_SAVE_DEBOUNCE_MS = 500;
|
|
213
236
|
function createOsmCoverageApi({
|
|
214
237
|
getSessionKey,
|
|
215
238
|
minZoom,
|
|
216
239
|
getDownloadUrl,
|
|
217
240
|
download = downloadOsmData,
|
|
218
|
-
isNetworkEnabled
|
|
241
|
+
isNetworkEnabled,
|
|
242
|
+
storage,
|
|
243
|
+
storageSaveDebounceMs = DEFAULT_STORAGE_SAVE_DEBOUNCE_MS
|
|
219
244
|
}) {
|
|
220
245
|
const getCoverageKey = (params) => [...getSessionKey(params), "coverage"];
|
|
221
246
|
const networkEnabled = () => isNetworkEnabled?.() ?? true;
|
|
222
247
|
const coverageChains = /* @__PURE__ */ new Map();
|
|
248
|
+
const hydratedSessions = /* @__PURE__ */ new Set();
|
|
249
|
+
const saveTimers = /* @__PURE__ */ new Map();
|
|
223
250
|
function emptyData() {
|
|
224
251
|
return {
|
|
225
252
|
graph: emptyParsedOsmData(),
|
|
226
253
|
coverage: null,
|
|
227
|
-
fetchHistory: emptyFetchHistory()
|
|
254
|
+
fetchHistory: emptyFetchHistory(),
|
|
255
|
+
savedAt: null
|
|
228
256
|
};
|
|
229
257
|
}
|
|
258
|
+
function scheduleSave(sessionKey, data) {
|
|
259
|
+
if (!storage || !data.coverage) return;
|
|
260
|
+
const chainKey = JSON.stringify(sessionKey);
|
|
261
|
+
const previous = saveTimers.get(chainKey);
|
|
262
|
+
if (previous) clearTimeout(previous);
|
|
263
|
+
const persisted = {
|
|
264
|
+
graph: data.graph,
|
|
265
|
+
coverage: data.coverage,
|
|
266
|
+
savedAt: data.savedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
267
|
+
};
|
|
268
|
+
saveTimers.set(
|
|
269
|
+
chainKey,
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
saveTimers.delete(chainKey);
|
|
272
|
+
void storage.save(sessionKey, persisted).catch((error) => {
|
|
273
|
+
console.error("Failed to persist OSM coverage session", error);
|
|
274
|
+
});
|
|
275
|
+
}, storageSaveDebounceMs)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
async function restoreSession(queryClient, params, options) {
|
|
279
|
+
if (!storage) return false;
|
|
280
|
+
const sessionKey = getSessionKey(params);
|
|
281
|
+
const chainKey = JSON.stringify(sessionKey);
|
|
282
|
+
if (options?.forceReload) {
|
|
283
|
+
hydratedSessions.delete(chainKey);
|
|
284
|
+
}
|
|
285
|
+
if (hydratedSessions.has(chainKey)) return false;
|
|
286
|
+
hydratedSessions.add(chainKey);
|
|
287
|
+
const loaded = await storage.load(sessionKey);
|
|
288
|
+
if (!loaded?.coverage) return false;
|
|
289
|
+
const current = queryClient.getQueryData(sessionKey);
|
|
290
|
+
if (current?.coverage) return false;
|
|
291
|
+
queryClient.setQueryData(sessionKey, {
|
|
292
|
+
graph: loaded.graph,
|
|
293
|
+
coverage: loaded.coverage,
|
|
294
|
+
fetchHistory: emptyFetchHistory(),
|
|
295
|
+
savedAt: loaded.savedAt
|
|
296
|
+
});
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
async function clearPersisted(params) {
|
|
300
|
+
if (!storage) return;
|
|
301
|
+
const sessionKey = getSessionKey(params);
|
|
302
|
+
const chainKey = JSON.stringify(sessionKey);
|
|
303
|
+
const pending = saveTimers.get(chainKey);
|
|
304
|
+
if (pending) {
|
|
305
|
+
clearTimeout(pending);
|
|
306
|
+
saveTimers.delete(chainKey);
|
|
307
|
+
}
|
|
308
|
+
hydratedSessions.delete(chainKey);
|
|
309
|
+
await storage.clear(sessionKey);
|
|
310
|
+
}
|
|
230
311
|
async function ensureCoverage(queryClient, {
|
|
231
312
|
bounds,
|
|
232
313
|
zoom,
|
|
233
314
|
mapSizePx,
|
|
234
315
|
force = false,
|
|
316
|
+
skipRestore = false,
|
|
317
|
+
clearPersistedOnForce = false,
|
|
235
318
|
...sessionParams
|
|
236
319
|
}) {
|
|
237
320
|
if (!networkEnabled()) {
|
|
@@ -242,6 +325,12 @@ function createOsmCoverageApi({
|
|
|
242
325
|
const coverageKey = getCoverageKey(params);
|
|
243
326
|
const chainKey = JSON.stringify(sessionKey);
|
|
244
327
|
const run = async () => {
|
|
328
|
+
if (!skipRestore && !force) {
|
|
329
|
+
await restoreSession(queryClient, params);
|
|
330
|
+
}
|
|
331
|
+
if (force && clearPersistedOnForce) {
|
|
332
|
+
await clearPersisted(params);
|
|
333
|
+
}
|
|
245
334
|
const requestKey = [...coverageKey, crypto.randomUUID()];
|
|
246
335
|
return queryClient.fetchQuery({
|
|
247
336
|
queryKey: requestKey,
|
|
@@ -274,7 +363,14 @@ function createOsmCoverageApi({
|
|
|
274
363
|
for (const newGraph of fetchedGraphs) {
|
|
275
364
|
graph = mergeParsedOsm(graph, newGraph);
|
|
276
365
|
}
|
|
277
|
-
|
|
366
|
+
const next2 = {
|
|
367
|
+
graph,
|
|
368
|
+
coverage,
|
|
369
|
+
fetchHistory,
|
|
370
|
+
savedAt: fetchedAt
|
|
371
|
+
};
|
|
372
|
+
scheduleSave(sessionKey, next2);
|
|
373
|
+
return next2;
|
|
278
374
|
});
|
|
279
375
|
return { skipped: false };
|
|
280
376
|
},
|
|
@@ -299,8 +395,11 @@ function createOsmCoverageApi({
|
|
|
299
395
|
return useQuery({
|
|
300
396
|
queryKey: getSessionKey(params),
|
|
301
397
|
queryFn: () => emptyData(),
|
|
302
|
-
|
|
398
|
+
// Placeholder (not initialData) so durable restore via setQueryData is not fought
|
|
399
|
+
// by a permanently seeded empty session.
|
|
400
|
+
placeholderData: emptyData(),
|
|
303
401
|
staleTime: Number.POSITIVE_INFINITY,
|
|
402
|
+
gcTime: Number.POSITIVE_INFINITY,
|
|
304
403
|
enabled: networkEnabled(),
|
|
305
404
|
select: options?.select
|
|
306
405
|
});
|
|
@@ -332,6 +431,8 @@ function createOsmCoverageApi({
|
|
|
332
431
|
sessionKey: getSessionKey,
|
|
333
432
|
coverageKey: getCoverageKey,
|
|
334
433
|
ensureCoverage,
|
|
434
|
+
restoreSession,
|
|
435
|
+
clearPersisted,
|
|
335
436
|
createUseQuery,
|
|
336
437
|
createUseIsFetching,
|
|
337
438
|
createUseResetOnSessionChange
|
|
@@ -358,4 +459,4 @@ function buildOverpassInterpreterUrl(server, query) {
|
|
|
358
459
|
var overpassDeUrl = "https://overpass-api.de/api/interpreter?data=";
|
|
359
460
|
var overpassVkUrl = "https://maps.mail.ru/osm/tools/overpass/api/interpreter?data=";
|
|
360
461
|
|
|
361
|
-
export {
|
|
462
|
+
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,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osm-editor-kit/osm-coverage",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
4
|
"description": "Incremental OSM coverage fetching for map viewports with TanStack Query session state.",
|
|
5
|
+
"homepage": "https://github.com/osmberlin/street-space-editor/tree/main/packages/osm-coverage",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"author": "Tobias Jordans",
|
|
7
8
|
"repository": {
|
|
@@ -9,11 +10,11 @@
|
|
|
9
10
|
"url": "git+https://github.com/osmberlin/street-space-editor.git",
|
|
10
11
|
"directory": "packages/osm-coverage"
|
|
11
12
|
},
|
|
12
|
-
"type": "module",
|
|
13
|
-
"sideEffects": false,
|
|
14
13
|
"files": [
|
|
15
14
|
"dist"
|
|
16
15
|
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
17
18
|
"exports": {
|
|
18
19
|
".": {
|
|
19
20
|
"types": "./dist/index.d.ts",
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"postpack": "bun ../../scripts/package-exports-restore.ts"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@osm-editor-kit/osm-data": "
|
|
39
|
+
"@osm-editor-kit/osm-data": "^0.1.0-alpha.1",
|
|
39
40
|
"@tanstack/react-query": "^5.85.5",
|
|
40
41
|
"@turf/area": "^7.3.5",
|
|
41
42
|
"@turf/bbox": "^7.3.5",
|