dbf-mirador 4.3.0-testing2 → 4.3.0-testing4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -13
- package/dist/{CommentSharp-DaW0oBJy.js → CommentSharp-BJ6oQAe3.js} +2 -2
- package/dist/{CommentSharp-DaW0oBJy.js.map → CommentSharp-BJ6oQAe3.js.map} +1 -1
- package/dist/{FailedImageContext-DYZ9cZx9.js → FailedImageContext-BhA0TpVp.js} +22 -1
- package/dist/FailedImageContext-BhA0TpVp.js.map +1 -0
- package/dist/{GalleryView-DyLH1kk0.js → GalleryView-rCx0Q4f3.js} +3 -3
- package/dist/{GalleryView-DyLH1kk0.js.map → GalleryView-rCx0Q4f3.js.map} +1 -1
- package/dist/{OpenSeadragonViewer-CBmX5sYE.js → OpenSeadragonViewer-4nw8qaVM.js} +377 -327
- package/dist/OpenSeadragonViewer-4nw8qaVM.js.map +1 -0
- package/dist/{WindowViewer-BW0_w7wv.js → WindowViewer-uHVYcMsX.js} +2 -2
- package/dist/{WindowViewer-BW0_w7wv.js.map → WindowViewer-uHVYcMsX.js.map} +1 -1
- package/dist/{WorkspaceArea-C0RjqnIL.js → WorkspaceArea-7bydz9m-.js} +11 -11
- package/dist/{WorkspaceArea-C0RjqnIL.js.map → WorkspaceArea-7bydz9m-.js.map} +1 -1
- package/dist/mirador.es.js +1310 -1110
- package/dist/mirador.es.js.map +1 -1
- package/dist/mirador.min.js +65 -65
- package/dist/mirador.min.js.map +1 -1
- package/package.json +6 -2
- package/src/assets/icons/poi-marker.svg +2 -0
- package/src/components/AnnotationsOverlay.jsx +50 -28
- package/src/components/MapViewer.jsx +80 -0
- package/src/components/index.js +1 -0
- package/src/lib/AnnotationItem.js +18 -0
- package/src/lib/AnnotationResource.js +14 -0
- package/src/lib/CanvasAnnotationDisplay.js +104 -0
- package/src/plugins/poiPreviewPlugin.tsx +357 -0
- package/dist/FailedImageContext-DYZ9cZx9.js.map +0 -1
- package/dist/OpenSeadragonViewer-CBmX5sYE.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbf-mirador",
|
|
3
|
-
"version": "4.3.0-
|
|
3
|
+
"version": "4.3.0-testing4",
|
|
4
4
|
"description": "An open-source, web-based 'multi-up' viewer that supports zoom-pan-rotate functionality, ability to display/compare simple images, and images with annotations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/mirador.min.js",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"uuid": "^8.1.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
|
+
"@ckeditor/ckeditor5-react": "~11.0.1",
|
|
88
89
|
"@emotion/react": "^11.10.6",
|
|
89
90
|
"@emotion/styled": "^11.10.6",
|
|
90
91
|
"@eslint/compat": "^2.0.3",
|
|
@@ -100,6 +101,8 @@
|
|
|
100
101
|
"@vitest/coverage-v8": "^4.1.2",
|
|
101
102
|
"@vitest/ui": "^4.1.2",
|
|
102
103
|
"chalk": "^5.3.0",
|
|
104
|
+
"ckeditor5": "~47.6.1",
|
|
105
|
+
"dbf-mirador": "file:.",
|
|
103
106
|
"eslint": "^10.0.0",
|
|
104
107
|
"eslint-config-prettier": "^10.1.8",
|
|
105
108
|
"eslint-plugin-import": "^2.32.0",
|
|
@@ -125,7 +128,8 @@
|
|
|
125
128
|
"vitest-fetch-mock": "^0.4.2"
|
|
126
129
|
},
|
|
127
130
|
"overrides": {
|
|
128
|
-
"brace-expansion@1": "^1.1.16"
|
|
131
|
+
"brace-expansion@1": "^1.1.16",
|
|
132
|
+
"dbf-mirador": "file:."
|
|
129
133
|
},
|
|
130
134
|
"peerDependencies": {
|
|
131
135
|
"@emotion/react": "^11.10.6",
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
2
|
+
<svg width="800px" height="800px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--gis" preserveAspectRatio="xMidYMid meet"><path d="M50.002 0C30.763 0 15 15.718 15 34.902c0 7.432 2.374 14.34 6.392 20.019L45.73 96.994c3.409 4.453 5.675 3.607 8.51-.235l26.843-45.683c.542-.981.967-2.026 1.338-3.092A34.446 34.446 0 0 0 85 34.902C85 15.718 69.24 0 50.002 0zm0 16.354c10.359 0 18.597 8.218 18.597 18.548c0 10.33-8.238 18.544-18.597 18.544c-10.36 0-18.601-8.215-18.601-18.544c0-10.33 8.241-18.548 18.6-18.548z" fill="#000000"></path></svg>
|
|
@@ -2,15 +2,16 @@ import { useRef, useEffect, useCallback, useMemo } from 'react';
|
|
|
2
2
|
import ReactDOM from 'react-dom';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import { useDebouncedCallback } from 'use-debounce';
|
|
5
|
+
import partition from 'lodash/partition';
|
|
5
6
|
import sortBy from 'lodash/sortBy';
|
|
6
7
|
import xor from 'lodash/xor';
|
|
7
8
|
import OpenSeadragonCanvasOverlay from '../lib/OpenSeadragonCanvasOverlay';
|
|
8
9
|
import CanvasWorld from '../lib/CanvasWorld';
|
|
9
|
-
import CanvasAnnotationDisplay from '../lib/CanvasAnnotationDisplay';
|
|
10
|
+
import CanvasAnnotationDisplay, { poiHitTarget } from '../lib/CanvasAnnotationDisplay';
|
|
10
11
|
import { buildPath2D } from '../lib/svgShapesToPath';
|
|
11
12
|
|
|
12
13
|
/** @private */
|
|
13
|
-
function isAnnotationAtPoint(canvasWorld, osdCanvasOverlay, resource, canvas, point) {
|
|
14
|
+
function isAnnotationAtPoint({ canvasWorld, osdCanvasOverlay, viewer }, resource, canvas, point) {
|
|
14
15
|
const [canvasX, canvasY] = canvasWorld.canvasToWorldCoordinates(canvas.id);
|
|
15
16
|
const relativeX = point.x - canvasX;
|
|
16
17
|
const relativeY = point.y - canvasY;
|
|
@@ -21,6 +22,21 @@ function isAnnotationAtPoint(canvasWorld, osdCanvasOverlay, resource, canvas, po
|
|
|
21
22
|
return [...svgPaths].some((path) => context.isPointInPath(buildPath2D(path), relativeX, relativeY));
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
if (resource.pointSelector) {
|
|
26
|
+
const { x, y } = resource.pointSelector;
|
|
27
|
+
const osdCanvasIndex = canvasWorld.canvases.findIndex((c) => c.id === canvas.id);
|
|
28
|
+
const viewportCanvas = viewer.world.getItemAt(osdCanvasIndex);
|
|
29
|
+
if (!viewportCanvas) return false;
|
|
30
|
+
// The icon renders at a constant on-screen size regardless of zoom (see
|
|
31
|
+
// CanvasAnnotationDisplay#pointContext), so its clickable footprint must counter-scale the
|
|
32
|
+
// same way, or the hit region would shrink/grow with the image instead of the icon.
|
|
33
|
+
const zoomRatio = viewportCanvas.viewportToImageZoom(viewer.viewport.getZoom(true));
|
|
34
|
+
const hitTarget = poiHitTarget(x, y, zoomRatio);
|
|
35
|
+
const dx = relativeX - hitTarget.x;
|
|
36
|
+
const dy = relativeY - hitTarget.y;
|
|
37
|
+
return dx * dx + dy * dy <= hitTarget.radius * hitTarget.radius;
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
if (resource.fragmentSelector) {
|
|
25
41
|
const [x, y, w, h] = resource.fragmentSelector;
|
|
26
42
|
return x <= relativeX && relativeX <= x + w && y <= relativeY && relativeY <= y + h;
|
|
@@ -82,30 +98,36 @@ export function AnnotationsOverlay({
|
|
|
82
98
|
const annotationsToContext = useCallback(
|
|
83
99
|
(renderedAnnotations, currentPalette) => {
|
|
84
100
|
const context = osdCanvasOverlay.context2d;
|
|
85
|
-
renderedAnnotations.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
const resources = renderedAnnotations.flatMap((annotation) => annotation.resources);
|
|
102
|
+
|
|
103
|
+
// POI pins always draw last, on top of every other annotation shape on the same canvas -
|
|
104
|
+
// notably including a Journey's own annotation target (an svg/fragment selector, not a
|
|
105
|
+
// pointSelector), which would otherwise draw in plain store order and can end up covering
|
|
106
|
+
// - and stealing clicks from - a POI pin it happens to overlap on screen.
|
|
107
|
+
const [otherResources, poiResources] = partition(resources, (resource) => !resource.pointSelector);
|
|
108
|
+
|
|
109
|
+
[...otherResources, ...poiResources].forEach((resource) => {
|
|
110
|
+
const osdCanvasIndex = canvasWorld.canvases.findIndex((canvas) => canvas.id === resource.targetId);
|
|
111
|
+
if (osdCanvasIndex === -1) return;
|
|
112
|
+
const viewportCanvas = viewer.world.getItemAt(osdCanvasIndex);
|
|
113
|
+
if (!viewportCanvas) return;
|
|
114
|
+
const offset = canvasWorld.offsetByCanvas(resource.targetId);
|
|
115
|
+
const zoomRatio = viewportCanvas.viewportToImageZoom(viewer.viewport.getZoom(true));
|
|
116
|
+
const canvasAnnotationDisplay = new CanvasAnnotationDisplay({
|
|
117
|
+
hovered: hoveredAnnotationIds.includes(resource.id),
|
|
118
|
+
offset,
|
|
119
|
+
palette: {
|
|
120
|
+
...currentPalette,
|
|
121
|
+
default: {
|
|
122
|
+
...currentPalette.default,
|
|
123
|
+
...(!highlightAllAnnotations && currentPalette.hidden),
|
|
102
124
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
canvasAnnotationDisplay.toContext(context);
|
|
125
|
+
},
|
|
126
|
+
resource,
|
|
127
|
+
selected: selectedAnnotationId === resource.id,
|
|
128
|
+
zoomRatio,
|
|
108
129
|
});
|
|
130
|
+
canvasAnnotationDisplay.toContext(context);
|
|
109
131
|
});
|
|
110
132
|
},
|
|
111
133
|
[osdCanvasOverlay, viewer, canvasWorld, highlightAllAnnotations, hoveredAnnotationIds, selectedAnnotationId],
|
|
@@ -137,12 +159,12 @@ export function AnnotationsOverlay({
|
|
|
137
159
|
.filter((resource) => {
|
|
138
160
|
if (canvas.id !== resource.targetId) return false;
|
|
139
161
|
|
|
140
|
-
return isAnnotationAtPoint(canvasWorld, osdCanvasOverlay, resource, canvas, point);
|
|
162
|
+
return isAnnotationAtPoint({ canvasWorld, osdCanvasOverlay, viewer }, resource, canvas, point);
|
|
141
163
|
});
|
|
142
164
|
|
|
143
165
|
return annos;
|
|
144
166
|
},
|
|
145
|
-
[annotations, canvasWorld, osdCanvasOverlay, searchAnnotations],
|
|
167
|
+
[annotations, canvasWorld, osdCanvasOverlay, searchAnnotations, viewer],
|
|
146
168
|
);
|
|
147
169
|
|
|
148
170
|
const onCanvasClick = useCallback(
|
|
@@ -188,7 +210,7 @@ export function AnnotationsOverlay({
|
|
|
188
210
|
const x = Math.cos(degrees * degreesToRadians) * radius + point.x;
|
|
189
211
|
const y = Math.sin(degrees * degreesToRadians) * radius + point.y;
|
|
190
212
|
|
|
191
|
-
if (isAnnotationAtPoint(canvasWorld, osdCanvasOverlay, anno, canvas, { x, y })) score += 1;
|
|
213
|
+
if (isAnnotationAtPoint({ canvasWorld, osdCanvasOverlay, viewer }, anno, canvas, { x, y })) score += 1;
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
return { anno, score };
|
|
@@ -206,7 +228,7 @@ export function AnnotationsOverlay({
|
|
|
206
228
|
toggleAnnotation(annosWithScore[0].anno.id);
|
|
207
229
|
}
|
|
208
230
|
},
|
|
209
|
-
[annotationsAtPoint, canvasWorld, osdCanvasOverlay, toggleAnnotation],
|
|
231
|
+
[annotationsAtPoint, canvasWorld, osdCanvasOverlay, toggleAnnotation, viewer],
|
|
210
232
|
);
|
|
211
233
|
|
|
212
234
|
const onCanvasMouseMove = useDebouncedCallback(
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useEffect, useId, useRef } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { viewer } from '../init';
|
|
4
|
+
import { poiPreviewPlugins } from '../plugins/poiPreviewPlugin.tsx';
|
|
5
|
+
|
|
6
|
+
// Hides Mirador's generic multi-window IIIF-viewer chrome (top bar, workspace controls,
|
|
7
|
+
// canvas panel) so a manifest reads as a single interactive map rather than a document
|
|
8
|
+
// viewer, and opens straight onto the annotation sidebar since that's where a map's
|
|
9
|
+
// POIs/journeys live. Exported so callers who need Mirador's raw `viewer()`/`Mirador.viewer()`
|
|
10
|
+
// API (e.g. the integration demo config) can build on the same preset instead of copying it.
|
|
11
|
+
export const defaultMapViewerConfig = {
|
|
12
|
+
theme: {
|
|
13
|
+
components: {
|
|
14
|
+
WindowTopBar: {
|
|
15
|
+
styleOverrides: {
|
|
16
|
+
root: {
|
|
17
|
+
display: 'none',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
window: {
|
|
24
|
+
defaultSideBarPanel: 'annotations',
|
|
25
|
+
// Closed by default (issue #410): the left sidebar (annotation list, table of
|
|
26
|
+
// contents, etc.) is Mirador's own document-viewer chrome, not part of the map UI -
|
|
27
|
+
// POIs/journeys are opened through the canvas or the poiPreview companion window
|
|
28
|
+
// instead. `highlightAllAnnotations` still draws markers on the canvas regardless.
|
|
29
|
+
sideBarOpenByDefault: false,
|
|
30
|
+
highlightAllAnnotations: true,
|
|
31
|
+
panels: {
|
|
32
|
+
canvas: false,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
workspaceControlPanel: {
|
|
36
|
+
enabled: false,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Drop-in map renderer: give it a IIIF manifest URL, get Mirador pre-configured to look
|
|
42
|
+
* and behave like a map instead of a generic multi-window viewer. Wraps `viewer()` so
|
|
43
|
+
* callers don't need to know Mirador's own instantiation/config API.
|
|
44
|
+
*/
|
|
45
|
+
export function MapViewer({ manifestId }) {
|
|
46
|
+
const baseId = useId().replace(/:/g, '');
|
|
47
|
+
const wrapperRef = useRef(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const wrapper = wrapperRef.current;
|
|
51
|
+
if (!wrapper) return undefined;
|
|
52
|
+
|
|
53
|
+
const container = document.createElement('div');
|
|
54
|
+
container.id = `mirador-map-viewer-${baseId}`;
|
|
55
|
+
container.style.position = 'absolute';
|
|
56
|
+
container.style.inset = 0;
|
|
57
|
+
wrapper.appendChild(container);
|
|
58
|
+
|
|
59
|
+
const instance = viewer(
|
|
60
|
+
{
|
|
61
|
+
...defaultMapViewerConfig,
|
|
62
|
+
id: container.id,
|
|
63
|
+
windows: [{ manifestId }],
|
|
64
|
+
},
|
|
65
|
+
poiPreviewPlugins,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
instance.unmount();
|
|
70
|
+
container.remove();
|
|
71
|
+
};
|
|
72
|
+
}, [baseId, manifestId]);
|
|
73
|
+
|
|
74
|
+
return <div ref={wrapperRef} style={{ height: '100%', position: 'relative', width: '100%' }} />;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
MapViewer.propTypes = {
|
|
78
|
+
/** URL of the IIIF manifest to render as a map. */
|
|
79
|
+
manifestId: PropTypes.string.isRequired,
|
|
80
|
+
};
|
package/src/components/index.js
CHANGED
|
@@ -41,6 +41,7 @@ export * from './ManifestInfo';
|
|
|
41
41
|
export * from './ManifestListItem';
|
|
42
42
|
export * from './ManifestListItemError';
|
|
43
43
|
export * from './ManifestRelatedLinks';
|
|
44
|
+
export * from './MapViewer';
|
|
44
45
|
export * from './MinimalWindow';
|
|
45
46
|
export * from './MiradorMenuButton';
|
|
46
47
|
export * from './MosaicRenderPreview';
|
|
@@ -120,4 +120,22 @@ export default class AnnotationItem {
|
|
|
120
120
|
|
|
121
121
|
return match && match[1].split(',').map((str) => parseInt(str, 10));
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
/** */
|
|
125
|
+
get pointSelector() {
|
|
126
|
+
const { selector } = this;
|
|
127
|
+
switch (typeof selector) {
|
|
128
|
+
case 'string':
|
|
129
|
+
return null;
|
|
130
|
+
case 'object':
|
|
131
|
+
return selector.find((s) => s.type && s.type === 'PointSelector');
|
|
132
|
+
default:
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** the POI's position within its journey (see mirador-annotation-editor's `dbf:journey`), or null when the POI is not part of a journey */
|
|
138
|
+
get journeyOrder() {
|
|
139
|
+
return this.resource['dbf:journey']?.order ?? null;
|
|
140
|
+
}
|
|
123
141
|
}
|
|
@@ -119,4 +119,18 @@ export default class AnnotationResource {
|
|
|
119
119
|
|
|
120
120
|
return match && match[1].split(',').map((str) => parseInt(str, 10));
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
/** */
|
|
124
|
+
get pointSelector() {
|
|
125
|
+
const { selector } = this;
|
|
126
|
+
if (selector && typeof selector === 'object' && selector['@type'] === 'oa:PointSelector') {
|
|
127
|
+
return { x: selector.x, y: selector.y };
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** the POI's position within its journey (see mirador-annotation-editor's `dbf:journey`), or null when the POI is not part of a journey */
|
|
133
|
+
get journeyOrder() {
|
|
134
|
+
return this.resource['dbf:journey']?.order ?? null;
|
|
135
|
+
}
|
|
122
136
|
}
|
|
@@ -4,6 +4,51 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { buildPath2D } from '../lib/svgShapesToPath';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The POI marker icon's path data and viewBox, copied from src/assets/icons/poi-marker.svg -
|
|
9
|
+
* inlined as a Path2D (rather than loaded as an image via drawImage) so it renders as a crisp
|
|
10
|
+
* vector at any zoom with no async image-load step to coordinate with the render loop. Update
|
|
11
|
+
* this constant (and the asset file, for reference) together if the icon changes.
|
|
12
|
+
*/
|
|
13
|
+
const POI_ICON_PATH_D =
|
|
14
|
+
'M50.002 0C30.763 0 15 15.718 15 34.902c0 7.432 2.374 14.34 6.392 20.019L45.73 96.994c3.409 4.453 5.675 3.607 8.51-.235l26.843-45.683c.542-.981.967-2.026 1.338-3.092A34.446 34.446 0 0 0 85 34.902C85 15.718 69.24 0 50.002 0zm0 16.354c10.359 0 18.597 8.218 18.597 18.548c0 10.33-8.238 18.544-18.597 18.544c-10.36 0-18.601-8.215-18.601-18.544c0-10.33 8.241-18.548 18.6-18.548z';
|
|
15
|
+
const POI_ICON_VIEWBOX_SIZE = 100;
|
|
16
|
+
/** The icon's own point (its pin tip), in viewBox units - this is what gets placed exactly on the annotated coordinate, not the icon's bounding-box center. Derived from the path's actual lowest point (~50, 100), not just its nearby `L` command endpoint. */
|
|
17
|
+
const POI_ICON_TIP = { x: 50, y: 100 };
|
|
18
|
+
/** Center/radius of the icon's circular head, in viewBox units - where a journey-order badge is drawn, overlapping the icon like a numbered pin. */
|
|
19
|
+
const POI_ICON_HEAD = { x: 50, y: 34.9, radius: 18.6 };
|
|
20
|
+
/** Font size of the journey-order number, relative to POI_ICON_HEAD.radius, so it's bigger than the badge circle would otherwise imply while the circle itself stays the same size. */
|
|
21
|
+
const POI_ICON_HEAD_FONT_SCALE = 1.6;
|
|
22
|
+
const POI_ICON_DEFAULT_FILL = '#1e88e5';
|
|
23
|
+
/** Constant on-screen height (CSS px) for the POI icon, regardless of zoom - counter-scaled the same way this class already counter-scales stroke width (see `lineWidth /= zoomRatio` in svgContext). */
|
|
24
|
+
export const POI_ICON_HEIGHT_PX = 44;
|
|
25
|
+
/**
|
|
26
|
+
* On-screen radius (CSS px, before counter-scaling by zoomRatio) of a POI marker's
|
|
27
|
+
* clickable/tappable hit target - deliberately independent of, and larger than, the icon's
|
|
28
|
+
* visual size (POI_ICON_HEIGHT_PX). The icon tapers to a fine point at its anchor, which is
|
|
29
|
+
* hard to hit precisely with a mouse and well under the ~44-48px minimum touch target size
|
|
30
|
+
* recommended for mobile (Apple HIG / Material Design), so the tappable area is deliberately
|
|
31
|
+
* generous rather than matching the icon's own footprint.
|
|
32
|
+
*/
|
|
33
|
+
export const POI_ICON_HIT_RADIUS_PX = 32;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The on-screen hit-test circle (already counter-scaled by zoomRatio, in the same coordinate
|
|
37
|
+
* space as a resource's `pointSelector`) for a POI marker anchored at (x, y). Centered on the
|
|
38
|
+
* icon's round head rather than its pin tip: the tip is the anchor point used for rendering
|
|
39
|
+
* (see pointContext/POI_ICON_TIP), but it's the least visually prominent, hardest-to-aim-for
|
|
40
|
+
* part of the icon, so hit-testing there instead of at the head would make the marker harder,
|
|
41
|
+
* not easier, to hit.
|
|
42
|
+
*/
|
|
43
|
+
export function poiHitTarget(x, y, zoomRatio) {
|
|
44
|
+
const iconScale = POI_ICON_HEIGHT_PX / zoomRatio / POI_ICON_VIEWBOX_SIZE;
|
|
45
|
+
return {
|
|
46
|
+
radius: POI_ICON_HIT_RADIUS_PX / zoomRatio,
|
|
47
|
+
x: x + (POI_ICON_HEAD.x - POI_ICON_TIP.x) * iconScale,
|
|
48
|
+
y: y + (POI_ICON_HEAD.y - POI_ICON_TIP.y) * iconScale,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
7
52
|
export default class CanvasAnnotationDisplay {
|
|
8
53
|
/** */
|
|
9
54
|
constructor({ resource, palette, zoomRatio, offset, selected, hovered }) {
|
|
@@ -20,11 +65,70 @@ export default class CanvasAnnotationDisplay {
|
|
|
20
65
|
this.context = context;
|
|
21
66
|
if (this.resource.svgSelector) {
|
|
22
67
|
this.svgContext();
|
|
68
|
+
} else if (this.resource.pointSelector) {
|
|
69
|
+
this.pointContext();
|
|
23
70
|
} else if (this.resource.fragmentSelector) {
|
|
24
71
|
this.fragmentContext();
|
|
25
72
|
}
|
|
26
73
|
}
|
|
27
74
|
|
|
75
|
+
/** */
|
|
76
|
+
currentPalette() {
|
|
77
|
+
if (this.hovered) return this.palette.hovered;
|
|
78
|
+
if (this.selected) return this.palette.selected;
|
|
79
|
+
return this.palette.default;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Draws a POI marker (IIIF PointSelector) as the SVG pin icon, at a constant on-screen size,
|
|
84
|
+
* with the icon's own tip placed exactly on the annotated point. When the annotation carries
|
|
85
|
+
* journey-order metadata (`dbf:journey.order`), also draws a numbered badge over the icon's
|
|
86
|
+
* head, so a POI's position within its journey is visible directly on the map.
|
|
87
|
+
*/
|
|
88
|
+
pointContext() {
|
|
89
|
+
const { x, y } = this.resource.pointSelector;
|
|
90
|
+
const currentPalette = this.currentPalette();
|
|
91
|
+
if (currentPalette.globalAlpha === 0) return;
|
|
92
|
+
|
|
93
|
+
const iconHeight = POI_ICON_HEIGHT_PX / this.zoomRatio;
|
|
94
|
+
const iconScale = iconHeight / POI_ICON_VIEWBOX_SIZE;
|
|
95
|
+
|
|
96
|
+
this.context.save();
|
|
97
|
+
this.context.translate(this.offset.x + x, this.offset.y + y);
|
|
98
|
+
this.context.scale(iconScale, iconScale);
|
|
99
|
+
this.context.translate(-POI_ICON_TIP.x, -POI_ICON_TIP.y);
|
|
100
|
+
|
|
101
|
+
this.context.globalAlpha = currentPalette.globalAlpha ?? 1;
|
|
102
|
+
this.context.fillStyle = currentPalette.fillStyle || POI_ICON_DEFAULT_FILL;
|
|
103
|
+
this.context.fill(new Path2D(POI_ICON_PATH_D));
|
|
104
|
+
|
|
105
|
+
const { journeyOrder } = this.resource;
|
|
106
|
+
if (journeyOrder != null) {
|
|
107
|
+
this.journeyOrderBadgeContext(journeyOrder);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.context.restore();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Draws a small numbered circle over the POI icon's head, in the icon's own (viewBox-unit,
|
|
115
|
+
* already-scaled/translated) coordinate space - called from within pointContext's transform,
|
|
116
|
+
* before it restores the context.
|
|
117
|
+
* @param {number} order
|
|
118
|
+
*/
|
|
119
|
+
journeyOrderBadgeContext(order) {
|
|
120
|
+
this.context.beginPath();
|
|
121
|
+
this.context.arc(POI_ICON_HEAD.x, POI_ICON_HEAD.y, POI_ICON_HEAD.radius, 0, Math.PI * 2);
|
|
122
|
+
this.context.fillStyle = '#ffffff';
|
|
123
|
+
this.context.fill();
|
|
124
|
+
|
|
125
|
+
this.context.fillStyle = this.currentPalette().fillStyle || POI_ICON_DEFAULT_FILL;
|
|
126
|
+
this.context.font = `bold ${POI_ICON_HEAD.radius * POI_ICON_HEAD_FONT_SCALE}px sans-serif`;
|
|
127
|
+
this.context.textAlign = 'center';
|
|
128
|
+
this.context.textBaseline = 'middle';
|
|
129
|
+
this.context.fillText(String(order), POI_ICON_HEAD.x, POI_ICON_HEAD.y);
|
|
130
|
+
}
|
|
131
|
+
|
|
28
132
|
/** */
|
|
29
133
|
get svgString() {
|
|
30
134
|
return this.resource.svgSelector.value;
|