diff options
| -rw-r--r-- | src/dumbyUtils.mjs | 50 | ||||
| -rw-r--r-- | src/dumbymap.mjs | 78 | ||||
| -rw-r--r-- | src/editor.mjs | 2 | ||||
| -rw-r--r-- | src/utils.mjs | 44 |
4 files changed, 121 insertions, 53 deletions
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 @@ | |||
| 1 | import LeaderLine from 'leader-line' | 1 | import LeaderLine from 'leader-line' |
| 2 | import { insideWindow, insideParent } from './utils' | 2 | import { insideWindow, insideParent } from './utils' |
| 3 | import proj4 from 'proj4' | ||
| 3 | 4 | ||
| 4 | export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ | 5 | export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ |
| 5 | 6 | ||
| @@ -333,3 +334,52 @@ export const addAnchorByPoint = ({ | |||
| 333 | 334 | ||
| 334 | return { ref: anchorName, link, title: desc } | 335 | return { ref: anchorName, link, title: desc } |
| 335 | } | 336 | } |
| 337 | |||
| 338 | /** | ||
| 339 | * setGeoSchemeByCRS. | ||
| 340 | * @description Add more information into Anchor Element within Geo Scheme by CRS | ||
| 341 | * @param {String} crs - EPSG/ESRI Code for CRS | ||
| 342 | * @return {Function} - Function for link | ||
| 343 | */ | ||
| 344 | export const setGeoSchemeByCRS = (crs) => (link) => { | ||
| 345 | const transform = proj4(crs, 'EPSG:4326') | ||
| 346 | const params = new URLSearchParams(link.search) | ||
| 347 | let xy = params.get('xy')?.split(',')?.map(Number) | ||
| 348 | |||
| 349 | // Set coords for Geo Scheme | ||
| 350 | if (link.href.startsWith('geo:0,0')) { | ||
| 351 | if (!xy) return null | ||
| 352 | |||
| 353 | const [lon, lat] = transform.forward(xy) | ||
| 354 | .map(value => parseFloat(value.toFixed(6))) | ||
| 355 | link.href = `geo:${lat},${lon}` | ||
| 356 | } | ||
| 357 | |||
| 358 | const [lat, lon] = link.href | ||
| 359 | .match(coordPattern) | ||
| 360 | .slice(1) | ||
| 361 | .map(Number) | ||
| 362 | |||
| 363 | if (!xy) { | ||
| 364 | xy = transform.inverse([lon, lat]) | ||
| 365 | params.set('xy', xy) | ||
| 366 | } | ||
| 367 | |||
| 368 | // set query strings | ||
| 369 | params.set('crs', crs) | ||
| 370 | params.set('q', `${lat},${lon}`) | ||
| 371 | link.search = params | ||
| 372 | |||
| 373 | const unit = proj4(crs).oProj.units | ||
| 374 | const invalidDegree = unit === 'degrees' && ( | ||
| 375 | (lon > 180 || lon < -180 || lat > 90 || lat < -90) || | ||
| 376 | (xy.every(v => v.toString().length < 3)) | ||
| 377 | ) | ||
| 378 | const invalidMeter = unit === 'm' && xy.find(v => v < 100) | ||
| 379 | if (invalidDegree || invalidMeter) { | ||
| 380 | link.replaceWith(document.createTextNode(link.textContent)) | ||
| 381 | return null | ||
| 382 | } | ||
| 383 | |||
| 384 | return link | ||
| 385 | } | ||
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' | |||
| 4 | import MarkdownItFrontMatter from 'markdown-it-front-matter' | 4 | import MarkdownItFrontMatter from 'markdown-it-front-matter' |
| 5 | import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' | 5 | import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' |
| 6 | import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay' | 6 | import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay' |
| 7 | import { onRemove, animateRectTransition, throttle, shiftByWindow } from './utils' | 7 | import { onRemove, animateRectTransition, throttle, shiftByWindow, replaceTextNodes } from './utils' |
| 8 | import { Layout, SideBySide, Overlay } from './Layout' | 8 | import { Layout, SideBySide, Overlay } from './Layout' |
| 9 | import * as utils from './dumbyUtils' | 9 | import * as utils from './dumbyUtils' |
| 10 | import * as menuItem from './MenuItem' | 10 | import * as menuItem from './MenuItem' |
| @@ -56,27 +56,6 @@ export const markdown2HTML = (container, mdContent) => { | |||
| 56 | .use(MarkdownItFrontMatter) | 56 | .use(MarkdownItFrontMatter) |
| 57 | .use(MarkdownItInjectLinenumbers) | 57 | .use(MarkdownItInjectLinenumbers) |
| 58 | 58 | ||
| 59 | /** Set up linkify for GeoLinks */ | ||
| 60 | const coordinateRegex = /^(\D*)(-?\d+\.?\d*)\s*([,\x2F\uFF0C])\s*(-?\d+\.?\d*)/ | ||
| 61 | const coordinateValue = { | ||
| 62 | validate: coordinateRegex, | ||
| 63 | normalize: function (match) { | ||
| 64 | const [, , x, sep, y] = match.text.match(coordinateRegex) | ||
| 65 | match.url = `geo:${y},${x}` | ||
| 66 | match.text = `${x}${sep}${y}` | ||
| 67 | match.index += match.text.indexOf(x) + 1 | ||
| 68 | return match | ||
| 69 | }, | ||
| 70 | } | ||
| 71 | const patterns = ['[', '(', '📍', '\uFF08', '@', 'twd'] | ||
| 72 | patterns.forEach(prefix => | ||
| 73 | md.linkify.add(prefix, coordinateValue), | ||
| 74 | ) | ||
| 75 | md.linkify.add('geo:', { | ||
| 76 | validate: /(-?\d+\.?\d*),(-?\d+\.?\d*)/, | ||
| 77 | normalize: match => match, | ||
| 78 | }) | ||
| 79 | |||
| 80 | /** Custom rule for Blocks in DumbyMap */ | 59 | /** Custom rule for Blocks in DumbyMap */ |
| 81 | // FIXME A better way to generate blocks | 60 | // FIXME A better way to generate blocks |
| 82 | md.renderer.rules.dumby_block_open = () => '<div>' | 61 | md.renderer.rules.dumby_block_open = () => '<div>' |
| @@ -130,7 +109,7 @@ export const generateMaps = (container, { | |||
| 130 | 109 | ||
| 131 | const htmlHolder = container.querySelector('.SemanticHtml') ?? container | 110 | const htmlHolder = container.querySelector('.SemanticHtml') ?? container |
| 132 | const blocks = addBlocks(htmlHolder) | 111 | const blocks = addBlocks(htmlHolder) |
| 133 | blocks.forEach(b => b.dataset.total = blocks.length) | 112 | blocks.forEach(b => { b.dataset.total = blocks.length }) |
| 134 | 113 | ||
| 135 | const showcase = document.createElement('div') | 114 | const showcase = document.createElement('div') |
| 136 | container.appendChild(showcase) | 115 | container.appendChild(showcase) |
| @@ -189,37 +168,32 @@ export const generateMaps = (container, { | |||
| 189 | }) | 168 | }) |
| 190 | 169 | ||
| 191 | /** LINK: Set CRS and GeoLinks */ | 170 | /** LINK: Set CRS and GeoLinks */ |
| 192 | register(proj4) | 171 | const setCRS = new Promise(resolve => { |
| 193 | fromEPSGCode(crs).then(projection => { | 172 | register(proj4) |
| 194 | const transform = proj4(crs, 'EPSG:4326').forward | 173 | fromEPSGCode(crs).then(() => resolve()) |
| 174 | }) | ||
| 175 | const addGeoSchemeByText = new Promise(resolve => { | ||
| 176 | const coordPatterns = [ | ||
| 177 | /[\x28\x5B\uFF08]\D*(-?\d+\.?\d*)([\x2F\s])(-?\d+\.?\d*)\D*[\x29\x5D\uFF09]/, | ||
| 178 | /(-?\d+\.?\d*)([,\uFF0C]\s?)(-?\d+\.?\d*)/, | ||
| 179 | ] | ||
| 180 | const re = new RegExp(coordPatterns.map(p => p.source).join('|'), 'g') | ||
| 181 | htmlHolder.querySelectorAll('p') | ||
| 182 | .forEach(p => { | ||
| 183 | replaceTextNodes(p, re, match => { | ||
| 184 | const a = document.createElement('a') | ||
| 185 | a.href = `geo:0,0?xy=${match.at(1)},${match.at(3)}` | ||
| 186 | a.textContent = match.at(0) | ||
| 187 | return a | ||
| 188 | }) | ||
| 189 | }) | ||
| 190 | resolve() | ||
| 191 | }) | ||
| 195 | 192 | ||
| 193 | Promise.all([setCRS, addGeoSchemeByText]).then(() => { | ||
| 196 | Array.from(container.querySelectorAll(geoLinkSelector)) | 194 | Array.from(container.querySelectorAll(geoLinkSelector)) |
| 197 | .map(link => { | 195 | .map(utils.setGeoSchemeByCRS(crs)) |
| 198 | // set coordinate as lat/lon in WGS84 | 196 | .filter(link => link instanceof window.HTMLAnchorElement) |
| 199 | const params = new URLSearchParams(link.search) | ||
| 200 | const [y, x] = link.href | ||
| 201 | .match(utils.coordPattern) | ||
| 202 | .splice(1) | ||
| 203 | .map(Number) | ||
| 204 | const [lon, lat] = transform([x, y]) | ||
| 205 | .map(value => parseFloat(value.toFixed(6))) | ||
| 206 | link.href = `geo:${lat},${lon}` | ||
| 207 | |||
| 208 | // set query strings | ||
| 209 | params.set('xy', `${x},${y}`) | ||
| 210 | params.set('crs', crs) | ||
| 211 | params.set('q', `${lat},${lon}`) | ||
| 212 | link.search = params | ||
| 213 | |||
| 214 | if (projection.getUnits() === 'degrees' && | ||
| 215 | (lon > 180 || lon < -180 || lat > 90 || lat < -90) | ||
| 216 | ) { | ||
| 217 | link.dataset.valid = false | ||
| 218 | link.title = `Invalid Coordinate, maybe try another crs other than ${crs}` | ||
| 219 | } | ||
| 220 | |||
| 221 | return link | ||
| 222 | }) | ||
| 223 | .forEach(utils.createGeoLink) | 197 | .forEach(utils.createGeoLink) |
| 224 | }) | 198 | }) |
| 225 | 199 | ||
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 @@ | |||
| 2 | import { markdown2HTML, generateMaps } from './dumbymap' | 2 | import { markdown2HTML, generateMaps } from './dumbymap' |
| 3 | import { defaultAliases, parseConfigsFromYaml } from 'mapclay' | 3 | import { defaultAliases, parseConfigsFromYaml } from 'mapclay' |
| 4 | import * as menuItem from './MenuItem' | 4 | import * as menuItem from './MenuItem' |
| 5 | import { addAnchorByPoint, createGeoLink } from './dumbyUtils.mjs' | 5 | import { addAnchorByPoint } from './dumbyUtils.mjs' |
| 6 | import { shiftByWindow } from './utils.mjs' | 6 | import { shiftByWindow } from './utils.mjs' |
| 7 | import * as tutorial from './tutorial' | 7 | import * as tutorial from './tutorial' |
| 8 | 8 | ||
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) => { | |||
| 133 | childRect.bottom > parentRect.top + offset | 133 | childRect.bottom > parentRect.top + offset |
| 134 | ) | 134 | ) |
| 135 | } | 135 | } |
| 136 | |||
| 137 | /** | ||
| 138 | * replaceTextNodes. | ||
| 139 | * @description Search current nodes by pattern, and replace them by new node | ||
| 140 | * @todo refactor to smaller methods | ||
| 141 | * @param {HTMLElement} element | ||
| 142 | * @param {RegExp} pattern | ||
| 143 | * @param {Function} newNode - Create new node by each result of String.prototype.matchAll | ||
| 144 | */ | ||
| 145 | export const replaceTextNodes = ( | ||
| 146 | element, | ||
| 147 | pattern, | ||
| 148 | newNode = (match) => { | ||
| 149 | const link = document.createElement('a') | ||
| 150 | link.textContent(match.at(0)) | ||
| 151 | return link | ||
| 152 | }, | ||
| 153 | ) => { | ||
| 154 | const nodeIterator = document.createNodeIterator( | ||
| 155 | element, | ||
| 156 | window.NodeFilter.SHOW_TEXT, | ||
| 157 | node => node.textContent.match(pattern) | ||
| 158 | ? window.NodeFilter.FILTER_ACCEPT | ||
| 159 | : window.NodeFilter.FILTER_REJECT, | ||
| 160 | ) | ||
| 161 | |||
| 162 | let node = nodeIterator.nextNode() | ||
| 163 | while (node) { | ||
| 164 | let index = 0 | ||
| 165 | for (const match of node.textContent.matchAll(pattern)) { | ||
| 166 | const text = node.textContent.slice(index, match.index) | ||
| 167 | index = match.index + match.at(0).length | ||
| 168 | node.parentElement.insertBefore(document.createTextNode(text), node) | ||
| 169 | node.parentElement.insertBefore(newNode(match), node) | ||
| 170 | } | ||
| 171 | if (index < node.textContent.length) { | ||
| 172 | const text = node.textContent.slice(index) | ||
| 173 | node.parentElement.insertBefore(document.createTextNode(text), node) | ||
| 174 | } | ||
| 175 | |||
| 176 | node.parentElement.removeChild(node) | ||
| 177 | node = nodeIterator.nextNode() | ||
| 178 | } | ||
| 179 | } | ||