diff options
Diffstat (limited to 'src/Link.mjs')
-rw-r--r-- | src/Link.mjs | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/src/Link.mjs b/src/Link.mjs new file mode 100644 index 0000000..61aa5f8 --- /dev/null +++ b/src/Link.mjs | |||
@@ -0,0 +1,239 @@ | |||
1 | import LeaderLine from 'leader-line' | ||
2 | import { insideWindow, insideParent } from './utils' | ||
3 | |||
4 | /** VAR: pattern for coodinates */ | ||
5 | export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ | ||
6 | |||
7 | /** | ||
8 | * Class: GeoLink - link for maps | ||
9 | * | ||
10 | * @extends {window.HTMLAnchorElement} | ||
11 | */ | ||
12 | export class GeoLink extends window.HTMLAnchorElement { | ||
13 | static replaceWith = (link) => | ||
14 | link.replaceWith(new GeoLink(link)) | ||
15 | |||
16 | /** | ||
17 | * Creates a new GeoLink instance | ||
18 | * | ||
19 | * @param {HTMLAnchorElement} link | ||
20 | */ | ||
21 | constructor (link) { | ||
22 | super() | ||
23 | this.innerHTML = link.innerHTML | ||
24 | this.href = link.href | ||
25 | |||
26 | const url = new URL(link.href) | ||
27 | const params = new URLSearchParams(link.search) | ||
28 | const xyInParams = params.get('xy')?.split(',')?.map(Number) | ||
29 | const [lon, lat] = url.href | ||
30 | ?.match(coordPattern) | ||
31 | ?.slice(1) | ||
32 | ?.reverse() | ||
33 | ?.map(Number) | ||
34 | const xy = xyInParams ?? [lon, lat] | ||
35 | |||
36 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false | ||
37 | |||
38 | // Geo information in link | ||
39 | this.dataset.lon = lon | ||
40 | this.dataset.lat = lat | ||
41 | this.dataset.crs = params.get('crs') | ||
42 | this.classList.add('with-leader-line', 'geolink') | ||
43 | this.classList.remove('not-geolink') | ||
44 | // TODO refactor as data attribute | ||
45 | this.targets = params.get('id')?.split(',') ?? null | ||
46 | this.title = 'Left-Click to move Camera, Middle-Click to clean anchor' | ||
47 | this.lines = [] | ||
48 | |||
49 | // Hover link for LeaderLine | ||
50 | this.onmouseover = () => this.getMarkersFromMaps(this) | ||
51 | .filter(isAnchorVisible) | ||
52 | .forEach(anchor => { | ||
53 | const labelText = new URL(this).searchParams.get('text') ?? this.textContent | ||
54 | const line = new LeaderLine({ | ||
55 | start: this, | ||
56 | end: anchor, | ||
57 | hide: true, | ||
58 | middleLabel: labelText, | ||
59 | path: 'magnet', | ||
60 | }) | ||
61 | line.show('draw', { duration: 300 }) | ||
62 | |||
63 | this.lines.push(line) | ||
64 | }) | ||
65 | |||
66 | this.onmouseout = () => removeLeaderLines(this) | ||
67 | |||
68 | // Click to move camera | ||
69 | this.onclick = (event) => { | ||
70 | event.preventDefault() | ||
71 | removeLeaderLines(this) | ||
72 | this.getMarkersFromMaps().forEach(marker => { | ||
73 | const map = marker.closest('.mapclay') | ||
74 | map.scrollIntoView({ behavior: 'smooth' }) | ||
75 | updateMapCameraByMarker([ | ||
76 | Number(this.dataset.lon), | ||
77 | Number(this.dataset.lat), | ||
78 | ])(marker) | ||
79 | }) | ||
80 | } | ||
81 | |||
82 | // Use middle click to remove markers | ||
83 | this.onauxclick = (e) => { | ||
84 | if (e.which !== 2) return | ||
85 | e.preventDefault() | ||
86 | removeLeaderLines(this) | ||
87 | this.getMarkersFromMaps() | ||
88 | .forEach(marker => marker.remove()) | ||
89 | } | ||
90 | } | ||
91 | |||
92 | /** | ||
93 | * getMarkersFromMaps. Get marker elements by GeoLink | ||
94 | * | ||
95 | * @param {HTMLAnchorElement} link | ||
96 | * @return {HTMLElement[]} markers | ||
97 | */ | ||
98 | getMarkersFromMaps () { | ||
99 | const params = new URLSearchParams(this.search) | ||
100 | const maps = Array.from( | ||
101 | this.closest('.Dumby') | ||
102 | .querySelectorAll('.mapclay[data-render="fulfilled"]'), | ||
103 | ) | ||
104 | return maps | ||
105 | .filter(map => this.targets ? this.targets.includes(map.id) : true) | ||
106 | .map(map => { | ||
107 | const renderer = map.renderer | ||
108 | const lonLat = [Number(this.dataset.lon), Number(this.dataset.lat)] | ||
109 | |||
110 | const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? | ||
111 | renderer.addMarker({ | ||
112 | xy: lonLat, | ||
113 | type: params.get('type') ?? null, | ||
114 | }) | ||
115 | marker.dataset.xy = lonLat | ||
116 | marker.title = new URLSearchParams(this.search).get('xy') ?? lonLat | ||
117 | const crs = this.dataset.crs | ||
118 | if (crs && crs !== 'EPSG:4326') { | ||
119 | marker.title += '@' + this.dataset.crs | ||
120 | } | ||
121 | |||
122 | return marker | ||
123 | }) | ||
124 | } | ||
125 | } | ||
126 | if (!window.customElements.get('dumby-geolink')) { | ||
127 | window.customElements.define('dumby-geolink', GeoLink, { extends: 'a' }) | ||
128 | } | ||
129 | |||
130 | /** | ||
131 | * Class: DocLink - link for DOM | ||
132 | * | ||
133 | * @extends {window.HTMLAnchorElement} | ||
134 | */ | ||
135 | export class DocLink extends window.HTMLAnchorElement { | ||
136 | static replaceWith = (link) => | ||
137 | link.replaceWith(new DocLink(link)) | ||
138 | |||
139 | /** | ||
140 | * Creates a new DocLink instance | ||
141 | * | ||
142 | * @param {HTMLAnchorElement} link | ||
143 | */ | ||
144 | constructor (link) { | ||
145 | super() | ||
146 | this.innerHTML = link.innerHTML | ||
147 | this.href = link.href | ||
148 | |||
149 | const label = decodeURIComponent(link.href.split('#')[1]) | ||
150 | const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) | ||
151 | if (!selector) return false | ||
152 | |||
153 | this.classList.add('with-leader-line', 'doclink') | ||
154 | this.lines = [] | ||
155 | |||
156 | this.onmouseover = () => { | ||
157 | const targets = document.querySelectorAll(selector) | ||
158 | |||
159 | targets.forEach(target => { | ||
160 | if (!target?.checkVisibility()) return | ||
161 | |||
162 | // highlight selected target | ||
163 | target.dataset.style = target.style.cssText | ||
164 | const rect = target.getBoundingClientRect() | ||
165 | const isTiny = rect.width < 100 || rect.height < 100 | ||
166 | if (isTiny) { | ||
167 | target.style.background = 'lightPink' | ||
168 | } else { | ||
169 | target.style.outline = 'lightPink 6px dashed' | ||
170 | } | ||
171 | |||
172 | // point to selected target | ||
173 | const line = new LeaderLine({ | ||
174 | start: this, | ||
175 | end: target, | ||
176 | middleLabel: LeaderLine.pathLabel({ | ||
177 | text: label, | ||
178 | fontWeight: 'bold', | ||
179 | }), | ||
180 | hide: true, | ||
181 | path: 'magnet', | ||
182 | }) | ||
183 | this.lines.push(line) | ||
184 | line.show('draw', { duration: 300 }) | ||
185 | }) | ||
186 | } | ||
187 | |||
188 | this.onmouseout = () => { | ||
189 | removeLeaderLines(this) | ||
190 | |||
191 | // resume targets from highlight | ||
192 | const targets = document.querySelectorAll(selector) | ||
193 | targets.forEach(target => { | ||
194 | target.style.cssText = target.dataset.style | ||
195 | delete target.dataset.style | ||
196 | }) | ||
197 | } | ||
198 | } | ||
199 | } | ||
200 | if (!window.customElements.get('dumby-doclink')) { | ||
201 | window.customElements.define('dumby-doclink', DocLink, { extends: 'a' }) | ||
202 | } | ||
203 | |||
204 | /** | ||
205 | * isAnchorVisible. check anchor(marker) is visible for current map camera | ||
206 | * | ||
207 | * @param {Element} anchor | ||
208 | */ | ||
209 | const isAnchorVisible = anchor => { | ||
210 | const mapContainer = anchor.closest('.mapclay') | ||
211 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | ||
212 | } | ||
213 | |||
214 | /** | ||
215 | * updateMapByMarker. get function for updating map camera by marker | ||
216 | * | ||
217 | * @param {Number[]} xy | ||
218 | * @return {Function} function | ||
219 | */ | ||
220 | const updateMapCameraByMarker = lonLat => marker => { | ||
221 | const renderer = marker.closest('.mapclay')?.renderer | ||
222 | renderer.updateCamera({ center: lonLat }, true) | ||
223 | } | ||
224 | |||
225 | /** | ||
226 | * removeLeaderLines. clean lines start from link | ||
227 | * | ||
228 | * @param {HTMLAnchorElement} link | ||
229 | */ | ||
230 | export const removeLeaderLines = link => { | ||
231 | if (!link.lines) return | ||
232 | link.lines.forEach(line => { | ||
233 | line.hide('draw', { duration: 300 }) | ||
234 | setTimeout(() => { | ||
235 | line.remove() | ||
236 | }, 300) | ||
237 | }) | ||
238 | link.lines = [] | ||
239 | } | ||