Hello all!
I'm having a strange issue here with a somewhat "complex" case involving filters and mapbox gl draw.
Here's my scenario:
I've made a little gif of the problem. You can see in the first part that it all works out just fine. In the second part, I've reloaded the page and tried again in which presents the error. Not only my layers go away, but other as well.
I'm linking to imgur because github won't accept the gif with > 10mb.
"@mapbox/mapbox-gl-draw": "^1.0.4",
"@mapbox/mapbox-gl-geocoder": "^2.2.0",
"@mapbox/mapbox-gl-language": "^0.9.2",
"@turf/combine": "^5.1.5",
"mapbox-gl": "^0.43.0",
this is the layer I'm editing in the video.
export default {
'enterprises': {
'id': 'enterprises',
'group': GROUP_REGISTRY[4],
'visible': true,
'geometryType': 'MultiPolygon',
'editTool': 'polygon',
'verboseName': 'Empreendimentos',
'description': 'Empreendimentos dispon铆veis na base de dados do Geoadmin.',
'source': 'enterprises',
'source-layer': 'enterprises',
'type': 'fill',
'paint': {
'fill-color': '#FF7C00',
'fill-opacity': 0.3,
'fill-outline-color': '#FF7C00'
},
'labels': {
'layout': {
'text-max-width': 20,
'text-field': '{label}',
'text-size': 18,
'text-font': ['League Mono Regular', 'Montserrat Light']
},
'paint': {
'text-halo-width': 0.5,
'text-halo-color': '#FFFFFF',
'text-color': '#FF7C00',
'text-opacity': 1
}
},
'legend': {
'visible': true
},
'can': {
'create': {
infoView: 'CreateEnterpriseForm',
infoTitle: 'Novo Empreendimento'
},
'update': {
action: 'enterprises/update'
},
'delete': {
action: 'enterprises/delete'
},
'detail': true,
'select': true
}
}
}
this creates all my custom layers. from a layer definition. I expand this to the editable and selectable layers.
const generateLayers = function (registry) {
let layers = {}
const tenant = getTenant()
each(registry, (layer, name) => {
layers[name] = layer
const sourceLayer = layer['source-layer']
layer['source-layer'] = `${sourceLayer}-${tenant}`
if (layer.labels) {
const labelLayerName = getLabelLayerName(layer)
layers[labelLayerName] = createLabelLayer(layer)
}
if (isEditable(layer)) {
const editableLayerName = getEditableLayerName(layer)
layers[editableLayerName] = createEditableLayer(layer)
}
if (isSelectable(layer)) {
const selectionLayerName = getSelectionLayerName(layer)
layers[selectionLayerName] = createSelectionLayer(layer)
}
})
return layers
}
this is the method I use to set things up
setUpDraw (layer) {
let controls = {}
if (get(layer, 'can.create')) {
controls[layer.editTool] = true
}
if (get(layer, 'can.delete')) {
controls['trash'] = true
}
this.drawControl = new MapboxDraw({
controls: controls,
displayControlsDefault: false
})
this.editManager = new FeatureEditManager(
store,
this.map,
layer
)
this.map.addControl(this.drawControl, 'top-left')
const features = this.editManager._mergeFeatures()
this.drawControl.add(features)
if (get(layer, 'can.create')) {
this.attachDrawEvent('draw.create', bind(this.editManager.onFeatureCreated, this.editManager))
}
if (get(layer, 'can.update')) {
this.attachDrawEvent('draw.update', bind(this.editManager.onFeatureUpdated, this.editManager))
}
if (get(layer, 'can.delete')) {
this.attachDrawEvent('draw.delete', bind(this.editManager.onFeatureDeleted, this.editManager))
}
},
this is the class I use to track the CRUD operations in the layer
import map from 'lodash/map'
import get from 'lodash/get'
import filter from 'lodash/filter'
import {
coerceGeometry
} from '@/gis/utils/geometry'
import {
getEditableLayerName
} from '@/gis/layers/utils'
import {
getFeaturesVector,
getFeaturesJSON,
toFeatureCollection
} from '@/gis/utils/features'
/* global mapBus */
class FeatureEditManager {
constructor (store, mapObject, vectLayer) {
this.store = store
this.map = mapObject
this.vectLayer = vectLayer
this.jsonSource = this.map.getSource(getEditableLayerName(this.vectLayer))
}
_mergeFeatures () {
const vectFeatures = this.getVectorFeatures()
const jsonFeatures = this.getJSONFeatures()
return toFeatureCollection([
...vectFeatures,
...jsonFeatures
])
}
getVectorFeatures () {
return getFeaturesVector(this.map, this.vectLayer)
}
getJSONSource () {
return this.map.getSource(this.jsonLayer.source)
}
getJSONFeatures () {
try {
return this.jsonSource.serialize().data.features
} catch (err) {
console.error('erro ao pegar features')
return []
}
}
updateFilter () {
const features = this.getJSONFeatures()
const featFilter = features.reduce((ids, feature) => {
ids.push(feature.id)
return ids
}, ['!in', '$id'])
this.map.setFilter(this.vectLayer.id, featFilter)
}
createJSONFeature (feature) {
const newFeatures = toFeatureCollection([
...this.getJSONFeatures(),
...[feature]
])
this.jsonSource.setData(newFeatures)
}
updateJSONFeature (feature) {
const oldFeatures = filter(this.getJSONFeatures(), (f) => {
return f.id !== feature.id
})
const newFeatures = toFeatureCollection([
...oldFeatures,
feature
])
this.jsonSource.setData(newFeatures)
this.updateFilter()
}
deleteJSONFeature (feature) {
const oldFeatures = this.getJSONFeatures()
const newFeatures = toFeatureCollection([
...filter(oldFeatures, (f) => {
return f.id !== feature.id
})
])
this.jsonSource.setData(newFeatures)
this.updateFilter()
}
onFeatureCreated (e) {
const feature = e.features[0]
if (!feature) {
return
}
this.createJSONFeature(feature)
const infoView = get(this.vectLayer, 'can.create.infoView')
const infoTitle = get(this.vectLayer, 'can.create.infoTitle')
this.store.commit('maps/setFeatures', event.features)
if (infoView) {
this.store.commit('maps/setInfoView', infoView)
this.store.commit('maps/setInfoTitle', infoTitle)
this.store.commit('maps/openFeatureInfo')
}
const callback = get(this.vectLayer, 'can.create.callback')
if (callback) {
callback(event)
}
}
onFeatureUpdated (e) {
const feature = e.features[0]
if (!feature) {
return
}
this.updateJSONFeature(feature)
const coercedGeometry = coerceGeometry(this.vectLayer, feature)
const updateAction = get(this.vectLayer, 'can.update.action')
if (updateAction) {
this.store.dispatch(updateAction, {
instance: {id: feature.id},
data: {geometry: coercedGeometry}
})
}
}
onFeatureDeleted (event) {
const feature = event.features[0]
if (!feature) {
return
}
this.deleteJSONFeature(feature)
const deleteAction = get(this.vectLayer, 'can.update.action')
if (deleteAction) {
this.store.dispatch(deleteAction, {
item: {id: feature.id},
data: {map: true}
})
}
}
}
export {FeatureEditManager as default}
I was getting a message saying that the layer did not followed the specs. I've tried the same code with tegola tile server, but the issue persists.
Hi @george-silva, thanks for reporting this issue.
There are several factors that could be at play in the situation you described above. Could you please provide a minimal, self-contained example reproducing this issue? Having a live example to work with will make it much more likely that we can successfully diagnose and solve the problem.
Hi @anandthakker !
I'm trying to something like that using codepen. I'll see if I can get it done tonight. The biggest issue I see is having a similar protobuf source, but I'll see if I can use something we already have (if you have any tips or know a similar source available, it would be nice)
As promised, here is a codepen: https://codepen.io/george-silva/pen/mXWyXv
The deal is that I haven't been able to reproduce the issue in the pen.
The only main difference is that I have a setTimeout between the binding of the event and _mergeFeatures method. this was done mainly to emulate the extra control I have that adds the DrawControl and gather all the features after that.
Any insight is much appreciated.
@george-silva Good progress! Can you continue investigating, focusing on the differences between the pen and your production code? (I'm afraid we do have to insist on being provided a minimal example that shows the issue -- with the volume of issues that this repository receives, we can't dedicate much time to debugging third party integrations.)
@jfirebaugh hello! I fully understand it.
The main difference here, from my point of view, is Vue2. Since the codepen does not use vue2 AT ALL, I'll try to limit the interfaces between mapbox and vue to see if there are any changes.
Well, indeed. The culprit of this issue is Vue.
What was happening:
The code before:
<template>
<div id="map"></div>
</template>
<script>
export default {
name: 'MapView',
data () {
return {map: null}
},
mounted () {
this.map = this.initMap() // initializes mapbox-gl map, creates layers, etc.
}
}
</script>
The code after:
<template>
<div id="map"></div>
</template>
<script>
export default {
name: 'MapView',
mounted () {
this.map = this.initMap() // initializes mapbox-gl map, creates layers, etc.
}
}
</script>
The fix is simple. By removing the map as a data property, vue does not track state of that object. it simply becomes a non-reactive member of the view.
Although this was a big headache for me, this is not a mapbox-gl bug, just an integration caveat.
@anandthakker @jfirebaugh what is the best way to document this for future users? Is there somewhere in the documentation that gives simple headlines for using mapbox-gl with other frameworks or something like that?
Thanks for following up @george-silva!
what is the best way to document this for future users?
This issue itself will probably be useful for users searching in the future! Maybe even more so if you could summarize here what was required in order to fix the issue?
Most helpful comment
Well, indeed. The culprit of this issue is Vue.
What was happening:
The code before:
The code after:
The fix is simple. By removing the map as a data property, vue does not track state of that object. it simply becomes a non-reactive member of the view.
Although this was a big headache for me, this is not a mapbox-gl bug, just an integration caveat.
@anandthakker @jfirebaugh what is the best way to document this for future users? Is there somewhere in the documentation that gives simple headlines for using mapbox-gl with other frameworks or something like that?