From 5d4a43abb181ae4fd63d2365210ea4949f10012e Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Mon, 14 Oct 2024 14:47:04 +0800 Subject: feat: improve GeoLink generation * Stop searching pattern by linkify, Add links with geo URI scheme by async function instead * Search text nodes with patterns, and replace them with links * Add checker for valid degree/meter coordinated * Add query string "xy" anyway --- src/dumbyUtils.mjs | 50 ++++++++++++++++++++++++++++++++++ src/dumbymap.mjs | 78 ++++++++++++++++++------------------------------------ src/editor.mjs | 2 +- src/utils.mjs | 44 ++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 53 deletions(-) (limited to 'src') diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index 8bd4423..7b06840 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs @@ -1,5 +1,6 @@ import LeaderLine from 'leader-line' import { insideWindow, insideParent } from './utils' +import proj4 from 'proj4' export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ @@ -333,3 +334,52 @@ export const addAnchorByPoint = ({ return { ref: anchorName, link, title: desc } } + +/** + * setGeoSchemeByCRS. + * @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 setGeoSchemeByCRS = (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) || + (xy.every(v => v.toString().length < 3)) + ) + const invalidMeter = unit === 'm' && xy.find(v => v < 100) + if (invalidDegree || invalidMeter) { + link.replaceWith(document.createTextNode(link.textContent)) + return null + } + + return link +} diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 81a65ca..4329bbf 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs @@ -4,7 +4,7 @@ import MarkdownItFootnote from 'markdown-it-footnote' import MarkdownItFrontMatter from 'markdown-it-front-matter' import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay' -import { onRemove, animateRectTransition, throttle, shiftByWindow } from './utils' +import { onRemove, animateRectTransition, throttle, shiftByWindow, replaceTextNodes } from './utils' import { Layout, SideBySide, Overlay } from './Layout' import * as utils from './dumbyUtils' import * as menuItem from './MenuItem' @@ -56,27 +56,6 @@ export const markdown2HTML = (container, mdContent) => { .use(MarkdownItFrontMatter) .use(MarkdownItInjectLinenumbers) - /** Set up linkify for GeoLinks */ - const coordinateRegex = /^(\D*)(-?\d+\.?\d*)\s*([,\x2F\uFF0C])\s*(-?\d+\.?\d*)/ - const coordinateValue = { - validate: coordinateRegex, - normalize: function (match) { - const [, , x, sep, y] = match.text.match(coordinateRegex) - match.url = `geo:${y},${x}` - match.text = `${x}${sep}${y}` - match.index += match.text.indexOf(x) + 1 - return match - }, - } - const patterns = ['[', '(', '📍', '\uFF08', '@', 'twd'] - patterns.forEach(prefix => - md.linkify.add(prefix, coordinateValue), - ) - md.linkify.add('geo:', { - validate: /(-?\d+\.?\d*),(-?\d+\.?\d*)/, - normalize: match => match, - }) - /** Custom rule for Blocks in DumbyMap */ // FIXME A better way to generate blocks md.renderer.rules.dumby_block_open = () => '
' @@ -130,7 +109,7 @@ export const generateMaps = (container, { const htmlHolder = container.querySelector('.SemanticHtml') ?? container const blocks = addBlocks(htmlHolder) - blocks.forEach(b => b.dataset.total = blocks.length) + blocks.forEach(b => { b.dataset.total = blocks.length }) const showcase = document.createElement('div') container.appendChild(showcase) @@ -189,37 +168,32 @@ export const generateMaps = (container, { }) /** LINK: Set CRS and GeoLinks */ - register(proj4) - fromEPSGCode(crs).then(projection => { - const transform = proj4(crs, 'EPSG:4326').forward + const setCRS = new Promise(resolve => { + register(proj4) + fromEPSGCode(crs).then(() => resolve()) + }) + const addGeoSchemeByText = new Promise(resolve => { + const coordPatterns = [ + /[\x28\x5B\uFF08]\D*(-?\d+\.?\d*)([\x2F\s])(-?\d+\.?\d*)\D*[\x29\x5D\uFF09]/, + /(-?\d+\.?\d*)([,\uFF0C]\s?)(-?\d+\.?\d*)/, + ] + const re = new RegExp(coordPatterns.map(p => p.source).join('|'), 'g') + htmlHolder.querySelectorAll('p') + .forEach(p => { + replaceTextNodes(p, re, match => { + const a = document.createElement('a') + a.href = `geo:0,0?xy=${match.at(1)},${match.at(3)}` + a.textContent = match.at(0) + return a + }) + }) + resolve() + }) + Promise.all([setCRS, addGeoSchemeByText]).then(() => { Array.from(container.querySelectorAll(geoLinkSelector)) - .map(link => { - // set coordinate as lat/lon in WGS84 - const params = new URLSearchParams(link.search) - const [y, x] = link.href - .match(utils.coordPattern) - .splice(1) - .map(Number) - const [lon, lat] = transform([x, y]) - .map(value => parseFloat(value.toFixed(6))) - link.href = `geo:${lat},${lon}` - - // set query strings - params.set('xy', `${x},${y}`) - params.set('crs', crs) - params.set('q', `${lat},${lon}`) - link.search = params - - if (projection.getUnits() === 'degrees' && - (lon > 180 || lon < -180 || lat > 90 || lat < -90) - ) { - link.dataset.valid = false - link.title = `Invalid Coordinate, maybe try another crs other than ${crs}` - } - - return link - }) + .map(utils.setGeoSchemeByCRS(crs)) + .filter(link => link instanceof window.HTMLAnchorElement) .forEach(utils.createGeoLink) }) diff --git a/src/editor.mjs b/src/editor.mjs index df6e3bf..d13be58 100644 --- a/src/editor.mjs +++ b/src/editor.mjs @@ -2,7 +2,7 @@ import { markdown2HTML, generateMaps } from './dumbymap' import { defaultAliases, parseConfigsFromYaml } from 'mapclay' import * as menuItem from './MenuItem' -import { addAnchorByPoint, createGeoLink } from './dumbyUtils.mjs' +import { addAnchorByPoint } from './dumbyUtils.mjs' import { shiftByWindow } from './utils.mjs' import * as tutorial from './tutorial' diff --git a/src/utils.mjs b/src/utils.mjs index d408b3d..1fe3ce5 100644 --- a/src/utils.mjs +++ b/src/utils.mjs @@ -133,3 +133,47 @@ export const insideParent = (childElement, parentElement) => { childRect.bottom > parentRect.top + offset ) } + +/** + * replaceTextNodes. + * @description Search current nodes by pattern, and replace them by new node + * @todo refactor to smaller methods + * @param {HTMLElement} element + * @param {RegExp} pattern + * @param {Function} newNode - Create new node by each result of String.prototype.matchAll + */ +export const replaceTextNodes = ( + element, + pattern, + newNode = (match) => { + const link = document.createElement('a') + link.textContent(match.at(0)) + return link + }, +) => { + const nodeIterator = document.createNodeIterator( + element, + window.NodeFilter.SHOW_TEXT, + node => node.textContent.match(pattern) + ? window.NodeFilter.FILTER_ACCEPT + : window.NodeFilter.FILTER_REJECT, + ) + + let node = nodeIterator.nextNode() + while (node) { + let index = 0 + for (const match of node.textContent.matchAll(pattern)) { + const text = node.textContent.slice(index, match.index) + index = match.index + match.at(0).length + node.parentElement.insertBefore(document.createTextNode(text), node) + node.parentElement.insertBefore(newNode(match), node) + } + if (index < node.textContent.length) { + const text = node.textContent.slice(index) + node.parentElement.insertBefore(document.createTextNode(text), node) + } + + node.parentElement.removeChild(node) + node = nodeIterator.nextNode() + } +} -- cgit v1.2.3-70-g09d2