Skip to content

Commit 7f7bae2

Browse files
committed
geojson layer wrapper class
1 parent 00f8e3e commit 7f7bae2

1 file changed

Lines changed: 155 additions & 154 deletions

File tree

app/javascript/maplibre/layers/geojson.js

Lines changed: 155 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,128 @@ import { buffer } from "@turf/buffer"
33
import { lineString } from "@turf/helpers"
44
import { length } from "@turf/length"
55
import { draw, select } from 'maplibre/edit'
6-
import { getFeature, getFeatures, layers } from 'maplibre/layers/layers'
6+
import { getFeature, layers } from 'maplibre/layers/layers'
7+
import { Layer } from 'maplibre/layers/layer'
78
import { map, mapProperties, removeStyleLayers } from 'maplibre/map'
89
import { defaultLineWidth, featureColor, initializeClusterStyles, initializeViewStyles, labelFont, setSource, styles } from 'maplibre/styles/styles'
910

10-
export function initializeGeoJSONLayers(id = null) {
11-
// console.log('Initializing geojson layers')
12-
let initLayers = layers.filter(l => l.type === 'geojson' && l.show !== false)
13-
if (id) { initLayers = initLayers.filter(l => l.id === id) }
11+
// Instance cache for GeoJSONLayer objects
12+
const instances = new Map()
1413

15-
initLayers.forEach((layer) => {
16-
initializeViewStyles('geojson-source-' + layer.id, !!layer.heatmap)
17-
if (!!layer.cluster) { initializeClusterStyles('geojson-source-' + layer.id, null) }
14+
function getInstance(id) {
15+
if (!instances.has(id)) {
16+
instances.set(id, new GeoJSONLayer(layers.find(l => l.id === id)))
17+
}
18+
return instances.get(id)
19+
}
1820

19-
initializeKmMarkerStyles(layer.id)
20-
renderGeoJSONLayer(layer.id)
21-
})
21+
export class GeoJSONLayer extends Layer {
22+
get kmMarkerSourceId() {
23+
return `km-marker-source-${this.id}`
24+
}
2225

23-
map.fire('geojson.load', { detail: { message: 'geojson source + styles loaded' } })
24-
}
26+
initialize() {
27+
initializeViewStyles(this.sourceId, !!this.layer.heatmap)
28+
if (this.layer.cluster) { initializeClusterStyles(this.sourceId, null) }
29+
this.initializeKmMarkerStyles()
30+
this.render()
31+
}
2532

26-
export function renderGeoJSONLayers(resetDraw = true) {
27-
layers.filter(l => l.type === 'geojson').forEach((layer) => {
28-
renderGeoJSONLayer(layer.id, resetDraw)
29-
})
30-
}
33+
render(resetDraw = true) {
34+
console.log("Redraw: Setting source data for geojson layer", this.layer)
35+
this.ensureFeaturePropertyIds()
36+
this.renderKmMarkers()
37+
const extrusionLines = this.renderExtrusionLines()
38+
const geojson = { type: 'FeatureCollection', features: this.layer.geojson.features.concat(extrusionLines) }
39+
map.getSource(this.sourceId).setData(geojson, false)
40+
this.resetDrawFeatures(resetDraw)
41+
}
3142

32-
export function renderGeoJSONLayer(id, resetDraw = true) {
33-
let layer = layers.find(l => l.id === id)
34-
console.log("Redraw: Setting source data for geojson layer", layer)
35-
36-
// this + `promoteId: 'id'` is a workaround for the maplibre limitation:
37-
// https://github.com/mapbox/mapbox-gl-js/issues/2716
38-
// because to highlight a feature we need the id,
39-
// and in the style layers it only accepts mumeric ids in the id field initially
40-
// TODO: only needed once, not each render
41-
layer.geojson.features.forEach((feature) => { feature.properties.id = feature.id })
42-
renderKmMarkersLayer(id)
43-
// - For LineStrings with a 'fill-extrusion-height', add a polygon to render extrusion
44-
let extrusionLines = renderExtrusionLines()
45-
let geojson = { type: 'FeatureCollection', features: layer.geojson.features.concat(extrusionLines) }
46-
47-
map.getSource(layer.type + '-source-' + layer.id).setData(geojson, false)
48-
49-
// draw has its own style layers based on editStyles
50-
if (draw) {
51-
if (resetDraw) {
43+
renderKmMarkers() {
44+
let kmMarkerFeatures = []
45+
this.layer.geojson.features.filter(feature => (feature.geometry.type === 'LineString' &&
46+
feature.properties['show-km-markers'] &&
47+
feature.geometry.coordinates.length >= 2)).forEach((f, index) => {
48+
49+
const line = lineString(f.geometry.coordinates)
50+
const distance = length(line, { units: 'kilometers' })
51+
let interval = 1
52+
for (let i = 0; i < Math.ceil(distance) + interval; i += interval) {
53+
const point = along(line, i, { units: 'kilometers' })
54+
point.properties['marker-color'] = f.properties['stroke'] || featureColor
55+
point.properties['marker-size'] = 11
56+
point.properties['marker-opacity'] = 1
57+
point.properties['km'] = i
58+
59+
if (i >= Math.ceil(distance)) {
60+
point.properties['marker-size'] = 14
61+
point.properties['km'] = Math.round(distance)
62+
if (Math.ceil(distance) < 100) {
63+
point.properties['km'] = Math.round(distance * 10) / 10
64+
}
65+
point.properties['km-marker-numbers-end'] = 1
66+
point.properties['sort-key'] = 2 + index
67+
}
68+
kmMarkerFeatures.push(point)
69+
}
70+
})
71+
72+
const markerFeatures = { type: 'FeatureCollection', features: kmMarkerFeatures }
73+
map.getSource(this.kmMarkerSourceId).setData(markerFeatures)
74+
}
75+
76+
initializeKmMarkerStyles() {
77+
removeStyleLayers(this.kmMarkerSourceId)
78+
this.kmMarkerStyles().forEach(style => {
79+
style = setSource(style, this.kmMarkerSourceId)
80+
map.addLayer(style)
81+
})
82+
}
83+
84+
kmMarkerStyles() {
85+
let styleLayers = []
86+
87+
styleLayers.push(makePointsLayer(2, 11))
88+
styleLayers.push(makeNumbersLayer(2, 11))
89+
styleLayers.push(makePointsLayer(5, 10, 11))
90+
styleLayers.push(makeNumbersLayer(5, 10, 11))
91+
styleLayers.push(makePointsLayer(10, 9, 10))
92+
styleLayers.push(makeNumbersLayer(10, 9, 10))
93+
styleLayers.push(makePointsLayer(25, 8, 9))
94+
styleLayers.push(makeNumbersLayer(25, 8, 9))
95+
styleLayers.push(makePointsLayer(50, 7, 8))
96+
styleLayers.push(makeNumbersLayer(50, 7, 8))
97+
styleLayers.push(makePointsLayer(100, 5, 7))
98+
styleLayers.push(makeNumbersLayer(100, 5, 7))
99+
100+
const base = { ...styles()['points-layer'] }
101+
styleLayers.push({
102+
...base,
103+
id: `km-marker-points-end`,
104+
filter: ["==", ["get", "km-marker-numbers-end"], 1]
105+
})
106+
styleLayers.push({
107+
id: `km-marker-numbers-end`,
108+
type: 'symbol',
109+
filter: ["==", ["get", "km-marker-numbers-end"], 1],
110+
layout: {
111+
'text-allow-overlap': true,
112+
'text-field': ['get', 'km'],
113+
'text-size': 12,
114+
'text-font': labelFont,
115+
'text-justify': 'center',
116+
'text-anchor': 'center'
117+
},
118+
paint: {
119+
'text-color': '#ffffff'
120+
}
121+
})
122+
123+
return styleLayers
124+
}
125+
126+
resetDrawFeatures(resetDraw) {
127+
if (draw && resetDraw) {
52128
// This has a performance drawback over draw.set(), but some feature
53129
// properties don't get updated otherwise
54130
// API: https://github.com/mapbox/mapbox-gl-draw/blob/main/docs/API.md
@@ -59,52 +135,58 @@ export function renderGeoJSONLayer(id, resetDraw = true) {
59135
let feature = getFeature(featureId, "geojson")
60136
if (feature) {
61137
draw.add(feature)
62-
// if we're in edit mode, re-select feature
63138
select(feature)
64139
}
65140
})
66141
}
67142
}
143+
144+
renderExtrusionLines() {
145+
if (mapProperties.terrain) { return [] }
146+
147+
let extrusionLines = this.layer.geojson.features.filter(feature => (
148+
feature.geometry.type === 'LineString' &&
149+
feature.properties['fill-extrusion-height'] &&
150+
feature.geometry.coordinates.length !== 1
151+
))
152+
153+
return extrusionLines.map(feature => {
154+
const width = feature.properties['fill-extrusion-width'] || feature.properties['stroke-width'] || defaultLineWidth
155+
const extrusionLine = buffer(feature, width, { units: 'meters' })
156+
extrusionLine.properties = { ...feature.properties }
157+
if (!extrusionLine.properties['fill-extrusion-color'] && feature.properties.stroke) {
158+
extrusionLine.properties['fill-extrusion-color'] = feature.properties.stroke
159+
}
160+
extrusionLine.properties['stroke-width'] = 0
161+
extrusionLine.properties['stroke-opacity'] = 0
162+
extrusionLine.properties['fill-opacity'] = 0
163+
return extrusionLine
164+
})
165+
}
68166
}
69167

70-
export function renderKmMarkersLayer(id) {
71-
let layer = layers.find(l => l.id === id)
168+
// Backward-compatible wrapper exports
72169

73-
let kmMarkerFeatures = []
74-
layer.geojson.features.filter(feature => (feature.geometry.type === 'LineString' &&
75-
feature.properties['show-km-markers'] &&
76-
feature.geometry.coordinates.length >= 2)).forEach((f, index) => {
170+
export function initializeGeoJSONLayers(id = null) {
171+
instances.clear()
172+
let initLayers = layers.filter(l => l.type === 'geojson' && l.show !== false)
173+
if (id) { initLayers = initLayers.filter(l => l.id === id) }
77174

78-
const line = lineString(f.geometry.coordinates)
79-
const distance = length(line, { units: 'kilometers' })
80-
// Create markers at useful intervals
81-
let interval = 1
82-
for (let i = 0; i < Math.ceil(distance) + interval; i += interval) {
83-
// Get point at current kilometer
84-
const point = along(line, i, { units: 'kilometers' })
85-
point.properties['marker-color'] = f.properties['stroke'] || featureColor
86-
point.properties['marker-size'] = 11
87-
point.properties['marker-opacity'] = 1
88-
point.properties['km'] = i
89-
90-
if (i >= Math.ceil(distance)) {
91-
point.properties['marker-size'] = 14
92-
point.properties['km'] = Math.round(distance)
93-
if (Math.ceil(distance) < 100) {
94-
point.properties['km'] = Math.round(distance * 10) / 10
95-
}
96-
point.properties['km-marker-numbers-end'] = 1
97-
point.properties['sort-key'] = 2 + index
98-
}
99-
kmMarkerFeatures.push(point)
100-
}
175+
initLayers.forEach((layer) => {
176+
getInstance(layer.id).initialize()
101177
})
102178

103-
let markerFeatures = {
104-
type: 'FeatureCollection',
105-
features: kmMarkerFeatures
106-
}
107-
map.getSource('km-marker-source-' + id).setData(markerFeatures)
179+
map.fire('geojson.load', { detail: { message: 'geojson source + styles loaded' } })
180+
}
181+
182+
export function renderGeoJSONLayers(resetDraw = true) {
183+
layers.filter(l => l.type === 'geojson').forEach((layer) => {
184+
renderGeoJSONLayer(layer.id, resetDraw)
185+
})
186+
}
187+
188+
export function renderGeoJSONLayer(id, resetDraw = true) {
189+
getInstance(id).render(resetDraw)
108190
}
109191

110192
function makePointsLayer(divisor, minzoom, maxzoom = 24) {
@@ -139,84 +221,3 @@ function makeNumbersLayer(divisor, minzoom, maxzoom=24) {
139221
}
140222
}
141223

142-
export function kmMarkerStyles (_id) {
143-
let layers = []
144-
const base = { ...styles()['points-layer'] }
145-
146-
layers.push(makePointsLayer(2, 11))
147-
layers.push(makeNumbersLayer(2, 11))
148-
149-
layers.push(makePointsLayer(5, 10, 11))
150-
layers.push(makeNumbersLayer(5, 10, 11))
151-
152-
layers.push(makePointsLayer(10, 9, 10))
153-
layers.push(makeNumbersLayer(10, 9, 10))
154-
155-
layers.push(makePointsLayer(25, 8, 9))
156-
layers.push(makeNumbersLayer(25, 8, 9))
157-
158-
layers.push(makePointsLayer(50, 7, 8))
159-
layers.push(makeNumbersLayer(50, 7, 8))
160-
161-
layers.push(makePointsLayer(100, 5, 7))
162-
layers.push(makeNumbersLayer(100, 5, 7))
163-
164-
// end point has different style
165-
layers.push({
166-
...base,
167-
id: `km-marker-points-end`,
168-
filter: ["==", ["get", "km-marker-numbers-end"], 1]
169-
})
170-
layers.push({
171-
id: `km-marker-numbers-end`,
172-
type: 'symbol',
173-
filter: ["==", ["get", "km-marker-numbers-end"], 1],
174-
layout: {
175-
'text-allow-overlap': true,
176-
'text-field': ['get', 'km'],
177-
'text-size': 12,
178-
'text-font': labelFont,
179-
'text-justify': 'center',
180-
'text-anchor': 'center'
181-
},
182-
paint: {
183-
'text-color': '#ffffff'
184-
}
185-
})
186-
187-
return layers
188-
}
189-
190-
export function initializeKmMarkerStyles(id) {
191-
removeStyleLayers('km-marker-source-' + id)
192-
kmMarkerStyles(id).forEach(style => {
193-
style = setSource (style, 'km-marker-source-' + id)
194-
map.addLayer(style)
195-
})
196-
}
197-
198-
function renderExtrusionLines() {
199-
// Disable extrusionlines on 3D terrain, it does not work
200-
if (mapProperties.terrain) { return [] }
201-
202-
let extrusionLines = getFeatures('geojson').filter(feature => (
203-
feature.geometry.type === 'LineString' &&
204-
feature.properties['fill-extrusion-height'] &&
205-
feature.geometry.coordinates.length !== 1 // don't break line animation
206-
))
207-
208-
extrusionLines = extrusionLines.map(feature => {
209-
const width = feature.properties['fill-extrusion-width'] || feature.properties['stroke-width'] || defaultLineWidth
210-
const extrusionLine = buffer(feature, width, { units: 'meters' })
211-
// clone properties hash, else we're writing into the original feature's properties
212-
extrusionLine.properties = { ...feature.properties }
213-
if (!extrusionLine.properties['fill-extrusion-color'] && feature.properties.stroke) {
214-
extrusionLine.properties['fill-extrusion-color'] = feature.properties.stroke
215-
}
216-
extrusionLine.properties['stroke-width'] = 0
217-
extrusionLine.properties['stroke-opacity'] = 0
218-
extrusionLine.properties['fill-opacity'] = 0
219-
return extrusionLine
220-
})
221-
return extrusionLines
222-
}

0 commit comments

Comments
 (0)