@signalk/charts-plugin 3.7.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.prettierignore +1 -0
  2. package/CHANGELOG.md +109 -0
  3. package/README.md +72 -0
  4. package/docs/screenshots/1_overview.png +0 -0
  5. package/docs/screenshots/2_region.png +0 -0
  6. package/docs/screenshots/3_jobs.png +0 -0
  7. package/package.json +13 -8
  8. package/plugin/cacheLayout.d.ts +72 -0
  9. package/plugin/cacheLayout.d.ts.map +1 -0
  10. package/plugin/cacheLayout.js +243 -0
  11. package/plugin/cacheLayout.js.map +1 -0
  12. package/plugin/chartDownloader.d.ts +122 -29
  13. package/plugin/chartDownloader.d.ts.map +1 -1
  14. package/plugin/chartDownloader.js +488 -313
  15. package/plugin/chartDownloader.js.map +1 -1
  16. package/plugin/chartDownloaderMBTilesHelpers.d.ts +10 -0
  17. package/plugin/chartDownloaderMBTilesHelpers.d.ts.map +1 -0
  18. package/plugin/chartDownloaderMBTilesHelpers.js +235 -0
  19. package/plugin/chartDownloaderMBTilesHelpers.js.map +1 -0
  20. package/plugin/chartDownloaderTileHelpers.d.ts +30 -0
  21. package/plugin/chartDownloaderTileHelpers.d.ts.map +1 -0
  22. package/plugin/chartDownloaderTileHelpers.js +277 -0
  23. package/plugin/chartDownloaderTileHelpers.js.map +1 -0
  24. package/plugin/charts.d.ts.map +1 -1
  25. package/plugin/charts.js +23 -5
  26. package/plugin/charts.js.map +1 -1
  27. package/plugin/constants.d.ts.map +1 -1
  28. package/plugin/index.d.ts.map +1 -1
  29. package/plugin/index.js +560 -47
  30. package/plugin/index.js.map +1 -1
  31. package/plugin/pluginStatus.js.map +1 -1
  32. package/plugin/projection.js.map +1 -1
  33. package/plugin/tileServer.d.ts +4 -2
  34. package/plugin/tileServer.d.ts.map +1 -1
  35. package/plugin/tileServer.js +37 -8
  36. package/plugin/tileServer.js.map +1 -1
  37. package/plugin/tokenProvider.d.ts +63 -0
  38. package/plugin/tokenProvider.d.ts.map +1 -0
  39. package/plugin/tokenProvider.js +258 -0
  40. package/plugin/tokenProvider.js.map +1 -0
  41. package/plugin/types.d.ts +37 -2
  42. package/plugin/types.d.ts.map +1 -1
  43. package/public/images/jobs.png +0 -0
  44. package/public/images/overview.png +0 -0
  45. package/public/images/region.png +0 -0
  46. package/public/index.html +392 -113
  47. package/public/js/index.js +620 -0
  48. package/public/leaflet/images/downloads.png +0 -0
  49. package/public/leaflet/images/layers-2x.png +0 -0
  50. package/public/leaflet/images/layers.png +0 -0
  51. package/public/leaflet/images/lifebuoy.png +0 -0
  52. package/public/leaflet/images/spritesheet-2x.png +0 -0
  53. package/public/leaflet/images/spritesheet.svg +156 -0
  54. package/public/leaflet/leaflet.css +677 -0
  55. package/public/leaflet/leaflet.draw.css +253 -0
  56. package/public/leaflet/leaflet.draw.js +2957 -0
  57. package/public/leaflet/leaflet.js +7467 -0
  58. package/public/style.css +351 -37
  59. package/docs/screenshots/1_placeholder.png +0 -0
  60. package/public/index.js +0 -349
@@ -0,0 +1,620 @@
1
+ var map
2
+ var settings
3
+ var layerControl
4
+ var drawnItems
5
+ var activeRegionLayer = null
6
+ var isDeleting = false
7
+ let jobsPollInterval = null
8
+ let jobsControl = null
9
+
10
+ const JobType = Object.freeze({
11
+ None: 0,
12
+ Seed: 1,
13
+ Delete: 2
14
+ })
15
+
16
+ // Chart source types this tool can prefetch ("seed"). Only proxied raster tile
17
+ // sources have a {z}/{x}/{y} cache to fill; style / manifest types
18
+ // (mapstyleJSON, tileJSON) and vector ENC (S-57) cannot be seeded, so they are
19
+ // hidden from the chart picker and the preview map. Mirrors SEEDABLE_SOURCE_TYPES
20
+ // in src/tileServer.ts (the server enforces the same rule); keep the two in sync.
21
+ const SEEDABLE_CHART_TYPES = ['tilelayer', 'WMS', 'WMTS']
22
+
23
+ function isSeedableChart(info) {
24
+ return (
25
+ !!info && info.proxy === true && SEEDABLE_CHART_TYPES.includes(info.type)
26
+ )
27
+ }
28
+
29
+ document.addEventListener('DOMContentLoaded', async () => {
30
+ await setupCharts()
31
+
32
+ setupRegions()
33
+ })
34
+
35
+ function normalizeLng(lng) {
36
+ let v = Number(lng)
37
+ if (!isFinite(v)) return v
38
+ while (v < -360) v += 360
39
+ while (v > 360) v -= 360
40
+ return v
41
+ }
42
+
43
+ function normalizePolygon(layer) {
44
+ const latlngs = layer.getLatLngs()
45
+
46
+ latlngs[0].forEach((ll) => {
47
+ ll.lng = normalizeLng(ll.lng)
48
+ })
49
+
50
+ layer.setLatLngs(latlngs)
51
+ }
52
+
53
+ function saveSettings() {
54
+ localStorage.setItem('settings', JSON.stringify(settings))
55
+ }
56
+
57
+ async function setupCharts() {
58
+ settings = JSON.parse(localStorage.getItem('settings') || '{}')
59
+
60
+ settings.map = settings.map || { center: [0, 0], zoomLevel: 3 }
61
+ settings.selections = settings.selections || { charts: [] }
62
+ settings.selections.charts = settings.selections.charts || []
63
+
64
+ map = L.map('map').setView(
65
+ settings ? settings.map.center : [0, 0],
66
+ settings ? settings.map.zoomLevel : 8
67
+ )
68
+
69
+ map.on('moveend', function () {
70
+ const center = map.getCenter()
71
+ const zoom = map.getZoom()
72
+ settings.map = {
73
+ center: [center.lat, center.lng],
74
+ zoomLevel: zoom
75
+ }
76
+ saveSettings()
77
+ })
78
+
79
+ // Layer to store drawn shapes
80
+ drawnItems = new L.FeatureGroup()
81
+ map.addLayer(drawnItems)
82
+
83
+ // Add draw controls
84
+ const drawControl = new L.Control.Draw({
85
+ position: 'topleft',
86
+ edit: {
87
+ featureGroup: drawnItems
88
+ },
89
+ draw: {
90
+ polygon: {
91
+ allowIntersection: false, // Restricts shapes to simple polygons
92
+ drawError: {
93
+ color: '#e1e100', // Color the shape will turn when intersects
94
+ message: "<strong>Oh snap!<strong> you can't draw that!" // Message that will show when intersect
95
+ },
96
+ shapeOptions: {
97
+ color: '#97009c'
98
+ }
99
+ },
100
+ polygon: true,
101
+ polyline: false,
102
+ rectangle: true,
103
+ circle: false,
104
+ marker: false,
105
+ circlemarker: false
106
+ }
107
+ })
108
+
109
+ map.on(L.Draw.Event.DELETESTART, () => {
110
+ isDeleting = true
111
+ })
112
+
113
+ map.on(L.Draw.Event.DELETESTOP, () => {
114
+ isDeleting = false
115
+ })
116
+
117
+ map.addControl(drawControl)
118
+
119
+ jobsControl = L.control({ position: 'topright' })
120
+
121
+ jobsControl.onAdd = function () {
122
+ const btn = L.DomUtil.create(
123
+ 'button',
124
+ 'leaflet-bar leaflet-control leaflet-control-custom leaflet-control-button leaflet-button'
125
+ )
126
+ btn.innerHTML =
127
+ '<img src="leaflet/images/downloads.png" alt="Active seeding jobs" style="width:16px;height:16px;">'
128
+
129
+ // Create badge
130
+ const badge = L.DomUtil.create('span', 'jobs-badge', btn)
131
+ badge.innerText = '0' // initial value
132
+ // Prevent map drag when clicking
133
+ L.DomEvent.disableClickPropagation(btn)
134
+
135
+ btn.onclick = toggleJobsModal
136
+ // Store reference so we can update it later
137
+ jobsControl._badge = badge
138
+ updateJobsBadge(0) // hide badge initially
139
+ return btn
140
+ }
141
+ jobsControl.addTo(map)
142
+
143
+ const helpControl = L.control({ position: 'bottomright' })
144
+
145
+ helpControl.onAdd = function () {
146
+ const btn = L.DomUtil.create(
147
+ 'button',
148
+ 'leaflet-bar leaflet-control leaflet-control-custom leaflet-control-button leaflet-button'
149
+ )
150
+ btn.innerHTML =
151
+ '<img src="leaflet/images/lifebuoy.png" alt="Help" style="width:16px;height:16px;">'
152
+
153
+ // Prevent map drag when clicking
154
+ L.DomEvent.disableClickPropagation(btn)
155
+
156
+ btn.onclick = toggleHelpModal
157
+ return btn
158
+ }
159
+ helpControl.addTo(map)
160
+
161
+ map.on(L.Draw.Event.CREATED, (e) => {
162
+ const layer = e.layer
163
+ normalizePolygon(layer)
164
+ drawnItems.addLayer(layer)
165
+ layer.feature = {
166
+ type: 'Feature',
167
+ properties: { name: '' }
168
+ }
169
+ layer.on('click', () => {
170
+ if (isDeleting) return
171
+ openRegionModal(layer)
172
+ })
173
+ drawnItems.addLayer(layer)
174
+ openRegionModal(layer)
175
+ saveRegions()
176
+ })
177
+ map.on(L.Draw.Event.EDITED, () => saveRegions())
178
+ map.on(L.Draw.Event.DELETED, () => saveRegions())
179
+ await loadRegions()
180
+
181
+ // Create a custom control
182
+ const statusControl = L.control({ position: 'bottomleft' })
183
+
184
+ statusControl.onAdd = function () {
185
+ const div = L.DomUtil.create('div', 'map-status-control')
186
+ div.innerHTML = 'Lat: --, Lng: -- | Zoom: --'
187
+ return div
188
+ }
189
+
190
+ statusControl.addTo(map)
191
+
192
+ map.on('mousemove', function (e) {
193
+ const { lat, lng } = e.latlng
194
+ document.querySelector('.map-status-control').innerHTML =
195
+ `Lat: ${lat.toFixed(5)}, Lng: ${lng.toFixed(5)} | Zoom: ${map.getZoom()}`
196
+ })
197
+
198
+ map.on('zoomend', function () {
199
+ const div = document.querySelector('.map-status-control')
200
+ if (div.textContent.includes('Lat')) {
201
+ const zoom = map.getZoom()
202
+ div.innerHTML = div.innerHTML.replace(/Zoom: \d+/, `Zoom: ${zoom}`)
203
+ }
204
+ })
205
+
206
+ const chartSelect = document.getElementById('chartSelect')
207
+ if (!chartSelect) return
208
+ // Fetch available charts from SignalK server
209
+ try {
210
+ const resp = await fetch('/signalk/v2/api/resources/charts')
211
+ if (!resp.ok) {
212
+ throw new Error(`Failed to fetch charts: ${resp.status}`)
213
+ }
214
+ const data = await resp.json()
215
+ const entries = Object.entries(data || {})
216
+ let overlayMaps = {}
217
+ entries
218
+ .sort(([, a], [, b]) => {
219
+ const na = a && a.name ? String(a.name) : ''
220
+ const nb = b && b.name ? String(b.name) : ''
221
+ return na.localeCompare(nb)
222
+ })
223
+ .forEach(([id, info]) => {
224
+ // Only proxied raster tile charts can be seeded; skip style / vector
225
+ // sources (e.g. mapstyleJSON) so they aren't offered for a job that
226
+ // would only fail. See isSeedableChart.
227
+ if (isSeedableChart(info)) {
228
+ const opt = document.createElement('option')
229
+ opt.value = id
230
+ opt.textContent = info && info.name ? info.name : id
231
+ chartSelect.appendChild(opt)
232
+
233
+ let layer = L.tileLayer(`${info.url}`, {
234
+ maxZoom: info.maxZoom || 19,
235
+ minZoom: info.minZoom || 1,
236
+ checked: settings.selections.charts.includes(info.name)
237
+ })
238
+ layer.addTo(map)
239
+ if (!settings.selections.charts.includes(info.name)) {
240
+ map.removeLayer(layer)
241
+ }
242
+ overlayMaps[info.name || id] = layer
243
+ }
244
+ })
245
+ layerControl = L.control
246
+ .layers({}, overlayMaps, {
247
+ position: 'topright'
248
+ })
249
+ .addTo(map)
250
+ } catch (err) {
251
+ console.error('Error loading maps:', err)
252
+ }
253
+
254
+ map.on('overlayadd', function (e) {
255
+ const name = e.name
256
+
257
+ if (!settings.selections.charts.includes(name)) {
258
+ settings.selections.charts.push(name)
259
+ }
260
+
261
+ saveSettings()
262
+ })
263
+
264
+ map.on('overlayremove', function (e) {
265
+ const name = e.name
266
+
267
+ settings.selections.charts = settings.selections.charts.filter(
268
+ (c) => c !== name
269
+ )
270
+
271
+ saveSettings()
272
+ })
273
+
274
+ setupDragAndDrop()
275
+ }
276
+
277
+ // Help
278
+
279
+ function toggleHelpModal() {
280
+ const modal = document.getElementById('help')
281
+ modal.classList.toggle('hidden')
282
+ }
283
+
284
+ // Jobs
285
+
286
+ function toggleJobsModal() {
287
+ let modal = document.getElementById('jobsModal')
288
+ const isHidden = modal.classList.toggle('hidden')
289
+
290
+ if (isHidden) {
291
+ // Stop polling while the panel is closed; nothing is watching it and the
292
+ // 2s fetch otherwise runs for the life of the page.
293
+ if (jobsPollInterval) {
294
+ clearInterval(jobsPollInterval)
295
+ jobsPollInterval = null
296
+ }
297
+ } else {
298
+ if (jobsPollInterval) return // already running
299
+ fetchActiveJobs() // fetch immediately, then poll while open
300
+ jobsPollInterval = setInterval(fetchActiveJobs, 2000)
301
+ }
302
+ }
303
+
304
+ function cloneTemplate(id) {
305
+ const tpl = document.getElementById(id)
306
+ if (!tpl) {
307
+ console.error(`Template "${id}" not found`)
308
+ return null
309
+ }
310
+ return tpl.content.cloneNode(true)
311
+ }
312
+
313
+ function fillProgress(container, job) {
314
+ const pct =
315
+ typeof job.progress === 'number' && isFinite(job.progress)
316
+ ? Math.max(0, Math.min(100, job.progress * 100))
317
+ : 0
318
+ const pctText = `${pct.toFixed(1)}%`
319
+
320
+ container.innerHTML = `
321
+ <span class="progress-bar">
322
+ <span class="progress-fill" style="width:${pct}%"></span>
323
+ </span>
324
+ <span class="percent">${pctText}</span>
325
+ `
326
+ }
327
+
328
+ function createJobCard(job) {
329
+ const frag = cloneTemplate('job-card-template')
330
+ if (!frag) return ''
331
+ const card = frag.querySelector('.job-card')
332
+
333
+ card.dataset.id = job.id
334
+
335
+ card.querySelector('.job-region').textContent = job.regionName
336
+ card.querySelector('.job-chart').textContent = job.chartName
337
+ card.querySelector('.job-status').textContent = job.status || ''
338
+ card.querySelector('.job-total').textContent = job.totalTiles.toLocaleString()
339
+ if (job.type === JobType.Seed) {
340
+ card.querySelector('.job-seed').classList.remove('hidden')
341
+ card.querySelector('.job-downloaded').textContent =
342
+ job.downloadedTiles.toLocaleString()
343
+ card.querySelector('.job-cached').textContent =
344
+ job.cachedTiles.toLocaleString()
345
+ card.querySelector('.job-failed').textContent =
346
+ job.failedTiles.toLocaleString()
347
+ } else if (job.type === JobType.Delete) {
348
+ card.querySelector('.job-delete').classList.remove('hidden')
349
+ card.querySelector('.job-deleted').textContent = job.deletedTiles
350
+ ? job.deletedTiles.toLocaleString()
351
+ : '0'
352
+ }
353
+
354
+ card.querySelector('.job-remove').addEventListener('click', () => {
355
+ takeAction(job.id, 'remove')
356
+ })
357
+
358
+ fillProgress(card.querySelector('.job-card-progress'), job)
359
+
360
+ return frag
361
+ }
362
+
363
+ function renderJobs(jobs) {
364
+ const cardContainer = document.getElementById('jobsGrid')
365
+
366
+ cardContainer.innerHTML = ''
367
+
368
+ if (!Array.isArray(jobs) || jobs.length === 0) {
369
+ document.getElementById('noJobsMessage').classList.remove('hidden')
370
+ return
371
+ }
372
+ document.getElementById('noJobsMessage').classList.add('hidden')
373
+ jobs.forEach((job, index) => {
374
+ cardContainer.appendChild(createJobCard(job))
375
+ })
376
+ }
377
+
378
+ async function fetchActiveJobs() {
379
+ try {
380
+ const resp = await fetch('/signalk/chart-tiles/cache/jobs')
381
+ if (!resp.ok) throw new Error('HTTP ' + resp.status)
382
+ const data = await resp.json()
383
+ renderJobs(data)
384
+ updateJobsBadge(data.filter((item) => item.state === 1).length)
385
+ } catch (err) {
386
+ console.error('Error fetching active jobs:', err)
387
+ }
388
+ }
389
+
390
+ async function createJob(action) {
391
+ try {
392
+ if (activeRegionLayer == null) {
393
+ return
394
+ }
395
+ const chart = document.getElementById('chartSelect').value
396
+ const minZoom = document.getElementById('minZoom').value
397
+ const maxZoom = document.getElementById('maxZoom').value
398
+ const refetch = document.getElementById('refetch').checked
399
+ const mbtiles = document.getElementById('mbtiles').checked
400
+ const vacuum = document.getElementById('vacuum').checked
401
+ const body = {}
402
+ body.minZoom = minZoom
403
+ body.maxZoom = maxZoom
404
+ body.feature = activeRegionLayer.toGeoJSON()
405
+ body.action = action
406
+ body.options = { refetch: refetch, mbtiles: mbtiles, vacuum: vacuum }
407
+
408
+ const resp = await fetch(`/signalk/chart-tiles/cache/${chart}`, {
409
+ method: 'POST',
410
+ headers: {
411
+ 'Content-Type': 'application/json'
412
+ },
413
+ body: JSON.stringify(body)
414
+ })
415
+ if (!resp.ok) {
416
+ throw new Error(`Failed to seed charts: ${resp.status}`)
417
+ }
418
+ const data = await resp.json()
419
+ const responseSpan = document.getElementById('response')
420
+ responseSpan.classList.remove('hidden')
421
+ responseSpan.textContent = data.message || 'Success'
422
+ setTimeout(() => {
423
+ responseSpan.textContent = ''
424
+ responseSpan.classList.add('hidden')
425
+ }, 2000)
426
+ } catch (err) {
427
+ console.error('Error seeding charts:', err)
428
+ const responseSpan = document.getElementById('response')
429
+ if (responseSpan) {
430
+ responseSpan.classList.remove('hidden')
431
+ responseSpan.textContent = err.message || 'Error seeding charts'
432
+ }
433
+ }
434
+ closeRegionModal()
435
+ }
436
+
437
+ function updateJobsBadge(count) {
438
+ if (!jobsControl._badge) return
439
+ jobsControl._badge.innerText = count
440
+ jobsControl._badge.style.display = count > 0 ? 'flex' : 'none'
441
+ }
442
+
443
+ function takeAction(jobId, action) {
444
+ const serverResponse = document.getElementById('response')
445
+ fetch(`/signalk/chart-tiles/cache/jobs/${jobId}`, {
446
+ method: 'POST',
447
+ headers: {
448
+ 'Content-Type': 'application/json'
449
+ },
450
+ body: JSON.stringify({
451
+ action: action
452
+ })
453
+ })
454
+ .then((response) => {
455
+ if (!response.ok) {
456
+ throw new Error(`Failed to ${action} job ${jobId}: ${response.status}`)
457
+ }
458
+ fetchActiveJobs()
459
+ })
460
+ .catch((error) => {
461
+ console.error(`Error ${action} job:`, error)
462
+ })
463
+ }
464
+
465
+ // Regions
466
+
467
+ function setupRegions() {
468
+ // Region modal form
469
+ document.getElementById('saveRegion').onclick = () => {
470
+ if (!activeRegionLayer) return
471
+
472
+ const p = activeRegionLayer.feature.properties
473
+ // Explicit DOM lookup rather than relying on the legacy
474
+ // id-becomes-window-property behaviour, which is non-strict-mode-only
475
+ // and a frequent source of "works in Chrome, breaks in tests" bugs.
476
+ p.name = document.getElementById('regionName').value
477
+ saveRegions()
478
+ }
479
+ }
480
+
481
+ function setupDragAndDrop() {
482
+ const mapEl = map.getContainer()
483
+
484
+ // Required so browser allows drops
485
+ mapEl.addEventListener('dragover', (e) => {
486
+ e.preventDefault()
487
+ mapEl.classList.add('map-drag-hover')
488
+ })
489
+
490
+ mapEl.addEventListener('dragleave', () => {
491
+ mapEl.classList.remove('map-drag-hover')
492
+ })
493
+
494
+ mapEl.addEventListener('drop', async (e) => {
495
+ e.preventDefault()
496
+ mapEl.classList.remove('map-drag-hover')
497
+
498
+ const file = e.dataTransfer.files[0]
499
+ if (!file) return
500
+
501
+ if (!file.name.endsWith('.geojson') && !file.name.endsWith('.json')) {
502
+ alert('Please drop a valid GeoJSON file.')
503
+ return
504
+ }
505
+
506
+ const text = await file.text()
507
+
508
+ let geojson
509
+ try {
510
+ geojson = JSON.parse(text)
511
+ } catch (err) {
512
+ alert('Invalid GeoJSON file.')
513
+ return
514
+ }
515
+
516
+ importGeoJSON(geojson)
517
+ })
518
+ }
519
+
520
+ function importGeoJSON(geojson) {
521
+ if (!geojson || !geojson.features) {
522
+ alert('Invalid GeoJSON structure.')
523
+ return
524
+ }
525
+
526
+ L.geoJSON(geojson, {
527
+ onEachFeature: (feature, layer) => {
528
+ layer.feature = feature
529
+ layer.onclick = () => openRegionModal(layer)
530
+ drawnItems.addLayer(layer)
531
+ }
532
+ })
533
+ saveRegions()
534
+ }
535
+
536
+ async function saveRegions() {
537
+ const geojson = drawnItems.toGeoJSON()
538
+ let data = JSON.stringify(geojson)
539
+
540
+ const res = await fetch('/signalk/chart-tiles/cache/regions', {
541
+ method: 'POST',
542
+ headers: { 'Content-Type': 'application/json' },
543
+ body: data
544
+ })
545
+
546
+ if (!res.ok) {
547
+ console.error('Failed to save regions')
548
+ return
549
+ }
550
+ updateRegionButtons()
551
+ }
552
+
553
+ async function loadRegions() {
554
+ const res = await fetch('/signalk/chart-tiles/cache/regions')
555
+ if (!res.ok) {
556
+ console.error('Failed to load saved drawings')
557
+ return
558
+ }
559
+ const geojson = await res.json()
560
+ drawnItems.clearLayers()
561
+ L.geoJSON(geojson, {
562
+ style: {
563
+ color: '#97009c'
564
+ },
565
+ onEachFeature: (feature, layer) => {
566
+ layer.on('click', () => {
567
+ if (isDeleting) return
568
+ openRegionModal(layer)
569
+ })
570
+ drawnItems.addLayer(layer)
571
+ }
572
+ })
573
+ }
574
+
575
+ function openRegionModal(layer) {
576
+ activeRegionLayer = layer
577
+
578
+ const p = layer.feature.properties
579
+
580
+ document.getElementById('regionName').value = p.name || p.Name || ''
581
+ const regionModal = document.getElementById('regionModal')
582
+ regionModal.classList.remove('hidden')
583
+
584
+ updateRegionButtons()
585
+ }
586
+
587
+ function closeRegionModal() {
588
+ const regionModal = document.getElementById('regionModal')
589
+ regionModal.classList.add('hidden')
590
+ activeRegionLayer = null
591
+ }
592
+
593
+ function isValidRegionName(name) {
594
+ return /^[\p{L}0-9_-]+$/u.test(name)
595
+ }
596
+
597
+ function updateRegionButtons() {
598
+ const regionInput = document.getElementById('regionName')
599
+ const saveRegionBtn = document.getElementById('saveRegion')
600
+ // First validate the value in the textbox to enable/disabl the save button
601
+ if (isValidRegionName(regionInput.value.trim())) {
602
+ regionInput.style.borderColor = ''
603
+ saveRegionBtn.disabled = false
604
+ } else {
605
+ regionInput.style.borderColor = 'red'
606
+ saveRegionBtn.disabled = true
607
+ }
608
+
609
+ const p = activeRegionLayer.feature.properties
610
+ const value = p.name || p.Name || ''
611
+ const createBtn = document.getElementById('createJobButton')
612
+ const deleteBtn = document.getElementById('deleteButton')
613
+ if (isValidRegionName(value)) {
614
+ createBtn.disabled = false
615
+ deleteBtn.disabled = false
616
+ } else {
617
+ createBtn.disabled = true
618
+ deleteBtn.disabled = true
619
+ }
620
+ }
Binary file