From 1dd8064531f6fffd54b541a58aa624ad2e27dac3 Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Thu, 24 Oct 2024 17:39:34 +0800 Subject: refactor: add module 'Link' for GeoLink and DocLink --- src/Link.mjs | 239 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 src/Link.mjs (limited to 'src/Link.mjs') 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 @@ +import LeaderLine from 'leader-line' +import { insideWindow, insideParent } from './utils' + +/** VAR: pattern for coodinates */ +export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ + +/** + * Class: GeoLink - link for maps + * + * @extends {window.HTMLAnchorElement} + */ +export class GeoLink extends window.HTMLAnchorElement { + static replaceWith = (link) => + link.replaceWith(new GeoLink(link)) + + /** + * Creates a new GeoLink instance + * + * @param {HTMLAnchorElement} link + */ + constructor (link) { + super() + this.innerHTML = link.innerHTML + this.href = link.href + + const url = new URL(link.href) + const params = new URLSearchParams(link.search) + const xyInParams = params.get('xy')?.split(',')?.map(Number) + const [lon, lat] = url.href + ?.match(coordPattern) + ?.slice(1) + ?.reverse() + ?.map(Number) + const xy = xyInParams ?? [lon, lat] + + if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false + + // Geo information in link + this.dataset.lon = lon + this.dataset.lat = lat + this.dataset.crs = params.get('crs') + this.classList.add('with-leader-line', 'geolink') + this.classList.remove('not-geolink') + // TODO refactor as data attribute + this.targets = params.get('id')?.split(',') ?? null + this.title = 'Left-Click to move Camera, Middle-Click to clean anchor' + this.lines = [] + + // Hover link for LeaderLine + this.onmouseover = () => this.getMarkersFromMaps(this) + .filter(isAnchorVisible) + .forEach(anchor => { + const labelText = new URL(this).searchParams.get('text') ?? this.textContent + const line = new LeaderLine({ + start: this, + end: anchor, + hide: true, + middleLabel: labelText, + path: 'magnet', + }) + line.show('draw', { duration: 300 }) + + this.lines.push(line) + }) + + this.onmouseout = () => removeLeaderLines(this) + + // Click to move camera + this.onclick = (event) => { + event.preventDefault() + removeLeaderLines(this) + this.getMarkersFromMaps().forEach(marker => { + const map = marker.closest('.mapclay') + map.scrollIntoView({ behavior: 'smooth' }) + updateMapCameraByMarker([ + Number(this.dataset.lon), + Number(this.dataset.lat), + ])(marker) + }) + } + + // Use middle click to remove markers + this.onauxclick = (e) => { + if (e.which !== 2) return + e.preventDefault() + removeLeaderLines(this) + this.getMarkersFromMaps() + .forEach(marker => marker.remove()) + } + } + + /** + * getMarkersFromMaps. Get marker elements by GeoLink + * + * @param {HTMLAnchorElement} link + * @return {HTMLElement[]} markers + */ + getMarkersFromMaps () { + const params = new URLSearchParams(this.search) + const maps = Array.from( + this.closest('.Dumby') + .querySelectorAll('.mapclay[data-render="fulfilled"]'), + ) + return maps + .filter(map => this.targets ? this.targets.includes(map.id) : true) + .map(map => { + const renderer = map.renderer + const lonLat = [Number(this.dataset.lon), Number(this.dataset.lat)] + + const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? + renderer.addMarker({ + xy: lonLat, + type: params.get('type') ?? null, + }) + marker.dataset.xy = lonLat + marker.title = new URLSearchParams(this.search).get('xy') ?? lonLat + const crs = this.dataset.crs + if (crs && crs !== 'EPSG:4326') { + marker.title += '@' + this.dataset.crs + } + + return marker + }) + } +} +if (!window.customElements.get('dumby-geolink')) { + window.customElements.define('dumby-geolink', GeoLink, { extends: 'a' }) +} + +/** + * Class: DocLink - link for DOM + * + * @extends {window.HTMLAnchorElement} + */ +export class DocLink extends window.HTMLAnchorElement { + static replaceWith = (link) => + link.replaceWith(new DocLink(link)) + + /** + * Creates a new DocLink instance + * + * @param {HTMLAnchorElement} link + */ + constructor (link) { + super() + this.innerHTML = link.innerHTML + this.href = link.href + + const label = decodeURIComponent(link.href.split('#')[1]) + const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) + if (!selector) return false + + this.classList.add('with-leader-line', 'doclink') + this.lines = [] + + this.onmouseover = () => { + const targets = document.querySelectorAll(selector) + + targets.forEach(target => { + if (!target?.checkVisibility()) return + + // highlight selected target + target.dataset.style = target.style.cssText + const rect = target.getBoundingClientRect() + const isTiny = rect.width < 100 || rect.height < 100 + if (isTiny) { + target.style.background = 'lightPink' + } else { + target.style.outline = 'lightPink 6px dashed' + } + + // point to selected target + const line = new LeaderLine({ + start: this, + end: target, + middleLabel: LeaderLine.pathLabel({ + text: label, + fontWeight: 'bold', + }), + hide: true, + path: 'magnet', + }) + this.lines.push(line) + line.show('draw', { duration: 300 }) + }) + } + + this.onmouseout = () => { + removeLeaderLines(this) + + // resume targets from highlight + const targets = document.querySelectorAll(selector) + targets.forEach(target => { + target.style.cssText = target.dataset.style + delete target.dataset.style + }) + } + } +} +if (!window.customElements.get('dumby-doclink')) { + window.customElements.define('dumby-doclink', DocLink, { extends: 'a' }) +} + +/** + * isAnchorVisible. check anchor(marker) is visible for current map camera + * + * @param {Element} anchor + */ +const isAnchorVisible = anchor => { + const mapContainer = anchor.closest('.mapclay') + return insideWindow(anchor) && insideParent(anchor, mapContainer) +} + +/** + * updateMapByMarker. get function for updating map camera by marker + * + * @param {Number[]} xy + * @return {Function} function + */ +const updateMapCameraByMarker = lonLat => marker => { + const renderer = marker.closest('.mapclay')?.renderer + renderer.updateCamera({ center: lonLat }, true) +} + +/** + * removeLeaderLines. clean lines start from link + * + * @param {HTMLAnchorElement} link + */ +export const removeLeaderLines = link => { + if (!link.lines) return + link.lines.forEach(line => { + line.hide('draw', { duration: 300 }) + setTimeout(() => { + line.remove() + }, 300) + }) + link.lines = [] +} -- cgit v1.2.3-70-g09d2