aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/Link.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Link.mjs')
-rw-r--r--src/Link.mjs239
1 files changed, 239 insertions, 0 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 @@
1import LeaderLine from 'leader-line'
2import { insideWindow, insideParent } from './utils'
3
4/** VAR: pattern for coodinates */
5export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/
6
7/**
8 * Class: GeoLink - link for maps
9 *
10 * @extends {window.HTMLAnchorElement}
11 */
12export 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}
126if (!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 */
135export 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}
200if (!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 */
209const 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 */
220const 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 */
230export 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}