aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/dumbyUtils.mjs
diff options
context:
space:
mode:
authorHsieh Chin Fan <pham@topo.tw>2024-10-24 17:39:34 +0800
committerHsieh Chin Fan <pham@topo.tw>2024-10-24 17:42:31 +0800
commit1dd8064531f6fffd54b541a58aa624ad2e27dac3 (patch)
tree8ebee7d446164abefe1ac5ff9742b916728f78ad /src/dumbyUtils.mjs
parent21ae439bfab7cf22338fcb80c7bf482d1a3c763c (diff)
refactor: add module 'Link' for GeoLink and DocLink
Diffstat (limited to 'src/dumbyUtils.mjs')
-rw-r--r--src/dumbyUtils.mjs292
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 @@
1import LeaderLine from 'leader-line' 1import LeaderLine from 'leader-line'
2import { insideWindow, insideParent, replaceTextNodes, full2Half } from './utils' 2import { replaceTextNodes, full2Half } from './utils'
3import proj4 from 'proj4' 3import proj4 from 'proj4'
4 4import { coordPattern, GeoLink } from './Link.mjs'
5export 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 */
96export 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 */
130const 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 */
150export 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 */
221export 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 */
277export 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 */
294const 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 */
304const 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 */
115export 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 */
383export const dragForAnchor = (container, range, endOfLeaderLine) => { 187export 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 */
447export 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}