diff options
| author | Hsieh Chin Fan <pham@topo.tw> | 2024-10-24 17:39:34 +0800 |
|---|---|---|
| committer | Hsieh Chin Fan <pham@topo.tw> | 2024-10-24 17:42:31 +0800 |
| commit | 1dd8064531f6fffd54b541a58aa624ad2e27dac3 (patch) | |
| tree | 8ebee7d446164abefe1ac5ff9742b916728f78ad /src | |
| parent | 21ae439bfab7cf22338fcb80c7bf482d1a3c763c (diff) | |
refactor: add module 'Link' for GeoLink and DocLink
Diffstat (limited to 'src')
| -rw-r--r-- | src/Link.mjs | 239 | ||||
| -rw-r--r-- | src/MenuItem.mjs | 11 | ||||
| -rw-r--r-- | src/dumbyUtils.mjs | 292 | ||||
| -rw-r--r-- | src/dumbymap.mjs | 13 |
4 files changed, 288 insertions, 267 deletions
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 @@ | |||
| 1 | import LeaderLine from 'leader-line' | ||
| 2 | import { insideWindow, insideParent } from './utils' | ||
| 3 | |||
| 4 | /** VAR: pattern for coodinates */ | ||
| 5 | export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Class: GeoLink - link for maps | ||
| 9 | * | ||
| 10 | * @extends {window.HTMLAnchorElement} | ||
| 11 | */ | ||
| 12 | export class GeoLink extends window.HTMLAnchorElement { | ||
| 13 | static replaceWith = (link) => | ||
| 14 | link.replaceWith(new GeoLink(link)) | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Creates a new GeoLink instance | ||
| 18 | * | ||
| 19 | * @param {HTMLAnchorElement} link | ||
| 20 | */ | ||
| 21 | constructor (link) { | ||
| 22 | super() | ||
| 23 | this.innerHTML = link.innerHTML | ||
| 24 | this.href = link.href | ||
| 25 | |||
| 26 | const url = new URL(link.href) | ||
| 27 | const params = new URLSearchParams(link.search) | ||
| 28 | const xyInParams = params.get('xy')?.split(',')?.map(Number) | ||
| 29 | const [lon, lat] = url.href | ||
| 30 | ?.match(coordPattern) | ||
| 31 | ?.slice(1) | ||
| 32 | ?.reverse() | ||
| 33 | ?.map(Number) | ||
| 34 | const xy = xyInParams ?? [lon, lat] | ||
| 35 | |||
| 36 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false | ||
| 37 | |||
| 38 | // Geo information in link | ||
| 39 | this.dataset.lon = lon | ||
| 40 | this.dataset.lat = lat | ||
| 41 | this.dataset.crs = params.get('crs') | ||
| 42 | this.classList.add('with-leader-line', 'geolink') | ||
| 43 | this.classList.remove('not-geolink') | ||
| 44 | // TODO refactor as data attribute | ||
| 45 | this.targets = params.get('id')?.split(',') ?? null | ||
| 46 | this.title = 'Left-Click to move Camera, Middle-Click to clean anchor' | ||
| 47 | this.lines = [] | ||
| 48 | |||
| 49 | // Hover link for LeaderLine | ||
| 50 | this.onmouseover = () => this.getMarkersFromMaps(this) | ||
| 51 | .filter(isAnchorVisible) | ||
| 52 | .forEach(anchor => { | ||
| 53 | const labelText = new URL(this).searchParams.get('text') ?? this.textContent | ||
| 54 | const line = new LeaderLine({ | ||
| 55 | start: this, | ||
| 56 | end: anchor, | ||
| 57 | hide: true, | ||
| 58 | middleLabel: labelText, | ||
| 59 | path: 'magnet', | ||
| 60 | }) | ||
| 61 | line.show('draw', { duration: 300 }) | ||
| 62 | |||
| 63 | this.lines.push(line) | ||
| 64 | }) | ||
| 65 | |||
| 66 | this.onmouseout = () => removeLeaderLines(this) | ||
| 67 | |||
| 68 | // Click to move camera | ||
| 69 | this.onclick = (event) => { | ||
| 70 | event.preventDefault() | ||
| 71 | removeLeaderLines(this) | ||
| 72 | this.getMarkersFromMaps().forEach(marker => { | ||
| 73 | const map = marker.closest('.mapclay') | ||
| 74 | map.scrollIntoView({ behavior: 'smooth' }) | ||
| 75 | updateMapCameraByMarker([ | ||
| 76 | Number(this.dataset.lon), | ||
| 77 | Number(this.dataset.lat), | ||
| 78 | ])(marker) | ||
| 79 | }) | ||
| 80 | } | ||
| 81 | |||
| 82 | // Use middle click to remove markers | ||
| 83 | this.onauxclick = (e) => { | ||
| 84 | if (e.which !== 2) return | ||
| 85 | e.preventDefault() | ||
| 86 | removeLeaderLines(this) | ||
| 87 | this.getMarkersFromMaps() | ||
| 88 | .forEach(marker => marker.remove()) | ||
| 89 | } | ||
| 90 | } | ||
| 91 | |||
| 92 | /** | ||
| 93 | * getMarkersFromMaps. Get marker elements by GeoLink | ||
| 94 | * | ||
| 95 | * @param {HTMLAnchorElement} link | ||
| 96 | * @return {HTMLElement[]} markers | ||
| 97 | */ | ||
| 98 | getMarkersFromMaps () { | ||
| 99 | const params = new URLSearchParams(this.search) | ||
| 100 | const maps = Array.from( | ||
| 101 | this.closest('.Dumby') | ||
| 102 | .querySelectorAll('.mapclay[data-render="fulfilled"]'), | ||
| 103 | ) | ||
| 104 | return maps | ||
| 105 | .filter(map => this.targets ? this.targets.includes(map.id) : true) | ||
| 106 | .map(map => { | ||
| 107 | const renderer = map.renderer | ||
| 108 | const lonLat = [Number(this.dataset.lon), Number(this.dataset.lat)] | ||
| 109 | |||
| 110 | const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? | ||
| 111 | renderer.addMarker({ | ||
| 112 | xy: lonLat, | ||
| 113 | type: params.get('type') ?? null, | ||
| 114 | }) | ||
| 115 | marker.dataset.xy = lonLat | ||
| 116 | marker.title = new URLSearchParams(this.search).get('xy') ?? lonLat | ||
| 117 | const crs = this.dataset.crs | ||
| 118 | if (crs && crs !== 'EPSG:4326') { | ||
| 119 | marker.title += '@' + this.dataset.crs | ||
| 120 | } | ||
| 121 | |||
| 122 | return marker | ||
| 123 | }) | ||
| 124 | } | ||
| 125 | } | ||
| 126 | if (!window.customElements.get('dumby-geolink')) { | ||
| 127 | window.customElements.define('dumby-geolink', GeoLink, { extends: 'a' }) | ||
| 128 | } | ||
| 129 | |||
| 130 | /** | ||
| 131 | * Class: DocLink - link for DOM | ||
| 132 | * | ||
| 133 | * @extends {window.HTMLAnchorElement} | ||
| 134 | */ | ||
| 135 | export class DocLink extends window.HTMLAnchorElement { | ||
| 136 | static replaceWith = (link) => | ||
| 137 | link.replaceWith(new DocLink(link)) | ||
| 138 | |||
| 139 | /** | ||
| 140 | * Creates a new DocLink instance | ||
| 141 | * | ||
| 142 | * @param {HTMLAnchorElement} link | ||
| 143 | */ | ||
| 144 | constructor (link) { | ||
| 145 | super() | ||
| 146 | this.innerHTML = link.innerHTML | ||
| 147 | this.href = link.href | ||
| 148 | |||
| 149 | const label = decodeURIComponent(link.href.split('#')[1]) | ||
| 150 | const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) | ||
| 151 | if (!selector) return false | ||
| 152 | |||
| 153 | this.classList.add('with-leader-line', 'doclink') | ||
| 154 | this.lines = [] | ||
| 155 | |||
| 156 | this.onmouseover = () => { | ||
| 157 | const targets = document.querySelectorAll(selector) | ||
| 158 | |||
| 159 | targets.forEach(target => { | ||
| 160 | if (!target?.checkVisibility()) return | ||
| 161 | |||
| 162 | // highlight selected target | ||
| 163 | target.dataset.style = target.style.cssText | ||
| 164 | const rect = target.getBoundingClientRect() | ||
| 165 | const isTiny = rect.width < 100 || rect.height < 100 | ||
| 166 | if (isTiny) { | ||
| 167 | target.style.background = 'lightPink' | ||
| 168 | } else { | ||
| 169 | target.style.outline = 'lightPink 6px dashed' | ||
| 170 | } | ||
| 171 | |||
| 172 | // point to selected target | ||
| 173 | const line = new LeaderLine({ | ||
| 174 | start: this, | ||
| 175 | end: target, | ||
| 176 | middleLabel: LeaderLine.pathLabel({ | ||
| 177 | text: label, | ||
| 178 | fontWeight: 'bold', | ||
| 179 | }), | ||
| 180 | hide: true, | ||
| 181 | path: 'magnet', | ||
| 182 | }) | ||
| 183 | this.lines.push(line) | ||
| 184 | line.show('draw', { duration: 300 }) | ||
| 185 | }) | ||
| 186 | } | ||
| 187 | |||
| 188 | this.onmouseout = () => { | ||
| 189 | removeLeaderLines(this) | ||
| 190 | |||
| 191 | // resume targets from highlight | ||
| 192 | const targets = document.querySelectorAll(selector) | ||
| 193 | targets.forEach(target => { | ||
| 194 | target.style.cssText = target.dataset.style | ||
| 195 | delete target.dataset.style | ||
| 196 | }) | ||
| 197 | } | ||
| 198 | } | ||
| 199 | } | ||
| 200 | if (!window.customElements.get('dumby-doclink')) { | ||
| 201 | window.customElements.define('dumby-doclink', DocLink, { extends: 'a' }) | ||
| 202 | } | ||
| 203 | |||
| 204 | /** | ||
| 205 | * isAnchorVisible. check anchor(marker) is visible for current map camera | ||
| 206 | * | ||
| 207 | * @param {Element} anchor | ||
| 208 | */ | ||
| 209 | const isAnchorVisible = anchor => { | ||
| 210 | const mapContainer = anchor.closest('.mapclay') | ||
| 211 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | ||
| 212 | } | ||
| 213 | |||
| 214 | /** | ||
| 215 | * updateMapByMarker. get function for updating map camera by marker | ||
| 216 | * | ||
| 217 | * @param {Number[]} xy | ||
| 218 | * @return {Function} function | ||
| 219 | */ | ||
| 220 | const updateMapCameraByMarker = lonLat => marker => { | ||
| 221 | const renderer = marker.closest('.mapclay')?.renderer | ||
| 222 | renderer.updateCamera({ center: lonLat }, true) | ||
| 223 | } | ||
| 224 | |||
| 225 | /** | ||
| 226 | * removeLeaderLines. clean lines start from link | ||
| 227 | * | ||
| 228 | * @param {HTMLAnchorElement} link | ||
| 229 | */ | ||
| 230 | export const removeLeaderLines = link => { | ||
| 231 | if (!link.lines) return | ||
| 232 | link.lines.forEach(line => { | ||
| 233 | line.hide('draw', { duration: 300 }) | ||
| 234 | setTimeout(() => { | ||
| 235 | line.remove() | ||
| 236 | }, 300) | ||
| 237 | }) | ||
| 238 | link.lines = [] | ||
| 239 | } | ||
diff --git a/src/MenuItem.mjs b/src/MenuItem.mjs index 874c829..912080b 100644 --- a/src/MenuItem.mjs +++ b/src/MenuItem.mjs | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | import { shiftByWindow } from './utils.mjs' | 1 | import { shiftByWindow } from './utils.mjs' |
| 2 | import * as utils from './dumbyUtils.mjs' | 2 | import { GeoLink, removeLeaderLines } from './Link.mjs' |
| 3 | 3 | ||
| 4 | /** | 4 | /** |
| 5 | * @typedef {Object} RefLink | 5 | * @typedef {Object} RefLink |
| @@ -431,7 +431,7 @@ export const addRefLink = (cm, refLinks) => | |||
| 431 | /** | 431 | /** |
| 432 | * setGeoLinkTypeItem. | 432 | * setGeoLinkTypeItem. |
| 433 | * | 433 | * |
| 434 | * @param {HTMLAnchorElement} link | 434 | * @param {GeoLink} link |
| 435 | * @param {String} text | 435 | * @param {String} text |
| 436 | * @param {String} type | 436 | * @param {String} type |
| 437 | */ | 437 | */ |
| @@ -439,13 +439,14 @@ export const setGeoLinkTypeItem = ({ link, text, type }) => { | |||
| 439 | const params = new URLSearchParams(link.search) | 439 | const params = new URLSearchParams(link.search) |
| 440 | return new Item({ | 440 | return new Item({ |
| 441 | text, | 441 | text, |
| 442 | className: ['keep-menu'], | ||
| 442 | onclick: () => { | 443 | onclick: () => { |
| 443 | params.set('type', type) | 444 | params.set('type', type) |
| 444 | link.search = params | 445 | link.search = params |
| 445 | utils.removeLeaderLines(link) | 446 | removeLeaderLines(link) |
| 446 | utils.getMarkersFromMaps(link) | 447 | link.getMarkersFromMaps() |
| 447 | .forEach(marker => marker.remove()) | 448 | .forEach(marker => marker.remove()) |
| 448 | utils.getMarkersFromMaps(link) | 449 | link.getMarkersFromMaps() |
| 449 | }, | 450 | }, |
| 450 | }) | 451 | }) |
| 451 | } | 452 | } |
diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index 8fe23eb..6a1eaca 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs | |||
| @@ -1,8 +1,7 @@ | |||
| 1 | import LeaderLine from 'leader-line' | 1 | import LeaderLine from 'leader-line' |
| 2 | import { insideWindow, insideParent, replaceTextNodes, full2Half } from './utils' | 2 | import { replaceTextNodes, full2Half } from './utils' |
| 3 | import proj4 from 'proj4' | 3 | import proj4 from 'proj4' |
| 4 | 4 | import { coordPattern, GeoLink } from './Link.mjs' | |
| 5 | export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ | ||
| 6 | 5 | ||
| 7 | /** | 6 | /** |
| 8 | * focusNextMap. | 7 | * focusNextMap. |
| @@ -88,225 +87,6 @@ export function removeBlockFocus () { | |||
| 88 | } | 87 | } |
| 89 | 88 | ||
| 90 | /** | 89 | /** |
| 91 | * getMarkersFromMaps. Get marker elements by GeoLink | ||
| 92 | * | ||
| 93 | * @param {HTMLAnchorElement} link | ||
| 94 | * @return {HTMLElement[]} markers | ||
| 95 | */ | ||
| 96 | export const getMarkersFromMaps = link => { | ||
| 97 | const params = new URLSearchParams(link.search) | ||
| 98 | const maps = Array.from( | ||
| 99 | link.closest('.Dumby') | ||
| 100 | .querySelectorAll('.mapclay[data-render="fulfilled"]'), | ||
| 101 | ) | ||
| 102 | return maps | ||
| 103 | .filter(map => link.targets ? link.targets.includes(map.id) : true) | ||
| 104 | .map(map => { | ||
| 105 | const renderer = map.renderer | ||
| 106 | const lonLat = [Number(link.dataset.lon), Number(link.dataset.lat)] | ||
| 107 | |||
| 108 | const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? | ||
| 109 | renderer.addMarker({ | ||
| 110 | xy: lonLat, | ||
| 111 | type: params.get('type') ?? null, | ||
| 112 | }) | ||
| 113 | marker.dataset.xy = lonLat | ||
| 114 | marker.title = new URLSearchParams(link.search).get('xy') ?? lonLat | ||
| 115 | const crs = link.dataset.crs | ||
| 116 | if (crs && crs !== 'EPSG:4326') { | ||
| 117 | marker.title += '@' + link.dataset.crs | ||
| 118 | } | ||
| 119 | |||
| 120 | return marker | ||
| 121 | }) | ||
| 122 | } | ||
| 123 | |||
| 124 | /** | ||
| 125 | * addLeaderLine, from link element to target element | ||
| 126 | * | ||
| 127 | * @param {HTMLAnchorElement} link | ||
| 128 | * @param {Element} target | ||
| 129 | */ | ||
| 130 | const addLeaderLine = (link, target) => { | ||
| 131 | const labelText = new URL(link).searchParams.get('text') ?? link.textContent | ||
| 132 | const line = new LeaderLine({ | ||
| 133 | start: link, | ||
| 134 | end: target, | ||
| 135 | hide: true, | ||
| 136 | middleLabel: labelText, | ||
| 137 | path: 'magnet', | ||
| 138 | }) | ||
| 139 | line.show('draw', { duration: 300 }) | ||
| 140 | |||
| 141 | return line | ||
| 142 | } | ||
| 143 | |||
| 144 | /** | ||
| 145 | * Create geolinks, which points to map by geo schema and id | ||
| 146 | * | ||
| 147 | * @param {HTMLElement} Elements contains anchor elements for GeoLinks | ||
| 148 | * @returns {Boolean} ture is link is created, false if coordinates are invalid | ||
| 149 | */ | ||
| 150 | export const createGeoLink = (link) => { | ||
| 151 | const url = new URL(link.href) | ||
| 152 | const params = new URLSearchParams(link.search) | ||
| 153 | const xyInParams = params.get('xy')?.split(',')?.map(Number) | ||
| 154 | const [lon, lat] = url.href | ||
| 155 | ?.match(coordPattern) | ||
| 156 | ?.slice(1) | ||
| 157 | ?.reverse() | ||
| 158 | ?.map(Number) | ||
| 159 | const xy = xyInParams ?? [lon, lat] | ||
| 160 | |||
| 161 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false | ||
| 162 | |||
| 163 | // Geo information in link | ||
| 164 | link.dataset.lon = lon | ||
| 165 | link.dataset.lat = lat | ||
| 166 | link.dataset.crs = params.get('crs') | ||
| 167 | link.classList.add('with-leader-line', 'geolink') | ||
| 168 | link.classList.remove('not-geolink') | ||
| 169 | // TODO refactor as data attribute | ||
| 170 | link.targets = params.get('id')?.split(',') ?? null | ||
| 171 | link.title = 'Left-Click to move Camera, Middle-Click to clean anchor' | ||
| 172 | |||
| 173 | link.lines = [] | ||
| 174 | |||
| 175 | // Hover link for LeaderLine | ||
| 176 | link.onmouseover = () => { | ||
| 177 | if (link.dataset.valid === 'false') return | ||
| 178 | |||
| 179 | const anchors = getMarkersFromMaps(link) | ||
| 180 | anchors | ||
| 181 | .filter(isAnchorVisible) | ||
| 182 | .forEach(anchor => { | ||
| 183 | const line = addLeaderLine(link, anchor) | ||
| 184 | link.lines.push(line) | ||
| 185 | }) | ||
| 186 | } | ||
| 187 | link.onmouseout = () => removeLeaderLines(link) | ||
| 188 | |||
| 189 | // Click to move camera | ||
| 190 | link.onclick = (event) => { | ||
| 191 | event.preventDefault() | ||
| 192 | if (link.dataset.valid === 'false') return | ||
| 193 | |||
| 194 | removeLeaderLines(link) | ||
| 195 | getMarkersFromMaps(link).forEach(marker => { | ||
| 196 | const map = marker.closest('.mapclay') | ||
| 197 | map.scrollIntoView({ behavior: 'smooth' }) | ||
| 198 | updateMapCameraByMarker([ | ||
| 199 | Number(link.dataset.lon), | ||
| 200 | Number(link.dataset.lat), | ||
| 201 | ])(marker) | ||
| 202 | }) | ||
| 203 | } | ||
| 204 | |||
| 205 | // Use middle click to remove markers | ||
| 206 | link.onauxclick = (e) => { | ||
| 207 | if (e.which !== 2) return | ||
| 208 | e.preventDefault() | ||
| 209 | removeLeaderLines(link) | ||
| 210 | getMarkersFromMaps(link) | ||
| 211 | .forEach(marker => marker.remove()) | ||
| 212 | } | ||
| 213 | return true | ||
| 214 | } | ||
| 215 | |||
| 216 | /** | ||
| 217 | * CreateDocLink. | ||
| 218 | * | ||
| 219 | * @param {HTMLElement} Elements contains anchor elements for doclinks | ||
| 220 | */ | ||
| 221 | export const createDocLink = link => { | ||
| 222 | const label = decodeURIComponent(link.href.split('#')[1]) | ||
| 223 | const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) | ||
| 224 | if (!selector) return false | ||
| 225 | |||
| 226 | link.classList.add('with-leader-line', 'doclink') | ||
| 227 | link.lines = [] | ||
| 228 | |||
| 229 | link.onmouseover = () => { | ||
| 230 | const targets = document.querySelectorAll(selector) | ||
| 231 | |||
| 232 | targets.forEach(target => { | ||
| 233 | if (!target?.checkVisibility()) return | ||
| 234 | |||
| 235 | // highlight selected target | ||
| 236 | target.dataset.style = target.style.cssText | ||
| 237 | const rect = target.getBoundingClientRect() | ||
| 238 | const isTiny = rect.width < 100 || rect.height < 100 | ||
| 239 | if (isTiny) { | ||
| 240 | target.style.background = 'lightPink' | ||
| 241 | } else { | ||
| 242 | target.style.outline = 'lightPink 6px dashed' | ||
| 243 | } | ||
| 244 | |||
| 245 | // point to selected target | ||
| 246 | const line = new LeaderLine({ | ||
| 247 | start: link, | ||
| 248 | end: target, | ||
| 249 | middleLabel: LeaderLine.pathLabel({ | ||
| 250 | text: label, | ||
| 251 | fontWeight: 'bold', | ||
| 252 | }), | ||
| 253 | hide: true, | ||
| 254 | path: 'magnet', | ||
| 255 | }) | ||
| 256 | link.lines.push(line) | ||
| 257 | line.show('draw', { duration: 300 }) | ||
| 258 | }) | ||
| 259 | } | ||
| 260 | link.onmouseout = () => { | ||
| 261 | link.onmouseout = () => removeLeaderLines(link) | ||
| 262 | |||
| 263 | // resume targets from highlight | ||
| 264 | const targets = document.querySelectorAll(selector) | ||
| 265 | targets.forEach(target => { | ||
| 266 | target.style.cssText = target.dataset.style | ||
| 267 | delete target.dataset.style | ||
| 268 | }) | ||
| 269 | } | ||
| 270 | } | ||
| 271 | |||
| 272 | /** | ||
| 273 | * removeLeaderLines. clean lines start from link | ||
| 274 | * | ||
| 275 | * @param {HTMLAnchorElement} link | ||
| 276 | */ | ||
| 277 | export const removeLeaderLines = link => { | ||
| 278 | if (!link.lines) return | ||
| 279 | link.lines.forEach(line => { | ||
| 280 | line.hide('draw', { duration: 300 }) | ||
| 281 | setTimeout(() => { | ||
| 282 | line.remove() | ||
| 283 | }, 300) | ||
| 284 | }) | ||
| 285 | link.lines = [] | ||
| 286 | } | ||
| 287 | |||
| 288 | /** | ||
| 289 | * updateMapByMarker. get function for updating map camera by marker | ||
| 290 | * | ||
| 291 | * @param {Number[]} xy | ||
| 292 | * @return {Function} function | ||
| 293 | */ | ||
| 294 | const updateMapCameraByMarker = lonLat => marker => { | ||
| 295 | const renderer = marker.closest('.mapclay')?.renderer | ||
| 296 | renderer.updateCamera({ center: lonLat }, true) | ||
| 297 | } | ||
| 298 | |||
| 299 | /** | ||
| 300 | * isAnchorVisible. check anchor(marker) is visible for current map camera | ||
| 301 | * | ||
| 302 | * @param {Element} anchor | ||
| 303 | */ | ||
| 304 | const isAnchorVisible = anchor => { | ||
| 305 | const mapContainer = anchor.closest('.mapclay') | ||
| 306 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | ||
| 307 | } | ||
| 308 | |||
| 309 | /** | ||
| 310 | * addMarkerByPoint. | 90 | * addMarkerByPoint. |
| 311 | * | 91 | * |
| 312 | * @param {Number[]} options.point - page XY | 92 | * @param {Number[]} options.point - page XY |
| @@ -328,6 +108,30 @@ export const addMarkerByPoint = ({ point, map }) => { | |||
| 328 | } | 108 | } |
| 329 | 109 | ||
| 330 | /** | 110 | /** |
| 111 | * addGeoSchemeByText. | ||
| 112 | * | ||
| 113 | * @param {Node} node | ||
| 114 | */ | ||
| 115 | export const addGeoSchemeByText = async (node) => { | ||
| 116 | const digit = '[\\d\\uFF10-\\uFF19]' | ||
| 117 | const decimal = '[.\\uFF0E]' | ||
| 118 | const coordPatterns = `(-?${digit}+${decimal}?${digit}*)([,\x2F\uFF0C])(-?${digit}+${decimal}?${digit}*)` | ||
| 119 | const re = new RegExp(coordPatterns, 'g') | ||
| 120 | |||
| 121 | return replaceTextNodes(node, re, match => { | ||
| 122 | const [x, y] = [full2Half(match.at(1)), full2Half(match.at(3))] | ||
| 123 | // Don't process string which can be used as date | ||
| 124 | if (Date.parse(match.at(0) + ' 1990')) return null | ||
| 125 | |||
| 126 | const a = document.createElement('a') | ||
| 127 | a.className = 'not-geolink from-text' | ||
| 128 | a.href = `geo:0,0?xy=${x},${y}` | ||
| 129 | a.textContent = match.at(0) | ||
| 130 | return a | ||
| 131 | }) | ||
| 132 | } | ||
| 133 | |||
| 134 | /** | ||
| 331 | * setGeoSchemeByCRS. | 135 | * setGeoSchemeByCRS. |
| 332 | * @description Add more information into Anchor Element within Geo Scheme by CRS | 136 | * @description Add more information into Anchor Element within Geo Scheme by CRS |
| 333 | * @param {String} crs - EPSG/ESRI Code for CRS | 137 | * @param {String} crs - EPSG/ESRI Code for CRS |
| @@ -375,17 +179,17 @@ export const setGeoSchemeByCRS = (crs) => (link) => { | |||
| 375 | } | 179 | } |
| 376 | 180 | ||
| 377 | /** | 181 | /** |
| 378 | * dragForAnchor. | 182 | * addGeoLinkByDrag. |
| 379 | * | 183 | * |
| 380 | * @param {HTMLElement} container | 184 | * @param {HTMLElement} container |
| 381 | * @param {Range} range | 185 | * @param {Range} range |
| 382 | */ | 186 | */ |
| 383 | export const dragForAnchor = (container, range, endOfLeaderLine) => { | 187 | export const addGeoLinkByDrag = (container, range, endOfLeaderLine) => { |
| 384 | // link placeholder when dragging | 188 | // link placeholder when dragging |
| 385 | container.classList.add('dragging-geolink') | 189 | container.classList.add('dragging-geolink') |
| 386 | const geoLink = document.createElement('a') | 190 | const link = document.createElement('a') |
| 387 | geoLink.textContent = range.toString() | 191 | link.textContent = range.toString() |
| 388 | geoLink.classList.add('with-leader-line', 'geolink', 'drag', 'from-text') | 192 | link.classList.add('with-leader-line', 'geolink', 'drag', 'from-text') |
| 389 | 193 | ||
| 390 | // Replace current content with link | 194 | // Replace current content with link |
| 391 | const originContent = range.cloneContents() | 195 | const originContent = range.cloneContents() |
| @@ -394,11 +198,11 @@ export const dragForAnchor = (container, range, endOfLeaderLine) => { | |||
| 394 | range.insertNode(originContent) | 198 | range.insertNode(originContent) |
| 395 | } | 199 | } |
| 396 | range.deleteContents() | 200 | range.deleteContents() |
| 397 | range.insertNode(geoLink) | 201 | range.insertNode(link) |
| 398 | 202 | ||
| 399 | // Add leader-line | 203 | // Add leader-line |
| 400 | const line = new LeaderLine({ | 204 | const line = new LeaderLine({ |
| 401 | start: geoLink, | 205 | start: link, |
| 402 | end: endOfLeaderLine, | 206 | end: endOfLeaderLine, |
| 403 | path: 'magnet', | 207 | path: 'magnet', |
| 404 | }) | 208 | }) |
| @@ -416,7 +220,7 @@ export const dragForAnchor = (container, range, endOfLeaderLine) => { | |||
| 416 | container.classList.remove('dragging-geolink') | 220 | container.classList.remove('dragging-geolink') |
| 417 | container.onmousemove = null | 221 | container.onmousemove = null |
| 418 | container.onmouseup = null | 222 | container.onmouseup = null |
| 419 | geoLink.classList.remove('drag') | 223 | link.classList.remove('drag') |
| 420 | positionObserver.disconnect() | 224 | positionObserver.disconnect() |
| 421 | line.remove() | 225 | line.remove() |
| 422 | endOfLeaderLine.remove() | 226 | endOfLeaderLine.remove() |
| @@ -434,31 +238,7 @@ export const dragForAnchor = (container, range, endOfLeaderLine) => { | |||
| 434 | return | 238 | return |
| 435 | } | 239 | } |
| 436 | 240 | ||
| 437 | geoLink.href = `geo:${marker.dataset.xy.split(',').reverse()}` | 241 | link.href = `geo:${marker.dataset.xy.split(',').reverse()}` |
| 438 | createGeoLink(geoLink) | 242 | GeoLink.replaceWith(link) |
| 439 | } | 243 | } |
| 440 | } | 244 | } |
| 441 | |||
| 442 | /** | ||
| 443 | * addGeoSchemeByText. | ||
| 444 | * | ||
| 445 | * @param {Node} node | ||
| 446 | */ | ||
| 447 | export const addGeoSchemeByText = async (node) => { | ||
| 448 | const digit = '[\\d\\uFF10-\\uFF19]' | ||
| 449 | const decimal = '[.\\uFF0E]' | ||
| 450 | const coordPatterns = `(-?${digit}+${decimal}?${digit}*)([,\x2F\uFF0C])(-?${digit}+${decimal}?${digit}*)` | ||
| 451 | const re = new RegExp(coordPatterns, 'g') | ||
| 452 | |||
| 453 | return replaceTextNodes(node, re, match => { | ||
| 454 | const [x, y] = [full2Half(match.at(1)), full2Half(match.at(3))] | ||
| 455 | // Don't process string which can be used as date | ||
| 456 | if (Date.parse(match.at(0) + ' 1990')) return null | ||
| 457 | |||
| 458 | const a = document.createElement('a') | ||
| 459 | a.className = 'not-geolink from-text' | ||
| 460 | a.href = `geo:0,0?xy=${x},${y}` | ||
| 461 | a.textContent = match.at(0) | ||
| 462 | return a | ||
| 463 | }) | ||
| 464 | } | ||
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 92fe785..bb3737b 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
| @@ -6,6 +6,7 @@ import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' | |||
| 6 | import * as mapclay from 'mapclay' | 6 | import * as mapclay from 'mapclay' |
| 7 | import { onRemove, animateRectTransition, throttle, debounce, shiftByWindow } from './utils' | 7 | import { onRemove, animateRectTransition, throttle, debounce, shiftByWindow } from './utils' |
| 8 | import { Layout, SideBySide, Overlay, Sticky } from './Layout' | 8 | import { Layout, SideBySide, Overlay, Sticky } from './Layout' |
| 9 | import { GeoLink, DocLink } from './Link.mjs' | ||
| 9 | import * as utils from './dumbyUtils' | 10 | import * as utils from './dumbyUtils' |
| 10 | import * as menuItem from './MenuItem' | 11 | import * as menuItem from './MenuItem' |
| 11 | import PlainModal from 'plain-modal' | 12 | import PlainModal from 'plain-modal' |
| @@ -209,7 +210,7 @@ export const generateMaps = (container, { | |||
| 209 | if (node.matches?.('.mapclay') || node.closest?.('.mapclay')) return | 210 | if (node.matches?.('.mapclay') || node.closest?.('.mapclay')) return |
| 210 | 211 | ||
| 211 | // Add GeoLinks from plain texts | 212 | // Add GeoLinks from plain texts |
| 212 | addGeoLinksByText(node) | 213 | utils.addGeoSchemeByText(node) |
| 213 | 214 | ||
| 214 | // Render Map | 215 | // Render Map |
| 215 | const mapTarget = node.parentElement?.closest(mapBlockSelector) | 216 | const mapTarget = node.parentElement?.closest(mapBlockSelector) |
| @@ -245,9 +246,9 @@ export const generateMaps = (container, { | |||
| 245 | 246 | ||
| 246 | // Add GeoLinks/DocLinks by pattern | 247 | // Add GeoLinks/DocLinks by pattern |
| 247 | target.querySelectorAll(geoLinkSelector) | 248 | target.querySelectorAll(geoLinkSelector) |
| 248 | .forEach(utils.createGeoLink) | 249 | .forEach(GeoLink.replaceWith) |
| 249 | target.querySelectorAll(docLinkSelector) | 250 | target.querySelectorAll(docLinkSelector) |
| 250 | .forEach(utils.createDocLink) | 251 | .forEach(DocLink.replaceWith) |
| 251 | 252 | ||
| 252 | // Add GeoLinks from text nodes | 253 | // Add GeoLinks from text nodes |
| 253 | // const addedNodes = Array.from(mutation.addedNodes) | 254 | // const addedNodes = Array.from(mutation.addedNodes) |
| @@ -318,7 +319,7 @@ export const generateMaps = (container, { | |||
| 318 | values.at(-1) | 319 | values.at(-1) |
| 319 | .map(utils.setGeoSchemeByCRS(crsString)) | 320 | .map(utils.setGeoSchemeByCRS(crsString)) |
| 320 | .filter(link => link) | 321 | .filter(link => link) |
| 321 | .forEach(utils.createGeoLink) | 322 | .forEach(GeoLink.replaceWith) |
| 322 | }) | 323 | }) |
| 323 | } | 324 | } |
| 324 | 325 | ||
| @@ -556,7 +557,7 @@ export const generateMaps = (container, { | |||
| 556 | menu.appendChild(new menuItem.Item({ | 557 | menu.appendChild(new menuItem.Item({ |
| 557 | text: 'Delete', | 558 | text: 'Delete', |
| 558 | onclick: () => { | 559 | onclick: () => { |
| 559 | utils.getMarkersFromMaps(geoLink) | 560 | geoLink.getMarkersFromMaps() |
| 560 | .forEach(m => m.remove()) | 561 | .forEach(m => m.remove()) |
| 561 | geoLink.replaceWith( | 562 | geoLink.replaceWith( |
| 562 | document.createTextNode(geoLink.textContent), | 563 | document.createTextNode(geoLink.textContent), |
| @@ -637,7 +638,7 @@ export const generateMaps = (container, { | |||
| 637 | container.appendChild(pointByArrow) | 638 | container.appendChild(pointByArrow) |
| 638 | 639 | ||
| 639 | const timer = setTimeout(() => { | 640 | const timer = setTimeout(() => { |
| 640 | utils.dragForAnchor(container, range, pointByArrow) | 641 | utils.addGeoLinkByDrag(container, range, pointByArrow) |
| 641 | }, 300) | 642 | }, 300) |
| 642 | 643 | ||
| 643 | // Update leader-line with mouse move | 644 | // Update leader-line with mouse move |