pdf-mapview 0.1.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/LICENSE +12 -0
- package/README.md +197 -0
- package/dist/client/index.cjs +1155 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +102 -0
- package/dist/client/index.d.ts +102 -0
- package/dist/client/index.js +1149 -0
- package/dist/client/index.js.map +1 -0
- package/dist/ingest/cli.cjs +703 -0
- package/dist/ingest/cli.cjs.map +1 -0
- package/dist/ingest/cli.d.cts +1 -0
- package/dist/ingest/cli.d.ts +1 -0
- package/dist/ingest/cli.js +697 -0
- package/dist/ingest/cli.js.map +1 -0
- package/dist/ingest/index.cjs +751 -0
- package/dist/ingest/index.cjs.map +1 -0
- package/dist/ingest/index.d.cts +33 -0
- package/dist/ingest/index.d.ts +33 -0
- package/dist/ingest/index.js +738 -0
- package/dist/ingest/index.js.map +1 -0
- package/dist/ingest-sfbf6503.d.cts +647 -0
- package/dist/ingest-sfbf6503.d.ts +647 -0
- package/dist/server/index.cjs +751 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +3 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +738 -0
- package/dist/server/index.js.map +1 -0
- package/dist/shared/index.cjs +238 -0
- package/dist/shared/index.cjs.map +1 -0
- package/dist/shared/index.d.cts +8 -0
- package/dist/shared/index.d.ts +8 -0
- package/dist/shared/index.js +222 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/source-Cb1QZPVw.d.ts +60 -0
- package/dist/source-DHrup45h.d.cts +60 -0
- package/package.json +104 -0
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var zod = require('zod');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
// src/client/components/TileMapViewer.tsx
|
|
8
|
+
var normalizedPointSchema = zod.z.object({
|
|
9
|
+
x: zod.z.number().finite().min(0).max(1),
|
|
10
|
+
y: zod.z.number().finite().min(0).max(1)
|
|
11
|
+
});
|
|
12
|
+
var normalizedRectSchema = zod.z.object({
|
|
13
|
+
x: zod.z.number().finite().min(0).max(1),
|
|
14
|
+
y: zod.z.number().finite().min(0).max(1),
|
|
15
|
+
width: zod.z.number().finite().min(0).max(1),
|
|
16
|
+
height: zod.z.number().finite().min(0).max(1)
|
|
17
|
+
});
|
|
18
|
+
var regionFeatureSchema = zod.z.object({
|
|
19
|
+
id: zod.z.string().min(1),
|
|
20
|
+
geometry: zod.z.discriminatedUnion("type", [
|
|
21
|
+
zod.z.object({
|
|
22
|
+
type: zod.z.literal("polygon"),
|
|
23
|
+
points: zod.z.array(normalizedPointSchema).min(3)
|
|
24
|
+
}),
|
|
25
|
+
zod.z.object({
|
|
26
|
+
type: zod.z.literal("rectangle"),
|
|
27
|
+
rect: normalizedRectSchema
|
|
28
|
+
}),
|
|
29
|
+
zod.z.object({
|
|
30
|
+
type: zod.z.literal("point"),
|
|
31
|
+
point: normalizedPointSchema,
|
|
32
|
+
radius: zod.z.number().finite().positive().optional()
|
|
33
|
+
}),
|
|
34
|
+
zod.z.object({
|
|
35
|
+
type: zod.z.literal("label"),
|
|
36
|
+
point: normalizedPointSchema,
|
|
37
|
+
text: zod.z.string().min(1)
|
|
38
|
+
})
|
|
39
|
+
]),
|
|
40
|
+
label: zod.z.string().optional(),
|
|
41
|
+
metadata: zod.z.record(zod.z.unknown()).optional()
|
|
42
|
+
});
|
|
43
|
+
var regionCollectionSchema = zod.z.object({
|
|
44
|
+
type: zod.z.literal("FeatureCollection"),
|
|
45
|
+
regions: zod.z.array(regionFeatureSchema)
|
|
46
|
+
});
|
|
47
|
+
function normalizeRegions(input) {
|
|
48
|
+
if (!input) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(input)) {
|
|
52
|
+
return input;
|
|
53
|
+
}
|
|
54
|
+
return input.regions;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/client/core/ViewportStore.ts
|
|
58
|
+
var defaultViewState = {
|
|
59
|
+
center: { x: 0.5, y: 0.5 },
|
|
60
|
+
zoom: 1,
|
|
61
|
+
minZoom: 0,
|
|
62
|
+
maxZoom: 6,
|
|
63
|
+
containerWidth: 0,
|
|
64
|
+
containerHeight: 0
|
|
65
|
+
};
|
|
66
|
+
var ViewportStore = class {
|
|
67
|
+
state = defaultViewState;
|
|
68
|
+
listeners = /* @__PURE__ */ new Set();
|
|
69
|
+
getSnapshot() {
|
|
70
|
+
return this.state;
|
|
71
|
+
}
|
|
72
|
+
setState(state) {
|
|
73
|
+
this.state = state;
|
|
74
|
+
for (const listener of this.listeners) {
|
|
75
|
+
listener();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
subscribe(listener) {
|
|
79
|
+
this.listeners.add(listener);
|
|
80
|
+
return () => {
|
|
81
|
+
this.listeners.delete(listener);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/client/core/overlayProjection.ts
|
|
87
|
+
function projectRegion(controller, region) {
|
|
88
|
+
const bounds = getRegionBounds(region);
|
|
89
|
+
switch (region.geometry.type) {
|
|
90
|
+
case "polygon": {
|
|
91
|
+
const points = region.geometry.points.map((point) => controller.normalizedToScreen(point));
|
|
92
|
+
return {
|
|
93
|
+
bounds,
|
|
94
|
+
path: points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x} ${point.y}`).join(" ") + " Z",
|
|
95
|
+
points
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
case "rectangle": {
|
|
99
|
+
const topLeft = controller.normalizedToScreen({
|
|
100
|
+
x: region.geometry.rect.x,
|
|
101
|
+
y: region.geometry.rect.y
|
|
102
|
+
});
|
|
103
|
+
const bottomRight = controller.normalizedToScreen({
|
|
104
|
+
x: region.geometry.rect.x + region.geometry.rect.width,
|
|
105
|
+
y: region.geometry.rect.y + region.geometry.rect.height
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
bounds,
|
|
109
|
+
rect: {
|
|
110
|
+
x: topLeft.x,
|
|
111
|
+
y: topLeft.y,
|
|
112
|
+
width: bottomRight.x - topLeft.x,
|
|
113
|
+
height: bottomRight.y - topLeft.y
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
case "point": {
|
|
118
|
+
const center = controller.normalizedToScreen(region.geometry.point);
|
|
119
|
+
return {
|
|
120
|
+
bounds,
|
|
121
|
+
center
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
case "label": {
|
|
125
|
+
const center = controller.normalizedToScreen(region.geometry.point);
|
|
126
|
+
return {
|
|
127
|
+
bounds,
|
|
128
|
+
center,
|
|
129
|
+
text: region.geometry.text
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function getRegionBounds(region) {
|
|
135
|
+
return getGeometryBounds(region.geometry);
|
|
136
|
+
}
|
|
137
|
+
function getGeometryBounds(geometry) {
|
|
138
|
+
switch (geometry.type) {
|
|
139
|
+
case "rectangle":
|
|
140
|
+
return geometry.rect;
|
|
141
|
+
case "point":
|
|
142
|
+
return {
|
|
143
|
+
x: geometry.point.x,
|
|
144
|
+
y: geometry.point.y,
|
|
145
|
+
width: 0,
|
|
146
|
+
height: 0
|
|
147
|
+
};
|
|
148
|
+
case "label":
|
|
149
|
+
return {
|
|
150
|
+
x: geometry.point.x,
|
|
151
|
+
y: geometry.point.y,
|
|
152
|
+
width: 0,
|
|
153
|
+
height: 0
|
|
154
|
+
};
|
|
155
|
+
case "polygon": {
|
|
156
|
+
const xs = geometry.points.map((point) => point.x);
|
|
157
|
+
const ys = geometry.points.map((point) => point.y);
|
|
158
|
+
const minX = Math.min(...xs);
|
|
159
|
+
const minY = Math.min(...ys);
|
|
160
|
+
const maxX = Math.max(...xs);
|
|
161
|
+
const maxY = Math.max(...ys);
|
|
162
|
+
return {
|
|
163
|
+
x: minX,
|
|
164
|
+
y: minY,
|
|
165
|
+
width: maxX - minX,
|
|
166
|
+
height: maxY - minY
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/client/core/MapController.ts
|
|
173
|
+
var MapController = class {
|
|
174
|
+
store = new ViewportStore();
|
|
175
|
+
engine = null;
|
|
176
|
+
regions = [];
|
|
177
|
+
attachEngine(engine) {
|
|
178
|
+
this.engine = engine;
|
|
179
|
+
}
|
|
180
|
+
detachEngine(engine) {
|
|
181
|
+
if (this.engine === engine) {
|
|
182
|
+
this.engine = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
setRegions(regions) {
|
|
186
|
+
this.regions = regions;
|
|
187
|
+
}
|
|
188
|
+
getView() {
|
|
189
|
+
if (this.engine) {
|
|
190
|
+
return this.engine.getView();
|
|
191
|
+
}
|
|
192
|
+
return this.store.getSnapshot();
|
|
193
|
+
}
|
|
194
|
+
setView(view, options) {
|
|
195
|
+
this.engine?.setView(view, options);
|
|
196
|
+
}
|
|
197
|
+
fitToBounds(bounds, options) {
|
|
198
|
+
this.engine?.fitToBounds(bounds, options);
|
|
199
|
+
}
|
|
200
|
+
zoomToRegion(regionId, options) {
|
|
201
|
+
const region = this.regions.find((candidate) => candidate.id === regionId);
|
|
202
|
+
if (!region) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.fitToBounds(getRegionBounds(region), options);
|
|
206
|
+
}
|
|
207
|
+
screenToNormalized(point) {
|
|
208
|
+
return this.engine?.screenToNormalized(point) ?? { x: 0, y: 0 };
|
|
209
|
+
}
|
|
210
|
+
normalizedToScreen(point) {
|
|
211
|
+
return this.engine?.normalizedToScreen(point) ?? { x: 0, y: 0 };
|
|
212
|
+
}
|
|
213
|
+
resize() {
|
|
214
|
+
this.engine?.resize();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var MapRuntimeContext = react.createContext(null);
|
|
218
|
+
|
|
219
|
+
// src/client/hooks/useMapApi.ts
|
|
220
|
+
function useMapApi() {
|
|
221
|
+
return react.useContext(MapRuntimeContext)?.controller ?? null;
|
|
222
|
+
}
|
|
223
|
+
function useViewportState() {
|
|
224
|
+
const runtime = react.useContext(MapRuntimeContext);
|
|
225
|
+
if (!runtime) {
|
|
226
|
+
throw new Error("useViewportState must be used within a TileMapViewer");
|
|
227
|
+
}
|
|
228
|
+
return react.useSyncExternalStore(
|
|
229
|
+
(listener) => runtime.controller.store.subscribe(listener),
|
|
230
|
+
() => runtime.controller.store.getSnapshot()
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
function RegionLabelLayer({ region, projected }) {
|
|
234
|
+
if (!projected.center || !projected.text && !region.label) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
238
|
+
"div",
|
|
239
|
+
{
|
|
240
|
+
style: {
|
|
241
|
+
position: "absolute",
|
|
242
|
+
left: projected.center.x,
|
|
243
|
+
top: projected.center.y,
|
|
244
|
+
transform: "translate(-50%, -50%)",
|
|
245
|
+
pointerEvents: "none",
|
|
246
|
+
color: "#111827",
|
|
247
|
+
background: "rgba(255,255,255,0.92)",
|
|
248
|
+
padding: "2px 6px",
|
|
249
|
+
borderRadius: 6,
|
|
250
|
+
fontSize: 12,
|
|
251
|
+
lineHeight: 1.3,
|
|
252
|
+
border: "1px solid rgba(17,24,39,0.15)",
|
|
253
|
+
whiteSpace: "nowrap"
|
|
254
|
+
},
|
|
255
|
+
children: projected.text ?? region.label
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
function OverlayLayer(props) {
|
|
260
|
+
const api = useMapApi();
|
|
261
|
+
const view = useViewportState();
|
|
262
|
+
const projected = react.useMemo(() => {
|
|
263
|
+
if (!api) {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
return props.regions.map((region) => ({
|
|
267
|
+
region,
|
|
268
|
+
projected: projectRegion(api, region)
|
|
269
|
+
}));
|
|
270
|
+
}, [api, props.regions, view]);
|
|
271
|
+
if (!api) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
275
|
+
"div",
|
|
276
|
+
{
|
|
277
|
+
style: {
|
|
278
|
+
position: "absolute",
|
|
279
|
+
inset: 0,
|
|
280
|
+
pointerEvents: "none"
|
|
281
|
+
},
|
|
282
|
+
children: [
|
|
283
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
284
|
+
"svg",
|
|
285
|
+
{
|
|
286
|
+
width: "100%",
|
|
287
|
+
height: "100%",
|
|
288
|
+
style: {
|
|
289
|
+
position: "absolute",
|
|
290
|
+
inset: 0,
|
|
291
|
+
overflow: "visible"
|
|
292
|
+
},
|
|
293
|
+
children: projected.map(({ region, projected: projected2 }) => {
|
|
294
|
+
const isHovered = props.hoveredRegionId === region.id;
|
|
295
|
+
const isSelected = props.selectedRegionId === region.id;
|
|
296
|
+
const getLocalPoint = (event) => {
|
|
297
|
+
const rect = (event.currentTarget.ownerSVGElement ?? event.currentTarget).getBoundingClientRect();
|
|
298
|
+
return {
|
|
299
|
+
x: event.clientX - rect.left,
|
|
300
|
+
y: event.clientY - rect.top
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
const onPointerMove = (event) => {
|
|
304
|
+
const localPoint = getLocalPoint(event);
|
|
305
|
+
props.onRegionHover?.(region, {
|
|
306
|
+
screenPoint: localPoint,
|
|
307
|
+
normalizedPoint: api.screenToNormalized(localPoint),
|
|
308
|
+
nativeEvent: event
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
const onClick = (event) => {
|
|
312
|
+
const localPoint = getLocalPoint(event);
|
|
313
|
+
props.onRegionClick?.(region, {
|
|
314
|
+
screenPoint: localPoint,
|
|
315
|
+
normalizedPoint: api.screenToNormalized(localPoint),
|
|
316
|
+
nativeEvent: event
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
const custom = props.renderRegion?.({
|
|
320
|
+
region,
|
|
321
|
+
projected: projected2,
|
|
322
|
+
isHovered,
|
|
323
|
+
isSelected
|
|
324
|
+
});
|
|
325
|
+
if (custom) {
|
|
326
|
+
return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: custom }, region.id);
|
|
327
|
+
}
|
|
328
|
+
const sharedProps = {
|
|
329
|
+
onClick,
|
|
330
|
+
onPointerMove,
|
|
331
|
+
onPointerLeave: (event) => {
|
|
332
|
+
const localPoint = getLocalPoint(event);
|
|
333
|
+
props.onRegionHover?.(null, {
|
|
334
|
+
screenPoint: localPoint,
|
|
335
|
+
nativeEvent: event
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
style: {
|
|
339
|
+
pointerEvents: "auto",
|
|
340
|
+
cursor: "pointer"
|
|
341
|
+
},
|
|
342
|
+
fill: isSelected ? "rgba(37,99,235,0.24)" : isHovered ? "rgba(37,99,235,0.18)" : "rgba(37,99,235,0.12)",
|
|
343
|
+
stroke: isSelected ? "#1d4ed8" : "#2563eb",
|
|
344
|
+
strokeWidth: isSelected ? 2 : 1.5
|
|
345
|
+
};
|
|
346
|
+
switch (region.geometry.type) {
|
|
347
|
+
case "polygon":
|
|
348
|
+
return /* @__PURE__ */ jsxRuntime.jsx("path", { d: projected2.path, ...sharedProps }, region.id);
|
|
349
|
+
case "rectangle":
|
|
350
|
+
if (!projected2.rect) return null;
|
|
351
|
+
return /* @__PURE__ */ jsxRuntime.jsx("rect", { x: projected2.rect.x, y: projected2.rect.y, width: projected2.rect.width, height: projected2.rect.height, ...sharedProps }, region.id);
|
|
352
|
+
case "point":
|
|
353
|
+
if (!projected2.center) return null;
|
|
354
|
+
return /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: projected2.center.x, cy: projected2.center.y, r: region.geometry.radius ?? 8, ...sharedProps }, region.id);
|
|
355
|
+
case "label":
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
),
|
|
361
|
+
projected.map(({ region, projected: projected2 }) => /* @__PURE__ */ jsxRuntime.jsx(RegionLabelLayer, { region, projected: projected2 }, `${region.id}:label`))
|
|
362
|
+
]
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/shared/coordinates.ts
|
|
368
|
+
function clamp01(value) {
|
|
369
|
+
return Math.min(1, Math.max(0, value));
|
|
370
|
+
}
|
|
371
|
+
var tileLevelSchema = zod.z.object({
|
|
372
|
+
z: zod.z.number().int().min(0),
|
|
373
|
+
width: zod.z.number().int().positive(),
|
|
374
|
+
height: zod.z.number().int().positive(),
|
|
375
|
+
columns: zod.z.number().int().positive(),
|
|
376
|
+
rows: zod.z.number().int().positive(),
|
|
377
|
+
scale: zod.z.number().positive()
|
|
378
|
+
});
|
|
379
|
+
zod.z.object({
|
|
380
|
+
version: zod.z.literal(1),
|
|
381
|
+
kind: zod.z.literal("pdf-map"),
|
|
382
|
+
id: zod.z.string().min(1),
|
|
383
|
+
source: zod.z.object({
|
|
384
|
+
type: zod.z.union([zod.z.literal("pdf"), zod.z.literal("image")]),
|
|
385
|
+
originalFilename: zod.z.string().optional(),
|
|
386
|
+
page: zod.z.number().int().positive().optional(),
|
|
387
|
+
width: zod.z.number().int().positive(),
|
|
388
|
+
height: zod.z.number().int().positive(),
|
|
389
|
+
mimeType: zod.z.string().optional()
|
|
390
|
+
}),
|
|
391
|
+
coordinateSpace: zod.z.object({
|
|
392
|
+
normalized: zod.z.literal(true),
|
|
393
|
+
width: zod.z.number().int().positive(),
|
|
394
|
+
height: zod.z.number().int().positive()
|
|
395
|
+
}),
|
|
396
|
+
tiles: zod.z.object({
|
|
397
|
+
tileSize: zod.z.number().int().positive(),
|
|
398
|
+
format: zod.z.union([zod.z.literal("webp"), zod.z.literal("jpeg"), zod.z.literal("png")]),
|
|
399
|
+
minZoom: zod.z.number().int().min(0),
|
|
400
|
+
maxZoom: zod.z.number().int().min(0),
|
|
401
|
+
pathTemplate: zod.z.string().min(1),
|
|
402
|
+
levels: zod.z.array(tileLevelSchema)
|
|
403
|
+
}),
|
|
404
|
+
view: zod.z.object({
|
|
405
|
+
defaultCenter: zod.z.tuple([
|
|
406
|
+
zod.z.number().finite().min(0).max(1),
|
|
407
|
+
zod.z.number().finite().min(0).max(1)
|
|
408
|
+
]),
|
|
409
|
+
defaultZoom: zod.z.number().finite(),
|
|
410
|
+
minZoom: zod.z.number().finite(),
|
|
411
|
+
maxZoom: zod.z.number().finite()
|
|
412
|
+
}),
|
|
413
|
+
overlays: zod.z.object({
|
|
414
|
+
inline: regionCollectionSchema.optional(),
|
|
415
|
+
url: zod.z.string().optional()
|
|
416
|
+
}).optional(),
|
|
417
|
+
assets: zod.z.object({
|
|
418
|
+
preview: zod.z.string().optional()
|
|
419
|
+
}).optional(),
|
|
420
|
+
metadata: zod.z.object({
|
|
421
|
+
title: zod.z.string().optional(),
|
|
422
|
+
createdAt: zod.z.string().optional()
|
|
423
|
+
}).catchall(zod.z.unknown()).optional()
|
|
424
|
+
});
|
|
425
|
+
function resolveTileUrl(args) {
|
|
426
|
+
const template = args.overrideTemplate ?? args.manifest.tiles.pathTemplate;
|
|
427
|
+
const relative = template.replaceAll("{z}", String(args.z)).replaceAll("{x}", String(args.x)).replaceAll("{y}", String(args.y));
|
|
428
|
+
if (/^https?:\/\//.test(relative)) {
|
|
429
|
+
return relative;
|
|
430
|
+
}
|
|
431
|
+
if (!args.baseUrl) {
|
|
432
|
+
return relative;
|
|
433
|
+
}
|
|
434
|
+
if (/^https?:\/\//.test(args.baseUrl)) {
|
|
435
|
+
return new URL(relative.replace(/^\//, ""), ensureTrailingSlash(args.baseUrl)).toString();
|
|
436
|
+
}
|
|
437
|
+
return joinRelativeUrl(args.baseUrl, relative);
|
|
438
|
+
}
|
|
439
|
+
function ensureTrailingSlash(value) {
|
|
440
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
441
|
+
}
|
|
442
|
+
function joinRelativeUrl(baseUrl, path) {
|
|
443
|
+
const normalizedBase = ensureLeadingSlash(stripTrailingSlash(baseUrl));
|
|
444
|
+
const normalizedPath = stripLeadingSlash(path);
|
|
445
|
+
if (!normalizedPath) {
|
|
446
|
+
return normalizedBase || "/";
|
|
447
|
+
}
|
|
448
|
+
return normalizedBase ? `${normalizedBase}/${normalizedPath}` : `/${normalizedPath}`;
|
|
449
|
+
}
|
|
450
|
+
function stripLeadingSlash(value) {
|
|
451
|
+
return value.replace(/^\/+/, "");
|
|
452
|
+
}
|
|
453
|
+
function stripTrailingSlash(value) {
|
|
454
|
+
return value.replace(/\/+$/, "");
|
|
455
|
+
}
|
|
456
|
+
function ensureLeadingSlash(value) {
|
|
457
|
+
if (!value) {
|
|
458
|
+
return "";
|
|
459
|
+
}
|
|
460
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/client/engines/openSeadragonEngine.ts
|
|
464
|
+
async function createOpenSeadragonEngine(options) {
|
|
465
|
+
throwIfAborted(options.signal);
|
|
466
|
+
const OpenSeadragon = await import('openseadragon');
|
|
467
|
+
throwIfAborted(options.signal);
|
|
468
|
+
if (!options.container.isConnected) {
|
|
469
|
+
throw createAbortError();
|
|
470
|
+
}
|
|
471
|
+
const osd = OpenSeadragon.default;
|
|
472
|
+
const viewer = osd({
|
|
473
|
+
element: options.container,
|
|
474
|
+
showNavigationControl: false,
|
|
475
|
+
minZoomLevel: options.minZoom,
|
|
476
|
+
maxZoomLevel: options.maxZoom,
|
|
477
|
+
visibilityRatio: 1,
|
|
478
|
+
constrainDuringPan: true,
|
|
479
|
+
animationTime: 0.2,
|
|
480
|
+
gestureSettingsMouse: {
|
|
481
|
+
clickToZoom: false,
|
|
482
|
+
dblClickToZoom: true,
|
|
483
|
+
pinchToZoom: true,
|
|
484
|
+
flickEnabled: true,
|
|
485
|
+
scrollToZoom: true
|
|
486
|
+
},
|
|
487
|
+
tileSources: createTileSource(options.source)
|
|
488
|
+
});
|
|
489
|
+
if (options.signal?.aborted || !options.container.isConnected) {
|
|
490
|
+
viewer.destroy();
|
|
491
|
+
throw createAbortError();
|
|
492
|
+
}
|
|
493
|
+
let isOpen = false;
|
|
494
|
+
const publish = () => {
|
|
495
|
+
options.onViewChange?.(getView());
|
|
496
|
+
};
|
|
497
|
+
viewer.addHandler("open", () => {
|
|
498
|
+
isOpen = true;
|
|
499
|
+
if (options.initialView?.center) {
|
|
500
|
+
viewer.viewport.panTo(
|
|
501
|
+
imageToViewportPoint(options.initialView.center.x, options.initialView.center.y),
|
|
502
|
+
true
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (typeof options.initialView?.zoom === "number") {
|
|
506
|
+
viewer.viewport.zoomTo(options.initialView.zoom, void 0, true);
|
|
507
|
+
}
|
|
508
|
+
publish();
|
|
509
|
+
});
|
|
510
|
+
viewer.addHandler("animation", publish);
|
|
511
|
+
viewer.addHandler("resize", publish);
|
|
512
|
+
const dimensions = getDimensions(options.source);
|
|
513
|
+
const defaultView = getDefaultView(options, dimensions);
|
|
514
|
+
const getItem = () => {
|
|
515
|
+
const item = viewer.world.getItemAt(0);
|
|
516
|
+
return item && typeof item.viewportToImageCoordinates === "function" ? item : null;
|
|
517
|
+
};
|
|
518
|
+
const getContainerSize = () => {
|
|
519
|
+
const containerSize = viewer.container.getBoundingClientRect();
|
|
520
|
+
return {
|
|
521
|
+
width: containerSize.width,
|
|
522
|
+
height: containerSize.height
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
const imageToViewportPoint = (x, y) => {
|
|
526
|
+
const item = getItem();
|
|
527
|
+
if (!item) {
|
|
528
|
+
return new osd.Point(x, y);
|
|
529
|
+
}
|
|
530
|
+
return item.imageToViewportCoordinates(x * dimensions.width, y * dimensions.height);
|
|
531
|
+
};
|
|
532
|
+
const getView = () => {
|
|
533
|
+
const item = getItem();
|
|
534
|
+
const containerSize = getContainerSize();
|
|
535
|
+
if (!isOpen || !item) {
|
|
536
|
+
return {
|
|
537
|
+
...defaultView,
|
|
538
|
+
containerWidth: containerSize.width,
|
|
539
|
+
containerHeight: containerSize.height
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
const center = viewer.viewport.getCenter(true);
|
|
543
|
+
const imageCenter = item.viewportToImageCoordinates(center);
|
|
544
|
+
return {
|
|
545
|
+
center: {
|
|
546
|
+
x: clamp01(imageCenter.x / dimensions.width),
|
|
547
|
+
y: clamp01(imageCenter.y / dimensions.height)
|
|
548
|
+
},
|
|
549
|
+
zoom: viewer.viewport.getZoom(true),
|
|
550
|
+
minZoom: options.minZoom ?? 0,
|
|
551
|
+
maxZoom: options.maxZoom ?? 8,
|
|
552
|
+
containerWidth: containerSize.width,
|
|
553
|
+
containerHeight: containerSize.height
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
const engine = {
|
|
557
|
+
getView,
|
|
558
|
+
setView(view, transitionOptions) {
|
|
559
|
+
if (!isOpen || !getItem()) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (view.center) {
|
|
563
|
+
viewer.viewport.panTo(
|
|
564
|
+
imageToViewportPoint(view.center.x, view.center.y),
|
|
565
|
+
transitionOptions?.immediate ?? false
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
if (typeof view.zoom === "number") {
|
|
569
|
+
viewer.viewport.zoomTo(view.zoom, void 0, transitionOptions?.immediate ?? false);
|
|
570
|
+
}
|
|
571
|
+
publish();
|
|
572
|
+
},
|
|
573
|
+
fitToBounds(bounds, transitionOptions) {
|
|
574
|
+
const item = getItem();
|
|
575
|
+
if (!isOpen || !item) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (!bounds) {
|
|
579
|
+
viewer.viewport.goHome(transitionOptions?.immediate ?? false);
|
|
580
|
+
publish();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const topLeft = item.imageToViewportCoordinates(
|
|
584
|
+
bounds.x * dimensions.width,
|
|
585
|
+
bounds.y * dimensions.height
|
|
586
|
+
);
|
|
587
|
+
const bottomRight = item.imageToViewportCoordinates(
|
|
588
|
+
(bounds.x + bounds.width) * dimensions.width,
|
|
589
|
+
(bounds.y + bounds.height) * dimensions.height
|
|
590
|
+
);
|
|
591
|
+
viewer.viewport.fitBounds(
|
|
592
|
+
new osd.Rect(
|
|
593
|
+
topLeft.x,
|
|
594
|
+
topLeft.y,
|
|
595
|
+
bottomRight.x - topLeft.x || 0.01,
|
|
596
|
+
bottomRight.y - topLeft.y || 0.01
|
|
597
|
+
),
|
|
598
|
+
transitionOptions?.immediate ?? false
|
|
599
|
+
);
|
|
600
|
+
publish();
|
|
601
|
+
},
|
|
602
|
+
screenToNormalized(point) {
|
|
603
|
+
const item = getItem();
|
|
604
|
+
if (!isOpen || !item) {
|
|
605
|
+
const size = getContainerSize();
|
|
606
|
+
return {
|
|
607
|
+
x: size.width > 0 ? clamp01(point.x / size.width) : 0,
|
|
608
|
+
y: size.height > 0 ? clamp01(point.y / size.height) : 0
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const viewportPoint = viewer.viewport.pointFromPixel(
|
|
612
|
+
new osd.Point(point.x, point.y),
|
|
613
|
+
true
|
|
614
|
+
);
|
|
615
|
+
const imagePoint = item.viewportToImageCoordinates(viewportPoint);
|
|
616
|
+
return {
|
|
617
|
+
x: clamp01(imagePoint.x / dimensions.width),
|
|
618
|
+
y: clamp01(imagePoint.y / dimensions.height)
|
|
619
|
+
};
|
|
620
|
+
},
|
|
621
|
+
normalizedToScreen(point) {
|
|
622
|
+
if (!isOpen || !getItem()) {
|
|
623
|
+
const size = getContainerSize();
|
|
624
|
+
return {
|
|
625
|
+
x: point.x * size.width,
|
|
626
|
+
y: point.y * size.height
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const viewportPoint = imageToViewportPoint(point.x, point.y);
|
|
630
|
+
const pixel = viewer.viewport.pixelFromPoint(viewportPoint, true);
|
|
631
|
+
return {
|
|
632
|
+
x: pixel.x,
|
|
633
|
+
y: pixel.y
|
|
634
|
+
};
|
|
635
|
+
},
|
|
636
|
+
destroy() {
|
|
637
|
+
isOpen = false;
|
|
638
|
+
viewer.destroy();
|
|
639
|
+
},
|
|
640
|
+
resize() {
|
|
641
|
+
viewer.forceRedraw();
|
|
642
|
+
publish();
|
|
643
|
+
},
|
|
644
|
+
getContainer() {
|
|
645
|
+
return options.container;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
return engine;
|
|
649
|
+
}
|
|
650
|
+
function throwIfAborted(signal) {
|
|
651
|
+
if (signal?.aborted) {
|
|
652
|
+
throw createAbortError();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function createAbortError() {
|
|
656
|
+
const error = new Error("Viewer initialization aborted.");
|
|
657
|
+
error.name = "AbortError";
|
|
658
|
+
return error;
|
|
659
|
+
}
|
|
660
|
+
function getDefaultView(options, dimensions) {
|
|
661
|
+
const manifestView = options.source.type === "tiles" ? options.source.manifest.view : void 0;
|
|
662
|
+
return {
|
|
663
|
+
center: options.initialView?.center ?? normalizedCenterFromSource(options.source),
|
|
664
|
+
zoom: options.initialView?.zoom ?? manifestView?.defaultZoom ?? 1,
|
|
665
|
+
minZoom: options.minZoom ?? manifestView?.minZoom ?? 0,
|
|
666
|
+
maxZoom: options.maxZoom ?? manifestView?.maxZoom ?? 8,
|
|
667
|
+
containerWidth: dimensions.width,
|
|
668
|
+
containerHeight: dimensions.height
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function normalizedCenterFromSource(source) {
|
|
672
|
+
if (source.type === "tiles") {
|
|
673
|
+
return {
|
|
674
|
+
x: source.manifest.view.defaultCenter[0],
|
|
675
|
+
y: source.manifest.view.defaultCenter[1]
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
return { x: 0.5, y: 0.5 };
|
|
679
|
+
}
|
|
680
|
+
function createTileSource(source) {
|
|
681
|
+
if (source.type === "tiles") {
|
|
682
|
+
const manifest = source.manifest;
|
|
683
|
+
return {
|
|
684
|
+
width: manifest.source.width,
|
|
685
|
+
height: manifest.source.height,
|
|
686
|
+
tileSize: manifest.tiles.tileSize,
|
|
687
|
+
minLevel: manifest.tiles.minZoom,
|
|
688
|
+
maxLevel: manifest.tiles.maxZoom,
|
|
689
|
+
getTileUrl(level, x, y) {
|
|
690
|
+
if (source.getTileUrl) {
|
|
691
|
+
return source.getTileUrl({
|
|
692
|
+
manifest,
|
|
693
|
+
z: level,
|
|
694
|
+
x,
|
|
695
|
+
y
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
return resolveTileUrl({
|
|
699
|
+
manifest,
|
|
700
|
+
z: level,
|
|
701
|
+
x,
|
|
702
|
+
y,
|
|
703
|
+
baseUrl: source.baseUrl
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
if (source.type === "image") {
|
|
709
|
+
return {
|
|
710
|
+
type: "image",
|
|
711
|
+
url: source.src,
|
|
712
|
+
buildPyramid: false
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
throw new Error("OpenSeadragon engine only supports tile and image sources.");
|
|
716
|
+
}
|
|
717
|
+
function getDimensions(source) {
|
|
718
|
+
if (source.type === "tiles") {
|
|
719
|
+
return {
|
|
720
|
+
width: source.manifest.source.width,
|
|
721
|
+
height: source.manifest.source.height
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (source.type === "image") {
|
|
725
|
+
return {
|
|
726
|
+
width: source.width,
|
|
727
|
+
height: source.height
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
throw new Error("OpenSeadragon engine only supports image and tile sources.");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// src/shared/bytes.ts
|
|
734
|
+
function toUint8Array(input) {
|
|
735
|
+
if (input instanceof ArrayBuffer) {
|
|
736
|
+
return new Uint8Array(input);
|
|
737
|
+
}
|
|
738
|
+
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/client/engines/pdfJsEngine.ts
|
|
742
|
+
async function createPdfJsEngine(options) {
|
|
743
|
+
if (options.source.type !== "pdf") {
|
|
744
|
+
throw new Error("PDF.js engine only supports PDF sources.");
|
|
745
|
+
}
|
|
746
|
+
throwIfAborted2(options.signal);
|
|
747
|
+
const pdfjs = await import('pdfjs-dist/build/pdf.mjs');
|
|
748
|
+
throwIfAborted2(options.signal);
|
|
749
|
+
if (!options.container.isConnected) {
|
|
750
|
+
throw createAbortError2();
|
|
751
|
+
}
|
|
752
|
+
const file = options.source.file;
|
|
753
|
+
const loadingTask = pdfjs.getDocument(
|
|
754
|
+
typeof file === "string" ? {
|
|
755
|
+
url: file
|
|
756
|
+
} : {
|
|
757
|
+
data: toUint8Array(file)
|
|
758
|
+
}
|
|
759
|
+
);
|
|
760
|
+
const pdf = await loadingTask.promise;
|
|
761
|
+
if (options.signal?.aborted || !options.container.isConnected) {
|
|
762
|
+
loadingTask.destroy?.();
|
|
763
|
+
await pdf.destroy();
|
|
764
|
+
throw createAbortError2();
|
|
765
|
+
}
|
|
766
|
+
const page = await pdf.getPage(options.source.page ?? 1);
|
|
767
|
+
const canvas = document.createElement("canvas");
|
|
768
|
+
const context = canvas.getContext("2d");
|
|
769
|
+
if (!context) {
|
|
770
|
+
throw new Error("Unable to get 2D context for PDF canvas.");
|
|
771
|
+
}
|
|
772
|
+
options.container.innerHTML = "";
|
|
773
|
+
options.container.style.overflow = "hidden";
|
|
774
|
+
options.container.style.touchAction = "none";
|
|
775
|
+
options.container.appendChild(canvas);
|
|
776
|
+
const baseViewport = page.getViewport({ scale: 1 });
|
|
777
|
+
const containerRect = options.container.getBoundingClientRect();
|
|
778
|
+
const fitScale = containerRect.width > 0 && containerRect.height > 0 ? Math.min(containerRect.width / baseViewport.width, containerRect.height / baseViewport.height) : 1;
|
|
779
|
+
const renderScale = Math.max(1, fitScale);
|
|
780
|
+
const renderedViewport = page.getViewport({ scale: renderScale });
|
|
781
|
+
canvas.width = Math.ceil(renderedViewport.width);
|
|
782
|
+
canvas.height = Math.ceil(renderedViewport.height);
|
|
783
|
+
canvas.style.transformOrigin = "0 0";
|
|
784
|
+
canvas.style.willChange = "transform";
|
|
785
|
+
await page.render({
|
|
786
|
+
canvasContext: context,
|
|
787
|
+
viewport: renderedViewport
|
|
788
|
+
}).promise;
|
|
789
|
+
let view = {
|
|
790
|
+
center: options.initialView?.center ?? { x: 0.5, y: 0.5 },
|
|
791
|
+
zoom: options.initialView?.zoom ?? 1,
|
|
792
|
+
minZoom: options.minZoom ?? 0.5,
|
|
793
|
+
maxZoom: options.maxZoom ?? 8,
|
|
794
|
+
containerWidth: containerRect.width,
|
|
795
|
+
containerHeight: containerRect.height
|
|
796
|
+
};
|
|
797
|
+
let pan = { x: 0, y: 0 };
|
|
798
|
+
let dragging = false;
|
|
799
|
+
let dragOrigin = { x: 0, y: 0 };
|
|
800
|
+
const publish = () => {
|
|
801
|
+
options.onViewChange?.({ ...view });
|
|
802
|
+
};
|
|
803
|
+
const applyTransform = () => {
|
|
804
|
+
const rect = options.container.getBoundingClientRect();
|
|
805
|
+
view = {
|
|
806
|
+
...view,
|
|
807
|
+
containerWidth: rect.width,
|
|
808
|
+
containerHeight: rect.height
|
|
809
|
+
};
|
|
810
|
+
canvas.style.transform = `translate(${pan.x}px, ${pan.y}px) scale(${view.zoom})`;
|
|
811
|
+
publish();
|
|
812
|
+
};
|
|
813
|
+
const normalizedToCanvas = (point) => ({
|
|
814
|
+
x: point.x * canvas.width,
|
|
815
|
+
y: point.y * canvas.height
|
|
816
|
+
});
|
|
817
|
+
const screenToNormalized = (point) => {
|
|
818
|
+
const localX = (point.x - pan.x) / view.zoom;
|
|
819
|
+
const localY = (point.y - pan.y) / view.zoom;
|
|
820
|
+
return {
|
|
821
|
+
x: clamp01(localX / canvas.width),
|
|
822
|
+
y: clamp01(localY / canvas.height)
|
|
823
|
+
};
|
|
824
|
+
};
|
|
825
|
+
const handlePointerDown = (event) => {
|
|
826
|
+
dragging = true;
|
|
827
|
+
dragOrigin = { x: event.clientX - pan.x, y: event.clientY - pan.y };
|
|
828
|
+
};
|
|
829
|
+
const handlePointerMove = (event) => {
|
|
830
|
+
if (!dragging) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
pan = {
|
|
834
|
+
x: event.clientX - dragOrigin.x,
|
|
835
|
+
y: event.clientY - dragOrigin.y
|
|
836
|
+
};
|
|
837
|
+
applyTransform();
|
|
838
|
+
};
|
|
839
|
+
const handlePointerUp = () => {
|
|
840
|
+
dragging = false;
|
|
841
|
+
};
|
|
842
|
+
const handleWheel = (event) => {
|
|
843
|
+
event.preventDefault();
|
|
844
|
+
const delta = event.deltaY < 0 ? 1.1 : 0.9;
|
|
845
|
+
const nextZoom = Math.min(view.maxZoom, Math.max(view.minZoom, view.zoom * delta));
|
|
846
|
+
view = {
|
|
847
|
+
...view,
|
|
848
|
+
zoom: nextZoom
|
|
849
|
+
};
|
|
850
|
+
applyTransform();
|
|
851
|
+
};
|
|
852
|
+
options.container.addEventListener("pointerdown", handlePointerDown);
|
|
853
|
+
window.addEventListener("pointermove", handlePointerMove);
|
|
854
|
+
window.addEventListener("pointerup", handlePointerUp);
|
|
855
|
+
options.container.addEventListener("wheel", handleWheel, { passive: false });
|
|
856
|
+
applyTransform();
|
|
857
|
+
const engine = {
|
|
858
|
+
getView() {
|
|
859
|
+
return { ...view };
|
|
860
|
+
},
|
|
861
|
+
setView(nextView, transitionOptions) {
|
|
862
|
+
if (nextView.center) {
|
|
863
|
+
view = { ...view, center: nextView.center };
|
|
864
|
+
}
|
|
865
|
+
if (typeof nextView.zoom === "number") {
|
|
866
|
+
view = {
|
|
867
|
+
...view,
|
|
868
|
+
zoom: Math.min(view.maxZoom, Math.max(view.minZoom, nextView.zoom))
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
if (nextView.minZoom) {
|
|
872
|
+
view = { ...view, minZoom: nextView.minZoom };
|
|
873
|
+
}
|
|
874
|
+
if (nextView.maxZoom) {
|
|
875
|
+
view = { ...view, maxZoom: nextView.maxZoom };
|
|
876
|
+
}
|
|
877
|
+
if (transitionOptions?.immediate === false) {
|
|
878
|
+
requestAnimationFrame(applyTransform);
|
|
879
|
+
} else {
|
|
880
|
+
applyTransform();
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
fitToBounds(bounds) {
|
|
884
|
+
if (!bounds) {
|
|
885
|
+
view = { ...view, center: { x: 0.5, y: 0.5 }, zoom: 1 };
|
|
886
|
+
pan = { x: 0, y: 0 };
|
|
887
|
+
applyTransform();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const rect = options.container.getBoundingClientRect();
|
|
891
|
+
const zoomX = rect.width / (bounds.width * canvas.width || canvas.width);
|
|
892
|
+
const zoomY = rect.height / (bounds.height * canvas.height || canvas.height);
|
|
893
|
+
const nextZoom = Math.min(view.maxZoom, Math.max(view.minZoom, Math.min(zoomX, zoomY)));
|
|
894
|
+
const centerX = (bounds.x + bounds.width / 2) * canvas.width * nextZoom;
|
|
895
|
+
const centerY = (bounds.y + bounds.height / 2) * canvas.height * nextZoom;
|
|
896
|
+
pan = {
|
|
897
|
+
x: rect.width / 2 - centerX,
|
|
898
|
+
y: rect.height / 2 - centerY
|
|
899
|
+
};
|
|
900
|
+
view = {
|
|
901
|
+
...view,
|
|
902
|
+
center: {
|
|
903
|
+
x: bounds.x + bounds.width / 2,
|
|
904
|
+
y: bounds.y + bounds.height / 2
|
|
905
|
+
},
|
|
906
|
+
zoom: nextZoom
|
|
907
|
+
};
|
|
908
|
+
applyTransform();
|
|
909
|
+
},
|
|
910
|
+
screenToNormalized,
|
|
911
|
+
normalizedToScreen(point) {
|
|
912
|
+
const local = normalizedToCanvas(point);
|
|
913
|
+
return {
|
|
914
|
+
x: pan.x + local.x * view.zoom,
|
|
915
|
+
y: pan.y + local.y * view.zoom
|
|
916
|
+
};
|
|
917
|
+
},
|
|
918
|
+
destroy() {
|
|
919
|
+
options.container.removeEventListener("pointerdown", handlePointerDown);
|
|
920
|
+
window.removeEventListener("pointermove", handlePointerMove);
|
|
921
|
+
window.removeEventListener("pointerup", handlePointerUp);
|
|
922
|
+
options.container.removeEventListener("wheel", handleWheel);
|
|
923
|
+
loadingTask.destroy?.();
|
|
924
|
+
},
|
|
925
|
+
resize() {
|
|
926
|
+
applyTransform();
|
|
927
|
+
},
|
|
928
|
+
getContainer() {
|
|
929
|
+
return options.container;
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
return engine;
|
|
933
|
+
}
|
|
934
|
+
function throwIfAborted2(signal) {
|
|
935
|
+
if (signal?.aborted) {
|
|
936
|
+
throw createAbortError2();
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function createAbortError2() {
|
|
940
|
+
const error = new Error("Viewer initialization aborted.");
|
|
941
|
+
error.name = "AbortError";
|
|
942
|
+
return error;
|
|
943
|
+
}
|
|
944
|
+
var TileMapViewer = react.forwardRef(function TileMapViewer2(props, ref) {
|
|
945
|
+
const containerRef = react.useRef(null);
|
|
946
|
+
const controller = react.useMemo(() => new MapController(), []);
|
|
947
|
+
const [hoveredRegionId, setHoveredRegionId] = react.useState(null);
|
|
948
|
+
const sourceRef = react.useRef(props.source);
|
|
949
|
+
const initialViewRef = react.useRef(props.initialView);
|
|
950
|
+
const onViewChangeRef = react.useRef(props.onViewChange);
|
|
951
|
+
sourceRef.current = props.source;
|
|
952
|
+
initialViewRef.current = props.initialView;
|
|
953
|
+
onViewChangeRef.current = props.onViewChange;
|
|
954
|
+
const regions = react.useMemo(() => normalizeRegions(props.regions), [props.regions]);
|
|
955
|
+
const sourceKey = react.useMemo(() => getSourceKey(props.source), [props.source]);
|
|
956
|
+
const initialViewKey = react.useMemo(() => getInitialViewKey(props.initialView), [props.initialView]);
|
|
957
|
+
const mapApi = react.useMemo(
|
|
958
|
+
() => ({
|
|
959
|
+
getView: () => controller.getView(),
|
|
960
|
+
setView: (view, opts) => controller.setView(view, opts),
|
|
961
|
+
fitToBounds: (bounds, opts) => controller.fitToBounds(bounds, opts),
|
|
962
|
+
zoomToRegion: (regionId, opts) => controller.zoomToRegion(regionId, opts),
|
|
963
|
+
screenToNormalized: (point) => controller.screenToNormalized(point),
|
|
964
|
+
normalizedToScreen: (point) => controller.normalizedToScreen(point)
|
|
965
|
+
}),
|
|
966
|
+
[controller]
|
|
967
|
+
);
|
|
968
|
+
react.useEffect(() => {
|
|
969
|
+
controller.setRegions(regions);
|
|
970
|
+
}, [controller, regions]);
|
|
971
|
+
react.useImperativeHandle(ref, () => mapApi, [mapApi]);
|
|
972
|
+
react.useEffect(() => {
|
|
973
|
+
const container = containerRef.current;
|
|
974
|
+
if (!container) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const abortController = new AbortController();
|
|
978
|
+
let activeEngine = null;
|
|
979
|
+
const onViewChange = (view) => {
|
|
980
|
+
controller.store.setState(view);
|
|
981
|
+
onViewChangeRef.current?.(view);
|
|
982
|
+
};
|
|
983
|
+
const createEngine = async () => {
|
|
984
|
+
try {
|
|
985
|
+
const source = sourceRef.current;
|
|
986
|
+
const initialView = initialViewRef.current;
|
|
987
|
+
if (abortController.signal.aborted || containerRef.current !== container || !container.isConnected) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const engine = source.type === "pdf" ? await createPdfJsEngine({
|
|
991
|
+
container,
|
|
992
|
+
source,
|
|
993
|
+
minZoom: props.minZoom,
|
|
994
|
+
maxZoom: props.maxZoom,
|
|
995
|
+
initialView,
|
|
996
|
+
onViewChange,
|
|
997
|
+
signal: abortController.signal
|
|
998
|
+
}) : await createOpenSeadragonEngine({
|
|
999
|
+
container,
|
|
1000
|
+
source,
|
|
1001
|
+
minZoom: props.minZoom,
|
|
1002
|
+
maxZoom: props.maxZoom,
|
|
1003
|
+
initialView,
|
|
1004
|
+
onViewChange,
|
|
1005
|
+
signal: abortController.signal
|
|
1006
|
+
});
|
|
1007
|
+
if (abortController.signal.aborted || containerRef.current !== container || !container.isConnected) {
|
|
1008
|
+
engine.destroy();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
activeEngine = engine;
|
|
1012
|
+
controller.attachEngine(engine);
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
if (!(error instanceof Error) || error.name !== "AbortError") {
|
|
1015
|
+
console.error(error);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
void createEngine();
|
|
1020
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1021
|
+
controller.resize();
|
|
1022
|
+
});
|
|
1023
|
+
resizeObserver.observe(container);
|
|
1024
|
+
return () => {
|
|
1025
|
+
abortController.abort();
|
|
1026
|
+
resizeObserver.disconnect();
|
|
1027
|
+
if (activeEngine) {
|
|
1028
|
+
controller.detachEngine(activeEngine);
|
|
1029
|
+
activeEngine.destroy();
|
|
1030
|
+
}
|
|
1031
|
+
if (containerRef.current === container) {
|
|
1032
|
+
container.innerHTML = "";
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
}, [controller, initialViewKey, props.maxZoom, props.minZoom, sourceKey]);
|
|
1036
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1037
|
+
MapRuntimeContext.Provider,
|
|
1038
|
+
{
|
|
1039
|
+
value: {
|
|
1040
|
+
controller,
|
|
1041
|
+
regions
|
|
1042
|
+
},
|
|
1043
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1044
|
+
"div",
|
|
1045
|
+
{
|
|
1046
|
+
className: props.className,
|
|
1047
|
+
style: {
|
|
1048
|
+
position: "relative",
|
|
1049
|
+
width: "100%",
|
|
1050
|
+
height: "100%",
|
|
1051
|
+
minHeight: 320,
|
|
1052
|
+
background: "#f8fafc",
|
|
1053
|
+
overflow: "hidden",
|
|
1054
|
+
...props.style
|
|
1055
|
+
},
|
|
1056
|
+
children: [
|
|
1057
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1058
|
+
"div",
|
|
1059
|
+
{
|
|
1060
|
+
ref: containerRef,
|
|
1061
|
+
style: {
|
|
1062
|
+
position: "absolute",
|
|
1063
|
+
inset: 0
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
),
|
|
1067
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1068
|
+
OverlayLayer,
|
|
1069
|
+
{
|
|
1070
|
+
regions,
|
|
1071
|
+
selectedRegionId: props.selectedRegionId,
|
|
1072
|
+
hoveredRegionId,
|
|
1073
|
+
onRegionClick: props.onRegionClick,
|
|
1074
|
+
onRegionHover: (region, event) => {
|
|
1075
|
+
setHoveredRegionId(region?.id ?? null);
|
|
1076
|
+
props.onRegionHover?.(region, event);
|
|
1077
|
+
},
|
|
1078
|
+
renderRegion: props.renderRegion
|
|
1079
|
+
}
|
|
1080
|
+
)
|
|
1081
|
+
]
|
|
1082
|
+
}
|
|
1083
|
+
)
|
|
1084
|
+
}
|
|
1085
|
+
);
|
|
1086
|
+
});
|
|
1087
|
+
var objectIdCache = /* @__PURE__ */ new WeakMap();
|
|
1088
|
+
var nextObjectId = 1;
|
|
1089
|
+
function getSourceKey(source) {
|
|
1090
|
+
switch (source.type) {
|
|
1091
|
+
case "tiles":
|
|
1092
|
+
return JSON.stringify({
|
|
1093
|
+
type: source.type,
|
|
1094
|
+
manifestId: source.manifest.id,
|
|
1095
|
+
width: source.manifest.source.width,
|
|
1096
|
+
height: source.manifest.source.height,
|
|
1097
|
+
pathTemplate: source.manifest.tiles.pathTemplate,
|
|
1098
|
+
minZoom: source.manifest.tiles.minZoom,
|
|
1099
|
+
maxZoom: source.manifest.tiles.maxZoom,
|
|
1100
|
+
baseUrl: source.baseUrl ?? null,
|
|
1101
|
+
getTileUrl: source.getTileUrl ? getObjectId(source.getTileUrl) : null
|
|
1102
|
+
});
|
|
1103
|
+
case "image":
|
|
1104
|
+
return JSON.stringify({
|
|
1105
|
+
type: source.type,
|
|
1106
|
+
src: source.src,
|
|
1107
|
+
width: source.width,
|
|
1108
|
+
height: source.height
|
|
1109
|
+
});
|
|
1110
|
+
case "pdf":
|
|
1111
|
+
return JSON.stringify({
|
|
1112
|
+
type: source.type,
|
|
1113
|
+
page: source.page ?? 1,
|
|
1114
|
+
file: typeof source.file === "string" ? source.file : getObjectId(source.file)
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function getInitialViewKey(initialView) {
|
|
1119
|
+
if (!initialView) {
|
|
1120
|
+
return "null";
|
|
1121
|
+
}
|
|
1122
|
+
return JSON.stringify({
|
|
1123
|
+
center: initialView.center ? {
|
|
1124
|
+
x: initialView.center.x,
|
|
1125
|
+
y: initialView.center.y
|
|
1126
|
+
} : null,
|
|
1127
|
+
zoom: initialView.zoom ?? null,
|
|
1128
|
+
minZoom: initialView.minZoom ?? null,
|
|
1129
|
+
maxZoom: initialView.maxZoom ?? null
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
function getObjectId(value) {
|
|
1133
|
+
const existing = objectIdCache.get(value);
|
|
1134
|
+
if (existing) {
|
|
1135
|
+
return existing;
|
|
1136
|
+
}
|
|
1137
|
+
const id = nextObjectId;
|
|
1138
|
+
nextObjectId += 1;
|
|
1139
|
+
objectIdCache.set(value, id);
|
|
1140
|
+
return id;
|
|
1141
|
+
}
|
|
1142
|
+
var PdfMap = react.forwardRef(function PdfMap2(props, ref) {
|
|
1143
|
+
return /* @__PURE__ */ jsxRuntime.jsx(TileMapViewer, { ref, ...props });
|
|
1144
|
+
});
|
|
1145
|
+
function useRegions() {
|
|
1146
|
+
return react.useContext(MapRuntimeContext)?.regions ?? [];
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
exports.PdfMap = PdfMap;
|
|
1150
|
+
exports.TileMapViewer = TileMapViewer;
|
|
1151
|
+
exports.useMapApi = useMapApi;
|
|
1152
|
+
exports.useRegions = useRegions;
|
|
1153
|
+
exports.useViewportState = useViewportState;
|
|
1154
|
+
//# sourceMappingURL=index.cjs.map
|
|
1155
|
+
//# sourceMappingURL=index.cjs.map
|