diff options
Diffstat (limited to 'src/dumbyUtils.mjs')
-rw-r--r-- | src/dumbyUtils.mjs | 292 |
1 files changed, 36 insertions, 256 deletions
diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index 8fe23eb..6a1eaca 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs | |||
@@ -1,8 +1,7 @@ | |||
1 | import LeaderLine from 'leader-line' | 1 | import LeaderLine from 'leader-line' |
2 | import { insideWindow, insideParent, replaceTextNodes, full2Half } from './utils' | 2 | import { replaceTextNodes, full2Half } from './utils' |
3 | import proj4 from 'proj4' | 3 | import proj4 from 'proj4' |
4 | 4 | import { coordPattern, GeoLink } from './Link.mjs' | |
5 | export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ | ||
6 | 5 | ||
7 | /** | 6 | /** |
8 | * focusNextMap. | 7 | * focusNextMap. |
@@ -88,225 +87,6 @@ export function removeBlockFocus () { | |||
88 | } | 87 | } |
89 | 88 | ||
90 | /** | 89 | /** |
91 | * getMarkersFromMaps. Get marker elements by GeoLink | ||
92 | * | ||
93 | * @param {HTMLAnchorElement} link | ||
94 | * @return {HTMLElement[]} markers | ||
95 | */ | ||
96 | export const getMarkersFromMaps = link => { | ||
97 | const params = new URLSearchParams(link.search) | ||
98 | const maps = Array.from( | ||
99 | link.closest('.Dumby') | ||
100 | .querySelectorAll('.mapclay[data-render="fulfilled"]'), | ||
101 | ) | ||
102 | return maps | ||
103 | .filter(map => link.targets ? link.targets.includes(map.id) : true) | ||
104 | .map(map => { | ||
105 | const renderer = map.renderer | ||
106 | const lonLat = [Number(link.dataset.lon), Number(link.dataset.lat)] | ||
107 | |||
108 | const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? | ||
109 | renderer.addMarker({ | ||
110 | xy: lonLat, | ||
111 | type: params.get('type') ?? null, | ||
112 | }) | ||
113 | marker.dataset.xy = lonLat | ||
114 | marker.title = new URLSearchParams(link.search).get('xy') ?? lonLat | ||
115 | const crs = link.dataset.crs | ||
116 | if (crs && crs !== 'EPSG:4326') { | ||
117 | marker.title += '@' + link.dataset.crs | ||
118 | } | ||
119 | |||
120 | return marker | ||
121 | }) | ||
122 | } | ||
123 | |||
124 | /** | ||
125 | * addLeaderLine, from link element to target element | ||
126 | * | ||
127 | * @param {HTMLAnchorElement} link | ||
128 | * @param {Element} target | ||
129 | */ | ||
130 | const addLeaderLine = (link, target) => { | ||
131 | const labelText = new URL(link).searchParams.get('text') ?? link.textContent | ||
132 | const line = new LeaderLine({ | ||
133 | start: link, | ||
134 | end: target, | ||
135 | hide: true, | ||
136 | middleLabel: labelText, | ||
137 | path: 'magnet', | ||
138 | }) | ||
139 | line.show('draw', { duration: 300 }) | ||
140 | |||
141 | return line | ||
142 | } | ||
143 | |||
144 | /** | ||
145 | * Create geolinks, which points to map by geo schema and id | ||
146 | * | ||
147 | * @param {HTMLElement} Elements contains anchor elements for GeoLinks | ||
148 | * @returns {Boolean} ture is link is created, false if coordinates are invalid | ||
149 | */ | ||
150 | export const createGeoLink = (link) => { | ||
151 | const url = new URL(link.href) | ||
152 | const params = new URLSearchParams(link.search) | ||
153 | const xyInParams = params.get('xy')?.split(',')?.map(Number) | ||
154 | const [lon, lat] = url.href | ||
155 | ?.match(coordPattern) | ||
156 | ?.slice(1) | ||
157 | ?.reverse() | ||
158 | ?.map(Number) | ||
159 | const xy = xyInParams ?? [lon, lat] | ||
160 | |||
161 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false | ||
162 | |||
163 | // Geo information in link | ||
164 | link.dataset.lon = lon | ||
165 | link.dataset.lat = lat | ||
166 | link.dataset.crs = params.get('crs') | ||
167 | link.classList.add('with-leader-line', 'geolink') | ||
168 | link.classList.remove('not-geolink') | ||
169 | // TODO refactor as data attribute | ||
170 | link.targets = params.get('id')?.split(',') ?? null | ||
171 | link.title = 'Left-Click to move Camera, Middle-Click to clean anchor' | ||
172 | |||
173 | link.lines = [] | ||
174 | |||
175 | // Hover link for LeaderLine | ||
176 | link.onmouseover = () => { | ||
177 | if (link.dataset.valid === 'false') return | ||
178 | |||
179 | const anchors = getMarkersFromMaps(link) | ||
180 | anchors | ||
181 | .filter(isAnchorVisible) | ||
182 | .forEach(anchor => { | ||
183 | const line = addLeaderLine(link, anchor) | ||
184 | link.lines.push(line) | ||
185 | }) | ||
186 | } | ||
187 | link.onmouseout = () => removeLeaderLines(link) | ||
188 | |||
189 | // Click to move camera | ||
190 | link.onclick = (event) => { | ||
191 | event.preventDefault() | ||
192 | if (link.dataset.valid === 'false') return | ||
193 | |||
194 | removeLeaderLines(link) | ||
195 | getMarkersFromMaps(link).forEach(marker => { | ||
196 | const map = marker.closest('.mapclay') | ||
197 | map.scrollIntoView({ behavior: 'smooth' }) | ||
198 | updateMapCameraByMarker([ | ||
199 | Number(link.dataset.lon), | ||
200 | Number(link.dataset.lat), | ||
201 | ])(marker) | ||
202 | }) | ||
203 | } | ||
204 | |||
205 | // Use middle click to remove markers | ||
206 | link.onauxclick = (e) => { | ||
207 | if (e.which !== 2) return | ||
208 | e.preventDefault() | ||
209 | removeLeaderLines(link) | ||
210 | getMarkersFromMaps(link) | ||
211 | .forEach(marker => marker.remove()) | ||
212 | } | ||
213 | return true | ||
214 | } | ||
215 | |||
216 | /** | ||
217 | * CreateDocLink. | ||
218 | * | ||
219 | * @param {HTMLElement} Elements contains anchor elements for doclinks | ||
220 | */ | ||
221 | export const createDocLink = link => { | ||
222 | const label = decodeURIComponent(link.href.split('#')[1]) | ||
223 | const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) | ||
224 | if (!selector) return false | ||
225 | |||
226 | link.classList.add('with-leader-line', 'doclink') | ||
227 | link.lines = [] | ||
228 | |||
229 | link.onmouseover = () => { | ||
230 | const targets = document.querySelectorAll(selector) | ||
231 | |||
232 | targets.forEach(target => { | ||
233 | if (!target?.checkVisibility()) return | ||
234 | |||
235 | // highlight selected target | ||
236 | target.dataset.style = target.style.cssText | ||
237 | const rect = target.getBoundingClientRect() | ||
238 | const isTiny = rect.width < 100 || rect.height < 100 | ||
239 | if (isTiny) { | ||
240 | target.style.background = 'lightPink' | ||
241 | } else { | ||
242 | target.style.outline = 'lightPink 6px dashed' | ||
243 | } | ||
244 | |||
245 | // point to selected target | ||
246 | const line = new LeaderLine({ | ||
247 | start: link, | ||
248 | end: target, | ||
249 | middleLabel: LeaderLine.pathLabel({ | ||
250 | text: label, | ||
251 | fontWeight: 'bold', | ||
252 | }), | ||
253 | hide: true, | ||
254 | path: 'magnet', | ||
255 | }) | ||
256 | link.lines.push(line) | ||
257 | line.show('draw', { duration: 300 }) | ||
258 | }) | ||
259 | } | ||
260 | link.onmouseout = () => { | ||
261 | link.onmouseout = () => removeLeaderLines(link) | ||
262 | |||
263 | // resume targets from highlight | ||
264 | const targets = document.querySelectorAll(selector) | ||
265 | targets.forEach(target => { | ||
266 | target.style.cssText = target.dataset.style | ||
267 | delete target.dataset.style | ||
268 | }) | ||
269 | } | ||
270 | } | ||
271 | |||
272 | /** | ||
273 | * removeLeaderLines. clean lines start from link | ||
274 | * | ||
275 | * @param {HTMLAnchorElement} link | ||
276 | */ | ||
277 | export const removeLeaderLines = link => { | ||
278 | if (!link.lines) return | ||
279 | link.lines.forEach(line => { | ||
280 | line.hide('draw', { duration: 300 }) | ||
281 | setTimeout(() => { | ||
282 | line.remove() | ||
283 | }, 300) | ||
284 | }) | ||
285 | link.lines = [] | ||
286 | } | ||
287 | |||
288 | /** | ||
289 | * updateMapByMarker. get function for updating map camera by marker | ||
290 | * | ||
291 | * @param {Number[]} xy | ||
292 | * @return {Function} function | ||
293 | */ | ||
294 | const updateMapCameraByMarker = lonLat => marker => { | ||
295 | const renderer = marker.closest('.mapclay')?.renderer | ||
296 | renderer.updateCamera({ center: lonLat }, true) | ||
297 | } | ||
298 | |||
299 | /** | ||
300 | * isAnchorVisible. check anchor(marker) is visible for current map camera | ||
301 | * | ||
302 | * @param {Element} anchor | ||
303 | */ | ||
304 | const isAnchorVisible = anchor => { | ||
305 | const mapContainer = anchor.closest('.mapclay') | ||
306 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | ||
307 | } | ||
308 | |||
309 | /** | ||
310 | * addMarkerByPoint. | 90 | * addMarkerByPoint. |
311 | * | 91 | * |
312 | * @param {Number[]} options.point - page XY | 92 | * @param {Number[]} options.point - page XY |
@@ -328,6 +108,30 @@ export const addMarkerByPoint = ({ point, map }) => { | |||
328 | } | 108 | } |
329 | 109 | ||
330 | /** | 110 | /** |
111 | * addGeoSchemeByText. | ||
112 | * | ||
113 | * @param {Node} node | ||
114 | */ | ||
115 | export const addGeoSchemeByText = async (node) => { | ||
116 | const digit = '[\\d\\uFF10-\\uFF19]' | ||
117 | const decimal = '[.\\uFF0E]' | ||
118 | const coordPatterns = `(-?${digit}+${decimal}?${digit}*)([,\x2F\uFF0C])(-?${digit}+${decimal}?${digit}*)` | ||
119 | const re = new RegExp(coordPatterns, 'g') | ||
120 | |||
121 | return replaceTextNodes(node, re, match => { | ||
122 | const [x, y] = [full2Half(match.at(1)), full2Half(match.at(3))] | ||
123 | // Don't process string which can be used as date | ||
124 | if (Date.parse(match.at(0) + ' 1990')) return null | ||
125 | |||
126 | const a = document.createElement('a') | ||
127 | a.className = 'not-geolink from-text' | ||
128 | a.href = `geo:0,0?xy=${x},${y}` | ||
129 | a.textContent = match.at(0) | ||
130 | return a | ||
131 | }) | ||
132 | } | ||
133 | |||
134 | /** | ||
331 | * setGeoSchemeByCRS. | 135 | * setGeoSchemeByCRS. |
332 | * @description Add more information into Anchor Element within Geo Scheme by CRS | 136 | * @description Add more information into Anchor Element within Geo Scheme by CRS |
333 | * @param {String} crs - EPSG/ESRI Code for CRS | 137 | * @param {String} crs - EPSG/ESRI Code for CRS |
@@ -375,17 +179,17 @@ export const setGeoSchemeByCRS = (crs) => (link) => { | |||
375 | } | 179 | } |
376 | 180 | ||
377 | /** | 181 | /** |
378 | * dragForAnchor. | 182 | * addGeoLinkByDrag. |
379 | * | 183 | * |
380 | * @param {HTMLElement} container | 184 | * @param {HTMLElement} container |
381 | * @param {Range} range | 185 | * @param {Range} range |
382 | */ | 186 | */ |
383 | export const dragForAnchor = (container, range, endOfLeaderLine) => { | 187 | export const addGeoLinkByDrag = (container, range, endOfLeaderLine) => { |
384 | // link placeholder when dragging | 188 | // link placeholder when dragging |
385 | container.classList.add('dragging-geolink') | 189 | container.classList.add('dragging-geolink') |
386 | const geoLink = document.createElement('a') | 190 | const link = document.createElement('a') |
387 | geoLink.textContent = range.toString() | 191 | link.textContent = range.toString() |
388 | geoLink.classList.add('with-leader-line', 'geolink', 'drag', 'from-text') | 192 | link.classList.add('with-leader-line', 'geolink', 'drag', 'from-text') |
389 | 193 | ||
390 | // Replace current content with link | 194 | // Replace current content with link |
391 | const originContent = range.cloneContents() | 195 | const originContent = range.cloneContents() |
@@ -394,11 +198,11 @@ export const dragForAnchor = (container, range, endOfLeaderLine) => { | |||
394 | range.insertNode(originContent) | 198 | range.insertNode(originContent) |
395 | } | 199 | } |
396 | range.deleteContents() | 200 | range.deleteContents() |
397 | range.insertNode(geoLink) | 201 | range.insertNode(link) |
398 | 202 | ||
399 | // Add leader-line | 203 | // Add leader-line |
400 | const line = new LeaderLine({ | 204 | const line = new LeaderLine({ |
401 | start: geoLink, | 205 | start: link, |
402 | end: endOfLeaderLine, | 206 | end: endOfLeaderLine, |
403 | path: 'magnet', | 207 | path: 'magnet', |
404 | }) | 208 | }) |
@@ -416,7 +220,7 @@ export const dragForAnchor = (container, range, endOfLeaderLine) => { | |||
416 | container.classList.remove('dragging-geolink') | 220 | container.classList.remove('dragging-geolink') |
417 | container.onmousemove = null | 221 | container.onmousemove = null |
418 | container.onmouseup = null | 222 | container.onmouseup = null |
419 | geoLink.classList.remove('drag') | 223 | link.classList.remove('drag') |
420 | positionObserver.disconnect() | 224 | positionObserver.disconnect() |
421 | line.remove() | 225 | line.remove() |
422 | endOfLeaderLine.remove() | 226 | endOfLeaderLine.remove() |
@@ -434,31 +238,7 @@ export const dragForAnchor = (container, range, endOfLeaderLine) => { | |||
434 | return | 238 | return |
435 | } | 239 | } |
436 | 240 | ||
437 | geoLink.href = `geo:${marker.dataset.xy.split(',').reverse()}` | 241 | link.href = `geo:${marker.dataset.xy.split(',').reverse()}` |
438 | createGeoLink(geoLink) | 242 | GeoLink.replaceWith(link) |
439 | } | 243 | } |
440 | } | 244 | } |
441 | |||
442 | /** | ||
443 | * addGeoSchemeByText. | ||
444 | * | ||
445 | * @param {Node} node | ||
446 | */ | ||
447 | export const addGeoSchemeByText = async (node) => { | ||
448 | const digit = '[\\d\\uFF10-\\uFF19]' | ||
449 | const decimal = '[.\\uFF0E]' | ||
450 | const coordPatterns = `(-?${digit}+${decimal}?${digit}*)([,\x2F\uFF0C])(-?${digit}+${decimal}?${digit}*)` | ||
451 | const re = new RegExp(coordPatterns, 'g') | ||
452 | |||
453 | return replaceTextNodes(node, re, match => { | ||
454 | const [x, y] = [full2Half(match.at(1)), full2Half(match.at(3))] | ||
455 | // Don't process string which can be used as date | ||
456 | if (Date.parse(match.at(0) + ' 1990')) return null | ||
457 | |||
458 | const a = document.createElement('a') | ||
459 | a.className = 'not-geolink from-text' | ||
460 | a.href = `geo:0,0?xy=${x},${y}` | ||
461 | a.textContent = match.at(0) | ||
462 | return a | ||
463 | }) | ||
464 | } | ||