import LeaderLine from 'leader-line' import { replaceTextNodes, full2Half } from './utils' import proj4 from 'proj4' import { coordPattern, GeoLink } from './Link.mjs' /** * focusNextMap. * @param {Boolean} reverse - focus previous map */ export function focusNextMap (reverse = false) { const renderedList = this.utils.renderedMaps() const index = renderedList.findIndex(e => e.classList.contains('focus')) const nextIndex = (index + (reverse ? -1 : 1)) % renderedList.length const nextMap = renderedList.at(nextIndex) nextMap.classList.add('focus', 'focus-manual') nextMap.scrollIntoView({ behavior: 'smooth' }) } /** * focusNextBlock. * * @param {Boolean} reverse - focus previous block */ export function focusNextBlock (reverse = false) { const blocks = this.blocks.filter(b => b.checkVisibility({ contentVisibilityAuto: true, opacityProperty: true, visibilityProperty: true, }), ) const index = blocks.findIndex(e => e.classList.contains('focus')) const nextIndex = (index + (reverse ? -1 : 1)) % blocks.length blocks.forEach(b => b.classList.remove('focus')) const nextBlock = blocks.at(nextIndex) nextBlock?.classList?.add('focus') scrollToBlock(nextBlock) } /** * scrollToBlock. Smoothly scroll to target block. * If block is bigger than viewport, then pick strategy wisely. * * @param {HTMLElement} block - Scroll to this element */ export const scrollToBlock = block => { const parentRect = block.parentElement.getBoundingClientRect() const scrollBlock = block.getBoundingClientRect().height > parentRect.height * 0.8 ? 'nearest' : 'center' block.scrollIntoView({ behavior: 'smooth', block: scrollBlock }) } /** * focusDelay. Delay of throttle, value changes by cases */ export function focusDelay () { return window.window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300 } /** * switchToNextLayout. * * @param {Boolean} reverse - Switch to previous one */ export function switchToNextLayout (reverse = false) { const layouts = this.layouts const currentLayoutName = this.container.dataset.layout const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName) const padding = reverse ? -1 : 1 const nextIndex = currentIndex === -1 ? 0 : (currentIndex + padding + layouts.length) % layouts.length const nextLayout = layouts[nextIndex] this.container.dataset.layout = nextLayout.name } /** * removeBlockFocus. */ export function removeBlockFocus () { this.blocks.forEach(b => b.classList.remove('focus')) } /** * addMarkerByPoint. * * @param {Number[]} options.point - page XY * @param {HTMLElement} options.map */ export const addMarkerByPoint = ({ point, map, title }) => { const rect = map.getBoundingClientRect() const [lon, lat] = map.renderer .unproject([point[0] - rect.left, point[1] - rect.top]) .map(value => parseFloat(value.toFixed(6))) const marker = map.renderer.addMarker({ xy: [lon, lat], }) marker.dataset.xy = `${lon},${lat}` if (title) marker.title = title return marker } /** * addGeoSchemeByText. * * @param {Node} node */ export const addGeoSchemeByText = async (node) => { const digit = '[\\d\\uFF10-\\uFF19]' const decimal = '[.\\uFF0E]' const coordPattern = `(-?${digit}+${decimal}?${digit}*)` const DMSPattern = `([NEWS]${digit}+[dD°度]? ${digit}+[mM'分]? ${digit}+${decimal}?${digit}*[sS"秒]?)` const re = new RegExp(`${coordPattern}[,\x2F\uFF0C]${coordPattern}|${DMSPattern}\\s${DMSPattern}`, 'g') return replaceTextNodes(node, re, match => { const patterns = match.filter(p => p) const [match1, match2] = [full2Half(patterns.at(1)), full2Half(patterns.at(2))] let [x, y] = [undefined, undefined] // Get x,y by DMS or coordinates pattern if (match1.match('^[NEWS]')) { const dms2degreeString = (pchar, nchar) => (dms) => { const matches = dms.match(new RegExp(`([${pchar}${nchar}])(\\d+) (\\d+) (.*)$`)) if (!matches) return null return (matches[1] === pchar ? '' : '-') + (Number(matches[2]) + Number(matches[3]) / 60 + Number(matches[4]) / 3600) } x = [match1, match2].map(dms2degreeString('E', 'W')).find(x => x) y = [match1, match2].map(dms2degreeString('N', 'S')).find(y => y) } else { [x, y] = [match1, match2] } // Don't process string which can be used as date if (Date.parse(match.at(0) + ' 1990')) return null if (!x || !y) return null // Return anchor element with Geo Scheme const a = document.createElement('a') a.className = 'not-geolink from-text' a.href = `geo:0,0?xy=${x},${y}` a.textContent = match.at(0) return a }) } /** * @description Add more information into Anchor Element within Geo Scheme by CRS * @param {String} crs - EPSG/ESRI Code for CRS * @return {Function} - Function for link */ export const updateGeoSchemeByCRS = (crs) => (link) => { const transform = proj4(crs, 'EPSG:4326') const params = new URLSearchParams(link.search) let xy = params.get('xy')?.split(',')?.map(Number) // Set coords for Geo Scheme if (link.href.startsWith('geo:0,0')) { if (!xy) return null const [lon, lat] = transform.forward(xy) .map(value => parseFloat(value.toFixed(6))) link.href = `geo:${lat},${lon}` } const [lat, lon] = link.href .match(coordPattern) .slice(1) .map(Number) if (!xy) { xy = transform.inverse([lon, lat]) params.set('xy', xy) } // set query strings params.set('crs', crs) params.set('q', `${lat},${lon}`) link.search = params const unit = proj4(crs).oProj.units const invalidDegree = unit === 'degrees' && (lon > 180 || lon < -180 || lat > 90 || lat < -90) const invalidMeter = unit === 'm' && xy.find(v => v < 100) if (invalidDegree || invalidMeter) { link.replaceWith(document.createTextNode(link.textContent)) return null } return link } /** * addGeoLinkByDrag. * * @param {HTMLElement} container * @param {Range} range */ export const addGeoLinkByDrag = (container, range, endOfLeaderLine) => { // link placeholder when dragging container.classList.add('dragging-geolink') const link = document.createElement('a') link.textContent = range.toString() link.classList.add('with-leader-line', 'geolink', 'drag', 'from-text') // Replace current content with link const originContent = range.cloneContents() const resumeContent = () => { range.deleteContents() range.insertNode(originContent) } range.deleteContents() range.insertNode(link) // Add leader-line const line = new LeaderLine({ start: link, end: endOfLeaderLine, path: 'magnet', }) const positionObserver = new window.MutationObserver(() => { line.position() }) positionObserver.observe(endOfLeaderLine, { attributes: true, attributeFilter: ['style'], }) // Handler for dragend container.onmouseup = (e) => { container.classList.remove('dragging-geolink') container.onmousemove = null container.onmouseup = null link.classList.remove('drag') positionObserver.disconnect() line.remove() endOfLeaderLine.remove() const map = document.elementFromPoint(e.clientX, e.clientY) .closest('.mapclay[data-render="fulfilled"]') if (!map) { resumeContent() return } const marker = addMarkerByPoint({ point: [e.clientX, e.clientY], map }) if (!marker) { resumeContent() return } link.href = `geo:${marker.dataset.xy.split(',').reverse()}` GeoLink(link) } }