waibu-maps 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/bajo/.alias +1 -0
- package/bajo/config.json +10 -0
- package/bajoI18N/resource/en-US.json +17 -0
- package/bajoI18N/resource/id.json +19 -0
- package/package.json +34 -0
- package/waibuBootstrap/theme/component/factory/control-attribution.js +29 -0
- package/waibuBootstrap/theme/component/factory/control-buttons-item.js +14 -0
- package/waibuBootstrap/theme/component/factory/control-buttons.js +67 -0
- package/waibuBootstrap/theme/component/factory/control-center-position.js +28 -0
- package/waibuBootstrap/theme/component/factory/control-draw.js +43 -0
- package/waibuBootstrap/theme/component/factory/control-fullscreen.js +26 -0
- package/waibuBootstrap/theme/component/factory/control-geolocate.js +27 -0
- package/waibuBootstrap/theme/component/factory/control-group-button.js +15 -0
- package/waibuBootstrap/theme/component/factory/control-group-menu.js +45 -0
- package/waibuBootstrap/theme/component/factory/control-group.js +94 -0
- package/waibuBootstrap/theme/component/factory/control-image.js +43 -0
- package/waibuBootstrap/theme/component/factory/control-loader.js +27 -0
- package/waibuBootstrap/theme/component/factory/control-logo.js +50 -0
- package/waibuBootstrap/theme/component/factory/control-mouse-pos.js +78 -0
- package/waibuBootstrap/theme/component/factory/control-navigation.js +29 -0
- package/waibuBootstrap/theme/component/factory/control-ruler.js +47 -0
- package/waibuBootstrap/theme/component/factory/control-scale.js +27 -0
- package/waibuBootstrap/theme/component/factory/control-search.js +159 -0
- package/waibuBootstrap/theme/component/factory/control-zbp.js +70 -0
- package/waibuBootstrap/theme/component/factory/control.js +42 -0
- package/waibuBootstrap/theme/component/factory/layer-geojson.js +103 -0
- package/waibuBootstrap/theme/component/factory/layer-html-cluster.js +124 -0
- package/waibuBootstrap/theme/component/factory/map/options.js +44 -0
- package/waibuBootstrap/theme/component/factory/map.js +139 -0
- package/waibuBootstrap/theme/component/factory/script.js +20 -0
- package/waibuBootstrap/theme/component/factory/template.js +10 -0
- package/waibuBootstrap/theme/component/wmaps-base.js +40 -0
- package/waibuMpa/partial/menu.html +10 -0
- package/waibuMpa/route/default-style.js +13 -0
- package/waibuMpa/route/wmaps.js +13 -0
- package/waibuMpa/template/default-style.json +20 -0
- package/waibuMpa/template/wmaps.js +320 -0
- package/waibuStatic/asset/css/control-buttons.css +3 -0
- package/waibuStatic/asset/css/control-center-position.css +17 -0
- package/waibuStatic/asset/css/control-loader.css +7 -0
- package/waibuStatic/asset/css/control-mouse-position.css +21 -0
- package/waibuStatic/asset/css/control-ruler.css +12 -0
- package/waibuStatic/asset/css/control-search.css +25 -0
- package/waibuStatic/asset/css/wmaps.css +150 -0
- package/waibuStatic/asset/font/noto_sans_regular.pbf +0 -0
- package/waibuStatic/asset/image/center.svg +25 -0
- package/waibuStatic/asset/image/ruler.svg +4 -0
- package/waibuStatic/asset/js/control-buttons.js +98 -0
- package/waibuStatic/asset/js/control-center-position.js +19 -0
- package/waibuStatic/asset/js/control-mouse-position.js +47 -0
- package/waibuStatic/asset/js/control-ruler.js +204 -0
- package/waibuStatic/asset/js/donut-chart.js +58 -0
- package/waibuStatic/asset/js/lib/worker-timers.js +243 -0
- package/waibuStatic/virtual.json +7 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const opts = {
|
|
2
|
+
boolTrue: ['antialias', 'hash', 'maplibreLogo'],
|
|
3
|
+
boolFalse: ['noBoxZoom', 'noDoubleClickZoom', 'noDoubleClickZoom', 'noDragPan', 'noDragRotate',
|
|
4
|
+
'noInteractive', 'noKeyboard', 'noPitchWithRotate', 'noRenderWorldCopies', 'noScrollZoom',
|
|
5
|
+
'noTouchPitch', 'noTouchZoomRotate', 'noTrackResize', 'noValidateStyle'],
|
|
6
|
+
number: ['zoom', 'bearing', 'bearingSnap', 'clickTolerance', 'fadeDuration', 'maxPitch',
|
|
7
|
+
'maxZoom', 'minPitch', 'minZoom', 'pitch'],
|
|
8
|
+
array: ['center', 'bounds', 'maxBounds'],
|
|
9
|
+
string: ['basemap', 'attribution']
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function options (params = {}) {
|
|
13
|
+
const { camelCase, isString } = this.plugin.app.bajo.lib._
|
|
14
|
+
const { attrToArray } = this.plugin.app.waibuMpa
|
|
15
|
+
const { routePath } = this.plugin.app.waibu
|
|
16
|
+
const mapOpts = this.plugin.app.waibuMaps.getConfig().mapOptions
|
|
17
|
+
mapOpts.container = params.attr.id
|
|
18
|
+
const { $ } = this.component
|
|
19
|
+
for (const key in params.attr) {
|
|
20
|
+
const val = params.attr[key]
|
|
21
|
+
if (val === true) {
|
|
22
|
+
if (opts.boolTrue.includes(key)) mapOpts[key] = true
|
|
23
|
+
if (opts.boolFalse.includes(key)) mapOpts[camelCase(key.slice(2))] = false
|
|
24
|
+
} else {
|
|
25
|
+
if (opts.number.includes(key)) mapOpts[key] = Number(val)
|
|
26
|
+
else if (opts.array.includes(key)) mapOpts[key] = attrToArray(val).map(v => Number(v))
|
|
27
|
+
else if (opts.string.includes(key)) mapOpts[key] = val
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (params.attr.noBasemap) {
|
|
31
|
+
delete mapOpts.style
|
|
32
|
+
} else {
|
|
33
|
+
if (isString(params.attr.basemap)) mapOpts.style = params.attr.basemap
|
|
34
|
+
else mapOpts.style = 'waibuMaps:/default-style.json'
|
|
35
|
+
mapOpts.style = routePath(mapOpts.style)
|
|
36
|
+
}
|
|
37
|
+
mapOpts.attributionControl = true
|
|
38
|
+
$(`<div>${params.html}</div>`).find('script[block="control"]:contains(\'AttributionControl\')').each(function () {
|
|
39
|
+
mapOpts.attributionControl = false
|
|
40
|
+
})
|
|
41
|
+
return mapOpts
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default options
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import wmapsBase from '../wmaps-base.js'
|
|
2
|
+
|
|
3
|
+
import options from './map/options.js'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
const loadResource = `async loadResource (src) {
|
|
7
|
+
const resp = await fetch(src)
|
|
8
|
+
if (!resp.ok) throw new Error('Can\\'t load resource: ' + src)
|
|
9
|
+
return resp.json()
|
|
10
|
+
}`
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
async function map () {
|
|
14
|
+
const WmapsBase = await wmapsBase.call(this)
|
|
15
|
+
|
|
16
|
+
return class WmapsMap extends WmapsBase {
|
|
17
|
+
constructor (options) {
|
|
18
|
+
super(options)
|
|
19
|
+
this.readBlock()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async build () {
|
|
23
|
+
const { generateId } = this.plugin.app.bajo
|
|
24
|
+
const { without, omit, isString } = this.plugin.app.bajo.lib._
|
|
25
|
+
const { jsonStringify } = this.plugin.app.waibuMpa
|
|
26
|
+
const { $ } = this.component
|
|
27
|
+
|
|
28
|
+
this.params.attr.id = 'map' + (isString(this.params.attr.id) ? this.params.attr.id : generateId('alpha'))
|
|
29
|
+
this.params.tag = 'div'
|
|
30
|
+
this.params.attr['x-data'] = this.params.attr.id
|
|
31
|
+
this.params.attr['@keyup'] = 'onKeyup'
|
|
32
|
+
const mapOptions = await options.call(this, this.params)
|
|
33
|
+
this.block.reactive.push(`
|
|
34
|
+
async windowLoad () {
|
|
35
|
+
const mapOpts = ${jsonStringify(mapOptions, true)}
|
|
36
|
+
${this.block.mapOptions.join('\n')}
|
|
37
|
+
await this.run(new maplibregl.Map(mapOpts))
|
|
38
|
+
}
|
|
39
|
+
`)
|
|
40
|
+
this.params.attr['@load.window'] = 'await windowLoad()'
|
|
41
|
+
this.params.append = `<script>
|
|
42
|
+
document.addEventListener('alpine:init', () => {
|
|
43
|
+
Alpine.data('${this.params.attr.id}', () => {
|
|
44
|
+
let map
|
|
45
|
+
let wmaps
|
|
46
|
+
${this.block.nonReactive.join('\n')}
|
|
47
|
+
return {
|
|
48
|
+
init () {
|
|
49
|
+
${this.block.dataInit.join('\n')}
|
|
50
|
+
this.$watch('$store.wmpa.reqAborted', val => {
|
|
51
|
+
if (!val) return
|
|
52
|
+
const text = _.get(wmpa, 'fetchingApi.' + val + '.status')
|
|
53
|
+
if (text.startsWith('abort:')) {
|
|
54
|
+
const [, msg] = text.split(':')
|
|
55
|
+
wbs.notify(msg, { type: 'warning' }).then()
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
${this.block.reactive.join(',\n')},
|
|
60
|
+
async onMapLoad (evt) {
|
|
61
|
+
${this.block.mapLoad.join('\n')}
|
|
62
|
+
this.onMapStyle()
|
|
63
|
+
},
|
|
64
|
+
async onMapStyle () {
|
|
65
|
+
${this.block.mapStyle.join('\n')}
|
|
66
|
+
},
|
|
67
|
+
async onMissingImage (evt) {
|
|
68
|
+
${this.block.missingImage.join('\n')}
|
|
69
|
+
},
|
|
70
|
+
onLayerVisibility (layerId, shown) {
|
|
71
|
+
if (!shown) {
|
|
72
|
+
if (wmaps.popup) {
|
|
73
|
+
const el = wmaps.popup.getElement()
|
|
74
|
+
if (el && el.classList.contains('popup-layer-' + layerId)) wmaps.popup.remove()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
${this.block.layerVisibility.join('\n')}
|
|
78
|
+
},
|
|
79
|
+
async onKeyup (evt) {
|
|
80
|
+
if (evt.key === 'Escape') {
|
|
81
|
+
if (wmaps.popup) wmaps.popup.remove()
|
|
82
|
+
}
|
|
83
|
+
${this.block.keyup.join('\n')}
|
|
84
|
+
},
|
|
85
|
+
async run (instance) {
|
|
86
|
+
map = instance
|
|
87
|
+
wmaps = new WaibuMaps(instance, this)
|
|
88
|
+
map.on('moveend', evt => {
|
|
89
|
+
Alpine.store('map').center = evt.target.getCenter().toArray()
|
|
90
|
+
Alpine.store('map').zoom = evt.target.getZoom()
|
|
91
|
+
Alpine.store('map').bearing = evt.target.getBearing()
|
|
92
|
+
Alpine.store('map').pitch = evt.target.getPitch()
|
|
93
|
+
})
|
|
94
|
+
${this.block.mapExtend.join('\n')}
|
|
95
|
+
${this.block.control.join('\n')}
|
|
96
|
+
${this.block.run.join('\n')}
|
|
97
|
+
map.on('styledataloading', () => {
|
|
98
|
+
map.once('styledata', this.onMapStyle.bind(this))
|
|
99
|
+
})
|
|
100
|
+
map.on('styleimagemissing', this.onMissingImage.bind(this))
|
|
101
|
+
map.on('load', this.onMapLoad.bind(this))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
${this.block.init.join('\n')}
|
|
106
|
+
})
|
|
107
|
+
document.addEventListener('alpine:initializing', () => {
|
|
108
|
+
const props = {
|
|
109
|
+
id: '${this.params.attr.id}',
|
|
110
|
+
degree: Alpine.$persist('DMS').as('mapDegree'),
|
|
111
|
+
measure: Alpine.$persist('nautical').as('mapMeasure'),
|
|
112
|
+
zoomScrollCenter: Alpine.$persist(false).as('mapZoomScrollCenter'),
|
|
113
|
+
noMapRotate: Alpine.$persist(false).as('mapNoMapRotate'),
|
|
114
|
+
center: Alpine.$persist(null).as('mapCenter'),
|
|
115
|
+
zoom: Alpine.$persist(null).as('mapZoom'),
|
|
116
|
+
bearing: Alpine.$persist(null).as('mapBearing'),
|
|
117
|
+
pitch: Alpine.$persist(null).as('mapPitch')
|
|
118
|
+
}
|
|
119
|
+
for (const ctrl of ${jsonStringify(WmapsBase.controls, true)}) {
|
|
120
|
+
const name = 'ctrl' + wmpa.pascalCase(ctrl)
|
|
121
|
+
props[name] = Alpine.$persist(true).as('map' + _.upperFirst(name))
|
|
122
|
+
}
|
|
123
|
+
Alpine.store('map', props)
|
|
124
|
+
${this.block.initializing.join('\n')}
|
|
125
|
+
})
|
|
126
|
+
</script>`
|
|
127
|
+
const html = []
|
|
128
|
+
$(`<div>${this.params.html}</div>`).find('.childmap').each(function () {
|
|
129
|
+
html.push($(this).prop('outerHTML'))
|
|
130
|
+
})
|
|
131
|
+
this.params.html = html.join('\n')
|
|
132
|
+
const keys = without(Object.keys(mapOptions), 'style')
|
|
133
|
+
const omitted = ['noBasemap', ...keys]
|
|
134
|
+
this.params.attr = omit(this.params.attr, omitted)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import wmapsBase from '../wmaps-base.js'
|
|
2
|
+
|
|
3
|
+
async function script () {
|
|
4
|
+
const WmapsBase = await wmapsBase.call(this)
|
|
5
|
+
|
|
6
|
+
return class WmapsScript extends WmapsBase {
|
|
7
|
+
constructor (options) {
|
|
8
|
+
super(options)
|
|
9
|
+
this.params.noTag = true
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async build () {
|
|
13
|
+
const type = WmapsBase.blockTypes.includes(this.params.attr.type) ? this.params.attr.type : 'run'
|
|
14
|
+
this.block[type].push(this.params.html)
|
|
15
|
+
this.params.html = this.writeBlock()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default script
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
async function wmapsBase () {
|
|
2
|
+
return class WmapsBase extends this.baseFactory {
|
|
3
|
+
static scripts = [...super.scripts,
|
|
4
|
+
'waibuMaps.asset:/js/lib/worker-timers.js',
|
|
5
|
+
'waibuMaps.virtual:/maplibre/maplibre-gl.js',
|
|
6
|
+
'waibuMaps:/wmaps.js'
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
static css = [...super.css,
|
|
10
|
+
'waibuMaps.virtual:/maplibre/maplibre-gl.css',
|
|
11
|
+
'waibuMaps.asset:/css/wmaps.css'
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
constructor (options) {
|
|
15
|
+
super(options)
|
|
16
|
+
this.blockTypes = [...this.blockTypes,
|
|
17
|
+
'mapLoad', 'control', 'mapOptions', 'mapStyle', 'layerVisibility', 'missingImage',
|
|
18
|
+
'mapExtend'
|
|
19
|
+
]
|
|
20
|
+
this.init()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static controls = ['csrc', 'navigation-control', 'crlr', 'scale-control', 'attribution-control',
|
|
24
|
+
'fullscreen-control', 'geolocate-control', 'czbp', 'cmp']
|
|
25
|
+
|
|
26
|
+
async getWmapsTemplate (html, type, defEmpty = '') {
|
|
27
|
+
const { trim, isEmpty } = this.plugin.app.bajo.lib._
|
|
28
|
+
const { minify } = this.plugin.app.waibuMpa
|
|
29
|
+
const { $ } = this.component
|
|
30
|
+
let tpl = trim($(`<div>${html}</div>`).find(`wmaps-template[type="${type}"]`).prop('innerHTML'))
|
|
31
|
+
if (isEmpty(tpl)) {
|
|
32
|
+
if (defEmpty === 'popup') tpl = '<div class="px-3 py-2">{%= name %}</div>'
|
|
33
|
+
else tpl = defEmpty
|
|
34
|
+
}
|
|
35
|
+
return await minify(tpl)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default wmapsBase
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<c:div dim="height:100" flex="align-items:center justify-content:center" data-bs-toggle="dropdown" data-bs-auto-close="outside">
|
|
2
|
+
{% if (image !== '') { %}
|
|
3
|
+
<c:img src="{%= image %}" />
|
|
4
|
+
{% } else { %}
|
|
5
|
+
<c:icon oname="{%= icon %}" @click="{%= click %}" />
|
|
6
|
+
{% } %}
|
|
7
|
+
</c:div>
|
|
8
|
+
<c:div class="dropdown-menu">
|
|
9
|
+
{%= html %}
|
|
10
|
+
</c:div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const glyphUrl = 'waibuMaps.asset:/font/noto_sans_regular.pbf?s={fontstack}&r={range}'
|
|
2
|
+
|
|
3
|
+
const wmaps = {
|
|
4
|
+
url: '/default-style.json',
|
|
5
|
+
method: 'GET',
|
|
6
|
+
handler: async function (req, reply) {
|
|
7
|
+
const { routePath } = this.app.waibu
|
|
8
|
+
const glyphs = routePath(glyphUrl)
|
|
9
|
+
return reply.view('waibuMaps.template:/default-style.json', { glyphs })
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default wmaps
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { glyphUrl } from './default-style.js'
|
|
2
|
+
|
|
3
|
+
const wmaps = {
|
|
4
|
+
url: '/wmaps.js',
|
|
5
|
+
method: 'GET',
|
|
6
|
+
handler: async function (req, reply) {
|
|
7
|
+
const { routePath } = this.app.waibu
|
|
8
|
+
const glyphs = routePath(glyphUrl)
|
|
9
|
+
return reply.view('waibuMaps.template:/wmaps.js', { glyphs })
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default wmaps
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 8,
|
|
3
|
+
"glyphs": "<%= glyphs %>",
|
|
4
|
+
"sources": {
|
|
5
|
+
"osm": {
|
|
6
|
+
"type": "raster",
|
|
7
|
+
"tiles": ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"],
|
|
8
|
+
"tileSize": 256,
|
|
9
|
+
"attribution": "© OpenStreetMap Contributors",
|
|
10
|
+
"maxzoom": 19
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"layers": [
|
|
14
|
+
{
|
|
15
|
+
"id": "osm",
|
|
16
|
+
"type": "raster",
|
|
17
|
+
"source": "osm"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/* global maplibregl, geolib, _, wmpa, WorkerTimers, turf, Alpine */
|
|
2
|
+
|
|
3
|
+
class WaibuMaps { // eslint-disable-line no-unused-vars
|
|
4
|
+
constructor (map, scope) {
|
|
5
|
+
this.map = map
|
|
6
|
+
this.scope = scope
|
|
7
|
+
this.markers = {}
|
|
8
|
+
this.markersOnScreen = {}
|
|
9
|
+
this.popup = null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async handleClusterClick (layerId, clusterId = 'cluster_id') {
|
|
13
|
+
this.handlePointer(layerId)
|
|
14
|
+
this.map.on('click', layerId, async (e) => {
|
|
15
|
+
const features = this.map.queryRenderedFeatures(e.point, {
|
|
16
|
+
layers: [layerId]
|
|
17
|
+
})
|
|
18
|
+
const id = features[0].properties[clusterId]
|
|
19
|
+
const layer = this.map.getLayer(layerId)
|
|
20
|
+
const zoom = await this.map.getSource(layer.source).getClusterExpansionZoom(id)
|
|
21
|
+
this.map.easeTo({
|
|
22
|
+
center: features[0].geometry.coordinates,
|
|
23
|
+
zoom
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async createPopupHtml ({ props, handler, coordinates, layerId }, evt) {
|
|
29
|
+
let html = _.isString(handler) ? handler : undefined
|
|
30
|
+
if (_.isFunction(handler)) html = await handler.call(this, { props, coordinates, layerId }, evt)
|
|
31
|
+
if (!html) html = props.name ?? props.title ?? props.description ?? ''
|
|
32
|
+
return html
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getEventCoordinates (evt) {
|
|
36
|
+
let coordinates = evt.features[0].geometry.coordinates.slice()
|
|
37
|
+
if (_.isArray(coordinates[0])) {
|
|
38
|
+
const centroid = turf.centroid(evt.features[0])
|
|
39
|
+
coordinates = centroid.geometry.coordinates
|
|
40
|
+
}
|
|
41
|
+
while (Math.abs(evt.lngLat.lng - coordinates[0]) > 180) {
|
|
42
|
+
coordinates[0] += evt.lngLat.lng > coordinates[0] ? 360 : -360
|
|
43
|
+
}
|
|
44
|
+
return coordinates
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async extractPopup ({ evt, layerId, handler, props, coordinates }) {
|
|
48
|
+
props = props ?? evt.features[0].properties
|
|
49
|
+
coordinates = coordinates ?? this.getEventCoordinates(evt)
|
|
50
|
+
const html = await this.createPopupHtml({ props, handler, coordinates, layerId }, evt)
|
|
51
|
+
return { props, coordinates, html }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
createPopup () {
|
|
55
|
+
if (!this.popup) {
|
|
56
|
+
this.popup = new maplibregl.Popup({
|
|
57
|
+
closeButton: false,
|
|
58
|
+
closeOnClick: false
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
return this.popup
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async handleNonClusterClick (layerId, handler = 'name') {
|
|
65
|
+
if (handler === true) handler = 'name'
|
|
66
|
+
this.handlePointer(layerId)
|
|
67
|
+
this.map.on('click', layerId, async evt => {
|
|
68
|
+
const { coordinates, html, props } = await this.extractPopup({ evt, layerId, handler })
|
|
69
|
+
const popup = this.createPopup()
|
|
70
|
+
popup._props = props
|
|
71
|
+
popup._coordinates = coordinates
|
|
72
|
+
popup
|
|
73
|
+
.setLngLat(coordinates)
|
|
74
|
+
.setHTML(html)
|
|
75
|
+
.addTo(this.map)
|
|
76
|
+
.addClassName('popup-layer-' + layerId)
|
|
77
|
+
.addClassName('popup-target-' + props.id + '-' + props.feed)
|
|
78
|
+
})
|
|
79
|
+
this.map.on('click', () => {
|
|
80
|
+
if (this.popup) this.popup.remove()
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
popup ({ layerId, props, html, coordinates }) {
|
|
85
|
+
return new maplibregl.Popup({ className: 'popup-layer-' + layerId + ' popup-target-' + props.id })
|
|
86
|
+
.setLngLat(coordinates)
|
|
87
|
+
.setHTML(html)
|
|
88
|
+
.addTo(this.map)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
updateClusterMarkers ({ sourceId, clusterKey = 'cluster', clusterIdKey = 'clusterId', handler }) {
|
|
92
|
+
if (!handler) return
|
|
93
|
+
const newMarkers = {}
|
|
94
|
+
const features = this.map.querySourceFeatures(sourceId)
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < features.length; i++) {
|
|
97
|
+
const coords = features[i].geometry.coordinates
|
|
98
|
+
const props = features[i].properties
|
|
99
|
+
if (!props[clusterKey]) continue
|
|
100
|
+
const id = props[clusterIdKey]
|
|
101
|
+
|
|
102
|
+
let marker = this.markers[id]
|
|
103
|
+
if (!marker) {
|
|
104
|
+
const el = handler(props)
|
|
105
|
+
marker = this.markers[id] = new maplibregl.Marker({
|
|
106
|
+
element: el
|
|
107
|
+
}).setLngLat(coords)
|
|
108
|
+
}
|
|
109
|
+
newMarkers[id] = marker
|
|
110
|
+
|
|
111
|
+
if (!this.markersOnScreen[id]) marker.addTo(this.map)
|
|
112
|
+
}
|
|
113
|
+
for (const id in this.markersOnScreen) {
|
|
114
|
+
if (!newMarkers[id]) this.markersOnScreen[id].remove()
|
|
115
|
+
}
|
|
116
|
+
this.markersOnScreen = newMarkers
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
handlePointer (layerId) {
|
|
120
|
+
this.map.on('mouseenter', layerId, () => {
|
|
121
|
+
this.map.getCanvas().style.cursor = 'pointer'
|
|
122
|
+
})
|
|
123
|
+
this.map.on('mouseleave', layerId, () => {
|
|
124
|
+
this.map.getCanvas().style.cursor = ''
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
closePopup () {
|
|
129
|
+
if (this.popup) this.popup.remove()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async loadImage (src) {
|
|
133
|
+
if (_.isString(src)) {
|
|
134
|
+
const [url, id] = src.split(';')
|
|
135
|
+
src = { url, id }
|
|
136
|
+
}
|
|
137
|
+
if (!src.id) src.id = _.last(src.url.split('?')[0].split('#')[0].split('/')).split('.')[0]
|
|
138
|
+
if (this.map.listImages().includes(src.id)) return
|
|
139
|
+
const image = await this.map.loadImage(src.url)
|
|
140
|
+
this.map.addImage(src.id, image.data)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async loadImages (sources, fetch = true) {
|
|
144
|
+
for (const src of sources) {
|
|
145
|
+
if (fetch) {
|
|
146
|
+
const data = await wmpa.fetchApi(src)
|
|
147
|
+
if (_.isEmpty(data)) continue
|
|
148
|
+
for (const d of data) {
|
|
149
|
+
this.loadImage(d)
|
|
150
|
+
}
|
|
151
|
+
} else await this.loadImage(src)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async createControl (options = {}) {
|
|
156
|
+
const ctrl = new WaibuMapsControl(options)
|
|
157
|
+
ctrl.scope = this.scope
|
|
158
|
+
if (options.builder) {
|
|
159
|
+
if (_.isArray(options.builder)) ctrl.panels = options.builder
|
|
160
|
+
else if (_.isString(options.builder)) {
|
|
161
|
+
ctrl.panels = await wmpa.createComponent(options.builder)
|
|
162
|
+
} else {
|
|
163
|
+
const fn = options.builder.bind(ctrl.scope)
|
|
164
|
+
ctrl.panels = await fn(options.params)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.map.addControl(ctrl)
|
|
168
|
+
if (options.firstCall) options.firstCall.call(ctrl.scope)
|
|
169
|
+
return ctrl
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async createControlNative (className, options = {}) {
|
|
173
|
+
const name = wmpa.pascalCase(className)
|
|
174
|
+
const ctrl = new maplibregl[name](options)
|
|
175
|
+
let type = options.classSelector
|
|
176
|
+
if (!type) {
|
|
177
|
+
const types = _.kebabCase(className).split('-')
|
|
178
|
+
types.pop()
|
|
179
|
+
type = 'maplibregl-ctrl-' + types.join('-')
|
|
180
|
+
}
|
|
181
|
+
this.map.addControl(ctrl, options.position)
|
|
182
|
+
let el = document.querySelector('#' + this.map._container.id + ' .' + type)
|
|
183
|
+
if (el) {
|
|
184
|
+
if (options.classGroup) el = el.closest('.maplibregl-ctrl-group')
|
|
185
|
+
el.setAttribute('oncontextmenu', 'return false')
|
|
186
|
+
el.setAttribute('x-data', '')
|
|
187
|
+
el.setAttribute('x-show', '$store.map.ctrl' + name)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class WaibuMapsUtil {
|
|
193
|
+
constructor () {
|
|
194
|
+
this.defSources = {
|
|
195
|
+
type: 'raster',
|
|
196
|
+
tileSize: 256,
|
|
197
|
+
maxzoom: 19,
|
|
198
|
+
attribution: 'Waibu Maps'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.defLayer = {
|
|
202
|
+
type: 'raster'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.defStyle = {
|
|
206
|
+
version: 8,
|
|
207
|
+
glyphs: '<%= glyphs %>',
|
|
208
|
+
sources: {},
|
|
209
|
+
layers: []
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
decToDms (decimal, opts = {}) {
|
|
214
|
+
if (opts === true || opts === false) opts = { isLng: opts }
|
|
215
|
+
opts.north = opts.north ?? 'N'
|
|
216
|
+
opts.south = opts.south ?? 'S'
|
|
217
|
+
opts.east = opts.east ?? 'E'
|
|
218
|
+
opts.west = opts.west ?? 'W'
|
|
219
|
+
const result = geolib.decimalToSexagesimal(decimal)
|
|
220
|
+
if (opts.isLng) return result + ' ' + (decimal >= 0 ? opts.east : opts.west)
|
|
221
|
+
else return result + ' ' + (decimal >= 0 ? opts.north : opts.south)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getSourceId (id, ext) {
|
|
225
|
+
return 's-' + _.kebabCase(id) + (_.isEmpty(ext) ? '' : ('-' + ext))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
getLayerId (id, ext = '') {
|
|
229
|
+
return 'l-' + _.kebabCase(id) + (_.isEmpty(ext) ? '' : ('-' + ext))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getSourceLayerIds (id, ext) {
|
|
233
|
+
return [this.getSourceId(id, ext), this.getLayerId(id, ext)]
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
srcAsStyle (src) {
|
|
237
|
+
if ((_.isPlainObject(src) && src.version && src.sources)) return src
|
|
238
|
+
const result = {}
|
|
239
|
+
if (_.isString(src)) {
|
|
240
|
+
const url = new URL(src)
|
|
241
|
+
const ext = _.last(url.pathname.split('.'))
|
|
242
|
+
if (ext === 'json') return src
|
|
243
|
+
const domain = _.camelCase(url.hostname)
|
|
244
|
+
const sources = {}
|
|
245
|
+
sources[domain] = _.merge({}, this.defSources, { tiles: [src], attribution: url.searchParams.get('attribution') ?? '' })
|
|
246
|
+
const layers = [_.merge({}, this.defLayer, { id: domain, source: domain })]
|
|
247
|
+
_.merge(result, this.defStyle, { sources, layers })
|
|
248
|
+
} else {
|
|
249
|
+
if (src.type === 'VECTOR') return src.url
|
|
250
|
+
if (!['RASTER'].includes(src.type)) throw new Error('Invalid source type')
|
|
251
|
+
const sources = {}
|
|
252
|
+
const sourceId = this.getSourceId(src.code)
|
|
253
|
+
const layerId = this.getLayerId(src.code)
|
|
254
|
+
sources[sourceId] = _.merge({}, this.defSources, { name: src.name, provider: src.provider, tiles: [src.url], attribution: src.attribution, minzoom: src.minZoom ?? 1, maxzoom: src.maxZoom ?? 19 })
|
|
255
|
+
const layers = [_.merge({}, this.defLayer, { id: layerId, source: sourceId })]
|
|
256
|
+
_.merge(result, this.defStyle, { sources, layers })
|
|
257
|
+
}
|
|
258
|
+
return result
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
class WaibuMapsControl { // eslint-disable-line no-unused-vars
|
|
263
|
+
constructor (options = {}) {
|
|
264
|
+
this.position = options.position ?? 'top-right'
|
|
265
|
+
this.class = options.class
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
createControl () {
|
|
269
|
+
this.container = document.createElement('div')
|
|
270
|
+
// this.container.setAttribute('oncontextmenu', 'return false')
|
|
271
|
+
this.container.classList.add('maplibregl-ctrl', 'maplibregl-ctrl-wmaps')
|
|
272
|
+
if (this.class) {
|
|
273
|
+
const classes = _.without(this.class.split(' '), '', null, undefined)
|
|
274
|
+
if (classes.length > 0) {
|
|
275
|
+
this.container.classList.add(...classes)
|
|
276
|
+
const ctrlName = 'ctrl' + wmpa.pascalCase(classes[0])
|
|
277
|
+
if (_.has(Alpine.store('map'), ctrlName)) {
|
|
278
|
+
this.container.setAttribute('x-show', 'Alpine.store(\'map\')[\'' + ctrlName + '\']') // first class will be used as control switch class
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (this.panels) {
|
|
283
|
+
if (!_.isArray(this.panels)) this.panels = [this.panels]
|
|
284
|
+
if (this.panels.length > 0) {
|
|
285
|
+
for (const panel of this.panels) {
|
|
286
|
+
this.container.appendChild(panel)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
onAdd (map) {
|
|
293
|
+
this.map = map
|
|
294
|
+
this.createControl()
|
|
295
|
+
return this.container
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
onRemove () {
|
|
299
|
+
this.container.parentNode.removeChild(this.container)
|
|
300
|
+
this.map = undefined
|
|
301
|
+
this.scope = undefined
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getDefaultPosition () {
|
|
305
|
+
return this.position
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const wmapsUtil = new WaibuMapsUtil() // eslint-disable-line no-unused-vars
|
|
310
|
+
|
|
311
|
+
// patch
|
|
312
|
+
window.setInterval = WorkerTimers.setInterval
|
|
313
|
+
window.clearInterval = WorkerTimers.clearInterval
|
|
314
|
+
window.setTimeout = WorkerTimers.setTimeout
|
|
315
|
+
window.clearTimeout = WorkerTimers.clearTimeout
|
|
316
|
+
|
|
317
|
+
const _warn = console.warn
|
|
318
|
+
console.warn = (...items) => {
|
|
319
|
+
if (!(items[0] ?? '').includes('could not be loaded. Please make sure you have added the image with')) _warn(...items)
|
|
320
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.maplibregl-ctrl-center {
|
|
2
|
+
z-index: 1000;
|
|
3
|
+
position: absolute;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
top: 0;
|
|
7
|
+
pointer-events: none;
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
align-items: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.maplibregl-ctrl-center div {
|
|
14
|
+
width: 50px;
|
|
15
|
+
height: 50px;
|
|
16
|
+
background-image: url(../image/center.svg)
|
|
17
|
+
}
|