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.
@@ -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