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