Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix layer property reactivity #368

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/components/layerlist/LayerLegendImage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<script>

import LayerLegend from '../../util/LayerLegend';
import { LayerProxy } from '../../util/Layer'

/**
* Module for one legend element.
Expand All @@ -22,7 +23,8 @@ export default {
data () {
return {
resolution: this.mapView.getResolution(),
viewResolutionChanged: undefined
viewResolutionChanged: undefined,
layerProxy: new LayerProxy(this.layer, ['legend', 'legendOptions'])
}
},

Expand All @@ -44,6 +46,7 @@ export default {
if (this.viewResolutionChanged) {
this.mapView.un('change:resolution', this.viewResolutionChanged);
}
this.layerProxy.destroy();
},
computed: {
/**
Expand All @@ -52,10 +55,11 @@ export default {
legendURL () {
const options = {
language: this.$i18n.locale,
...this.layer.get('legendOptions')
...this.layerProxy.get('legendOptions')
};
return LayerLegend.getUrl(
this.layer, this.resolution, options, this.layer.get('legendUrl'));
this.layerProxy.getLayer(), this.resolution, options,
this.layerProxy.get('legendUrl'));
}
}
}
Expand Down
16 changes: 12 additions & 4 deletions src/components/layerlist/LayerList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<wgu-layerlistitem
v-for="layer in displayedLayers"
:key="layer.get('lid')"
:layer="layer"
:layer="layer.getLayer()"
:mapView="map.getView()"
:showLegends="showLegends"
:showOpacityControls="showOpacityControls"
Expand All @@ -13,6 +13,7 @@
</template>

<script>
import { LayerCollectionProxy } from '@/util/Layer';
import { Mapable } from '../../mixins/Mapable';
import LayerListItem from './LayerListItem'

Expand All @@ -28,7 +29,7 @@ export default {
},
data () {
return {
layers: []
layersProxy: undefined
}
},
methods: {
Expand All @@ -37,16 +38,23 @@ export default {
* Bind to the layers from the OpenLayers map.
*/
onMapBound () {
this.layers = this.map.getLayers().getArray();
this.layersProxy = new LayerCollectionProxy(this.map.getLayers(),
['lid', 'displayInLayerList', 'isBaseLayer']);
}
},
destroyed () {
this.layersProxy.destroy();
},
computed: {
/**
* Reactive property to return the OpenLayers layers to be shown in the control.
* Remarks: The 'displayInLayerList' attribute should default to true per convention.
*/
displayedLayers () {
return this.layers
if (!this.layersProxy) {
return [];
}
return this.layersProxy.getArray()
.filter(layer => layer.get('displayInLayerList') !== false && !layer.get('isBaseLayer'))
.reverse();
}
Expand Down
27 changes: 16 additions & 11 deletions src/components/layerlist/LayerListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
<v-checkbox
color="secondary"
hide-details
:input-value="layer.getVisible()"
:input-value="layerProxy.getVisible()"
@click.capture.stop="onItemClick()"
/>
</v-list-item-action>
<v-list-item-title>
{{ layer.get('name') }}
{{ layerProxy.get('name') }}
</v-list-item-title>
</template>
<v-list-item
v-if="showOpacityControl"
class="overflow-visible"
>
<wgu-layeropacitycontrol
:layer="layer"
:layer="layerProxy.getLayer()"
/>
</v-list-item>
<v-list-item
Expand All @@ -34,7 +34,7 @@
requests when the layer item is not expanded.
-->
<wgu-layerlegendimage v-if="open"
:layer="layer"
:layer="layerProxy.getLayer()"
:mapView="mapView"
/>
</v-list-item>
Expand All @@ -49,13 +49,13 @@
<v-checkbox
color="secondary"
hide-details
:input-value="layer.getVisible()"
@click.capture.stop="onItemClick(layer)"
:input-value="layerProxy.getVisible()"
@click.capture.stop="onItemClick(layerProxy)"
/>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
{{ layer.get('name') }}
{{ layerProxy.get('name') }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
Expand All @@ -64,6 +64,7 @@
<script>
import LayerLegendImage from './LayerLegendImage'
import LayerOpacityControl from './LayerOpacityControl'
import { LayerProxy } from '../../util/Layer'

export default {
name: 'wgu-layerlistitem',
Expand All @@ -73,7 +74,8 @@ export default {
},
data () {
return {
open: false
open: false,
layerProxy: new LayerProxy(this.layer, ['name', 'legend', 'opacityControl'])
}
},
props: {
Expand All @@ -82,12 +84,15 @@ export default {
showLegends: { type: Boolean, required: true },
showOpacityControls: { type: Boolean, required: true }
},
destroyed () {
this.layerProxy.destroy();
},
methods: {
/**
* Handler for click on layer item, toggles the layer`s visibility.
*/
onItemClick () {
this.layer.setVisible(!this.layer.getVisible());
this.layerProxy.setVisible(!this.layerProxy.getVisible());
}
},
computed: {
Expand All @@ -101,13 +106,13 @@ export default {
* Returns true, if the layer item should show a legend image.
*/
showLegend () {
return this.showLegends && !!this.layer.get('legend');
return this.showLegends && !!this.layerProxy.get('legend');
},
/**
* Returns true, if the layer item should show an opacity control.
*/
showOpacityControl () {
return this.showOpacityControls && !!this.layer.get('opacityControl');
return this.showOpacityControls && !!this.layerProxy.get('opacityControl');
}
}
};
Expand Down
187 changes: 187 additions & 0 deletions src/util/Layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,190 @@ const LayerUtil = {
}

export default LayerUtil;

/**
* Transparent proxy around an OpenLayers layer to be used in Vue components
* when reactive layer properties are required.
*/
export class LayerProxy {
/**
* @param {ol.layer.Base} layer OL layer
* @param {Array} properties An array of property key names which need to be
* accessed on the layer.
*/
constructor (layer, properties) {
this.layer = layer;
this.properties = {};
this.changeListeners = {};
properties.forEach(property => {
this.properties[property] = layer.get(property);
this.changeListeners[property] = () => {
this.properties[property] = layer.get(property);
};
layer.on(`change:${property}`, this.changeListeners[property]);
});

// Forward everything transparently to the underlying OL layer. The get()
// and getProperties() methods are trapped and handled by the proxy.
// Remarks: Neither set() nor setProperties() have to be handled. Property
// setters operate on the OL layer and then get synced into the proxy via
// observables.
return new Proxy(this, {
get: function (target, prop, receiver) {
if (prop in target.layer && !['get', 'getProperties'].includes(prop)) {
const p = target.layer[prop];
return (typeof p === 'function') ? p.bind(target.layer) : p;
}
return Reflect.get(target, prop, receiver);
}
});
}

/**
* Gets a value. The property name must be registered in the constructor.
* @param {String} property Key name.
* @returns {String} Value.
*/
get (property) {
return this.properties[property];
}

/**
* Get an object of all property names and values registered for in the
* constructor.
* @returns {Object} Object.
*/
getProperties () {
return this.properties;
}

/**
* Get the OL layer object wrapped by this proxy.
* @returns {ol.layer.Base} OL layer
*/
getLayer () {
return this.layer;
}

/**
* Destroy the proxy object. This must be invoked before the proxy goes out
* of scope to prevent dangling change notifications.
*/
destroy () {
Object.keys(this.changeListeners).forEach(property => {
this.layer.un(`change:${property}`, this.changeListeners[property]);
});
}
}

/**
* Transparent proxy around an OpenLayers collection for layers to be used in
* Vue components when reactive layer properties are required.
*/
export class LayerCollectionProxy {
/**
* @param {ol.Collection<ol.layer.Base>} collection OL collection of layers
* @param {Array} properties An array of property key names which need to be
* accessed on the layers.
*/
constructor (collection, properties) {
this.collection = collection;
this.layerProxies = [];

const createLayerProxy = (layer) => new LayerProxy(layer, properties);

// Sync against the underlying collection while retaining the order of
// elements.
// Remarks: A layer proxy must be destroyed before it goes out of scope.
// To support reactivity the instance of layerProxies must be preserved and
// the length property may not be invoked - see
// https://v2.vuejs.org/v2/guide/reactivity.html#For-Arrays
this.syncLayers = () => {
const newLayerProxies = [];
this.collection.forEach(layer => {
let layerProxy = this.layerProxies.find(proxy => proxy.getLayer() === layer);
if (!layerProxy) {
layerProxy = createLayerProxy(layer);
}
newLayerProxies.push(layerProxy);
});

this.layerProxies.forEach(layerProxy => {
if (!newLayerProxies.includes(layerProxy)) {
layerProxy.destroy();
}
});

this.layerProxies.splice(0);
this.layerProxies.push(...newLayerProxies);
};

this.syncLayers();

this.collection.on('change:length', this.syncLayers);

// Forward everything transparently to the underlying OL collection.
// The forEach() and getArray() and item() methods are trapped and handled
// by the proxy.
// Remarks: Methods which alter the collection are not handled.
// These operate on the OL collection and changes get synced into the
// proxy via observables. The methods pop(), push(), remove(),
// removeAt(), setAt() will operate on OL base layer arguments. Therefore
// returned objects by these methods will not properly support reactivity.
return new Proxy(this, {
get: function (target, prop, receiver) {
if (prop in target.collection &&
!['forEach', 'getArray', 'item'].includes(prop)) {
const p = target.collection[prop];
return (typeof p === 'function') ? p.bind(target.collection) : p;
}
return Reflect.get(target, prop, receiver);
}
});
}

/**
* Iterate over each element, calling the provided callback.
* @param {function} f The function to call for every element. This function
* takes 3 arguments (the element, the index and the array). The return value
* is ignored.
*/
forEach (f) {
this.layerProxies.forEach(f)
}

/**
* Get an array of LayerProxy objects for all layers in the collection.
* @returns {Array<LayerProxy>} Array of LayerProxy objects.
*/
getArray () {
return this.layerProxies;
}

/**
* Get the LayerProxy at the provided index.
* @param {Number} index Index.
* @returns Element.
*/
item (index) {
return this.layerProxies[index];
}

/**
* Get the OL collection object wrapped by this proxy.
* @returns {ol.Collection<ol.layer.Base>} OL collection of layers
*/
getCollection () {
return this.collection;
}

/**
* Destroy the proxy object. This must be invoked before the proxy goes out of scope.
*/
destroy () {
this.collection.un('change:length', this.syncLayers);
this.layerProxies.forEach(layerProxy => {
layerProxy.destroy();
});
}
}
Loading