import LeaderLine from 'leader-line'
import { insideWindow, insideParent } from './utils'
import * as markers from './marker.mjs'
/**
* @typedef {Object} GeoLink - anchor element with geo scheme and properties about maps
* @extends HTMLAnchorElement
* @property {string[]} targets - ids of target map elements
* @property {LeaderLine[]} lines
* @property {Object} dataset
* @property {string} dataset.lon - longitude string of geo scheme
* @property {string} dataset.lat - latitude string of geo scheme
* @property {string} dataset.crs - short name of CRS in EPSG/ESRI format
*/
/**
* DocLink: anchor element which points to DOM node by filter
* @typedef {Object} DocLink
* @extends HTMLAnchorElement
* @property {LeaderLine[]} lines
*/
/** VAR: pattern for coodinates */
export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/
/**
* GeoLink: append GeoLink features onto anchor element
* @param {HTMLAnchorElement} link
* @return {GeoLink}
*/
export const GeoLink = (link) => {
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
link.dataset.lon = lon
link.dataset.lat = lat
link.dataset.crs = params.get('crs')
link.classList.add('with-leader-line', 'geolink')
link.classList.remove('not-geolink')
// TODO refactor as data attribute
link.title = 'Left-Click:\t\tmove camera\nMiddle-Click:\tremove markers\nRight-Click:\t\topen menu'
link.targets = params.get('id')?.split(',') ?? null
link.lines = []
// Hover link for LeaderLine
link.onmouseover = () => getMarkersFromMaps(link)
.filter(isAnchorVisible)
.forEach(anchor => {
const labelText = new URL(link).searchParams.get('text') ?? link.textContent
const line = new LeaderLine({
start: link,
end: anchor,
hide: true,
middleLabel: LeaderLine.pathLabel({
text: labelText,
fontWeight: 'bold',
}),
path: link.dataset.linePath ?? 'magnet',
})
line.show('draw', { duration: 300 })
link.lines.push(line)
})
link.onmouseout = () => removeLeaderLines(link)
// Click to move camera
link.onclick = (event) => {
event.preventDefault()
removeLeaderLines(link)
getMarkersFromMaps(link).forEach(marker => {
const map = marker.closest('.mapclay')
map.scrollIntoView({ behavior: 'smooth' })
updateMapCameraByMarker([
Number(link.dataset.lon),
Number(link.dataset.lat),
])(marker)
})
}
// Use middle click to remove markers
link.onauxclick = (e) => {
if (e.which !== 2) return
e.preventDefault()
removeLeaderLines(link)
getMarkersFromMaps(link)
.forEach(marker => marker.remove())
}
return link
}
/**
* GeoLink: getMarkersFromMaps. Get marker elements by GeoLink
*
* @param {GeoLink} link
* @return {HTMLElement[]} markers
*/
export const getMarkersFromMaps = (link) => {
const params = new URLSearchParams(link.search)
const maps = Array.from(
link.closest('.Dumby')
.querySelectorAll('.mapclay[data-render="fulfilled"]'),
)
return maps
.filter(map => link.targets ? link.targets.includes(map.id) : true)
.map(map => {
const renderer = map.renderer
const lonLat = [Number(link.dataset.lon), Number(link.dataset.lat)]
const type = params.get('type') ?? 'pin'
const svg = markers[type]
const element = document.createElement('div')
element.style.cssText = `width: ${svg.size[0]}px; height: ${svg.size[1]}px;`
element.innerHTML = svg.html
const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ??
renderer.addMarker({
xy: lonLat,
element,
type,
anchor: svg.anchor,
size: svg.size,
})
marker.dataset.xy = lonLat
marker.title = link.textContent
return marker
})
}
/**
* DocLink: append DocLink features onto anchor element
* @param {HTMLAnchorElement} link
* @return {DocLink}
*/
export const DocLink = (link) => {
const label = decodeURIComponent(link.href.split('#')[1])
const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null)
if (!selector) return false
link.classList.add('with-leader-line', 'doclink')
link.lines = []
link.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: link,
end: target,
middleLabel: LeaderLine.pathLabel({
text: label,
fontWeight: 'bold',
}),
hide: true,
path: link.dataset.linePath ?? 'magnet',
})
link.lines.push(line)
line.show('draw', { duration: 300 })
})
}
link.onmouseout = () => {
removeLeaderLines(link)
// resume targets from highlight
const targets = document.querySelectorAll(selector)
targets.forEach(target => {
target.style.cssText = target.dataset.style
delete target.dataset.style
})
}
return link
}
/**
* 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 {GeoLink} link
*/
export const removeLeaderLines = link => {
if (!link.lines) return
link.lines.forEach(line => {
line.hide('draw', { duration: 300 })
setTimeout(() => {
line.remove()
}, 300)
})
link.lines = []
}