Skip to content

Backport #428: Refactoring of Map Loading Status component #437

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

Merged
merged 2 commits into from
Apr 14, 2025
Merged
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
130 changes: 17 additions & 113 deletions src/components/progress/MapLoadingStatus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,13 @@
</template>

<script>

import { Mapable } from '../../mixins/Mapable';
import {
Image as ImageSource, TileImage as TileImageSource,
Vector as VectorSource, Cluster as ClusterSource
} from 'ol/source';
import LayerGroup from 'ol/layer/Group';

export default {
name: 'wgu-maploading-status',
mixins: [Mapable],
data () {
return {
loading: 0,
visible: false
}
},
Expand All @@ -35,120 +28,31 @@ export default {
* and registers the current map layers.
*/
onMapBound () {
const me = this;
me.registerLayers(me.map.getLayerGroup());
this.registerMapEvents();
},

/**
* Registers the needed events for the passed layer and takes care
* of layer groups by calling itself recursively.
* Registers the needed events on the map.
*
* @param {ol/layer/Base | ol/layer/Group} layer Layer or group to register
*/
registerLayers (layer) {
const me = this;

if (layer instanceof LayerGroup) {
// call ourself recursively
const layers = layer.getLayers();
layers.forEach(function (child) {
me.registerLayers(child);
});
// handle future changes to this group or stop doing so
layers.on('add', me.onLayerAddedToGroup, me);
layers.on('remove', me.onLayerRemovedFromGroup, me);
} else {
const source = layer.getSource();
me.bindLoadHandlers(source, true);
// reacting on a changed source in a layer
layer.on('change:source', function (evt) {
me.bindLoadHandlers(evt.target.getSource(), true);
me.bindLoadHandlers(evt.oldValue, false);
});
}
},

/**
* Registers needed events for the passed source and takes care
* of sources which do not naturally support load events (`vector`-sources).
*
* @param {ol/source/Source} source The layer source to register
*/
bindLoadHandlers (source, register) {
const me = this;
let eventPrefix = '';
const method = register ? 'on' : 'un';

if (source instanceof ImageSource) {
// includes ImageWms, also e.g. ImageArcGISRest, OSM …
eventPrefix = 'image';
} else if (source instanceof TileImageSource) {
// includes TileWMS, Bing and more
eventPrefix = 'tile';
} else if (source instanceof VectorSource) {
if (source instanceof ClusterSource) {
source = source.getSource();
}
eventPrefix = 'vector';
}

if (eventPrefix) {
source[method](eventPrefix + 'loadstart', me.incrementLoading, me);
source[method](eventPrefix + 'loadend', me.decrementLoading, me);
source[method](eventPrefix + 'loaderror', me.decrementLoading, me);
}
},

/**
* Called whenever loading of a layer starts.
* This will increment the internal counter and show this loading indicator.
*/
incrementLoading () {
const me = this;
me.loading++;
me.visible = true;
registerMapEvents () {
this.map.on('loadstart', this.showLoader);
this.map.on('loadend', this.hideLoader);
},

/**
* Called whenever loading stops or errors.
* This will decrement the internal counter and if nothing is currently
* loading the counter is reset and the loading indicator is hidden.
* Called whenever loading of map data starts.
*/
decrementLoading () {
const me = this;
me.loading--;

if (me.loading <= 0) {
me.visible = false;
me.resetLoading();
}
showLoader () {
this.visible = true;
},

/**
* Helper method to reset the internal loading counter.
* Called whenever loading of map data has ended.
*/
resetLoading () {
this.loading = 0;
},

/**
* Takes care of added layers to any `ol/layer/Group`. These need to get
* registered to the appropriate handlers.
*
* @param {ol/events/Event} evt The add event of the `ol/layer/Group`.
*/
onLayerAddedToGroup: function (evt) {
this.registerLayers(evt.element);
},

/**
* Takes care of removed layers to any `ol/layer/Group`. These need to get
* unregistered from the appropriate handlers.
*
* @param {ol/events/Event} evt The remove event of the `ol/layer/Group`.
*/
onLayerRemovedFromGroup: function (evt) {
this.registerLayers(evt.element);
hideLoader () {
this.visible = false;
}
}
}
Expand All @@ -157,11 +61,11 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>

.v-progress-circular.wgu-maploading-status {
position: fixed;
bottom: 5em;
right: 4em;
z-index: 1;
}
.v-progress-circular.wgu-maploading-status {
position: fixed;
bottom: 5em;
right: 4em;
z-index: 1;
}

</style>
121 changes: 111 additions & 10 deletions tests/unit/specs/components/progress/MapLoadingStatus.spec.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,118 @@
import MapLoadingStatus from '@/components/progress/MapLoadingStatus'
import { shallowMount } from '@vue/test-utils';
import MapLoadingStatus from '@/components/progress/MapLoadingStatus';
import OlMap from 'ol/Map';
import OlView from 'ol/View';
import MapEvent from 'ol/MapEvent';
import MapEventType from 'ol/MapEventType';

describe('progress/MapLoadingStatus.vue', () => {
// Inspect the raw component options
it('is defined', () => {
expect(typeof MapLoadingStatus).to.not.equal('undefined');
expect(typeof MapLoadingStatus).to.not.be.an('undefined');
});

// Evaluate the results of functions in
// the raw component options
it('sets the correct default data', () => {
expect(typeof MapLoadingStatus.data).to.equal('function');
const defaultData = MapLoadingStatus.data();
expect(defaultData.loading).to.equal(0);
expect(defaultData.visible).to.equal(false);
describe('data', () => {
let comp;

beforeEach(() => {
comp = shallowMount(MapLoadingStatus);
});

it('has correct default data', () => {
expect(comp.vm.visible).to.be.false;
});

afterEach(() => {
comp.destroy();
});
});

describe('events', () => {
let comp;
let vm;

beforeEach(() => {
const olMap = new OlMap({
view: new OlView({
center: [0, 0],
zoom: 1
}),
layers: []
});

comp = shallowMount(MapLoadingStatus);
vm = comp.vm;
vm.map = olMap;
vm.onMapBound();
});

it('hides the circular progress when mounted', () => {
const circularProgress = comp.findComponent({ name: 'v-progress-circular' });
expect(circularProgress.exists(), 'Circular progress should not be visible').to.be.false;
});

it('shows the circular progress when map starts loading data', done => {
const frameState = {
layerStatesArray: []
};

vm.map.on('loadstart', () => {
vm.$nextTick(() => {
vm.$nextTick(() => {
try {
const circularProgress = comp.findComponent({ name: 'v-progress-circular' });
expect(circularProgress.exists(), 'Circular progress should be visible').to.be.true;
done();
} catch (error) {
done(error);
}
});
});
});

vm.map.dispatchEvent(new MapEvent(MapEventType.LOADSTART, vm.map, frameState));
});

it('hides the circular progress when map has finished loading data', done => {
const frameState = {
layerStatesArray: []
};

vm.map.on('loadstart', () => {
vm.$nextTick(() => {
vm.$nextTick(() => {
try {
const circularProgress = comp.findComponent({ name: 'v-progress-circular' });
expect(circularProgress.exists(), 'Circular progress should be visible').to.be.true;

// Send event that should hide the Map Loading Status component
vm.map.dispatchEvent(new MapEvent(MapEventType.LOADEND, vm.map, frameState));
} catch (error) {
done(error);
}
});
});
});

vm.map.on('loadend', () => {
vm.$nextTick(() => {
vm.$nextTick(() => {
try {
const circularProgress = comp.findComponent({ name: 'v-progress-circular' });
expect(circularProgress.exists(), 'Circular progress should be hidden').to.be.false;
done();
} catch (error) {
done(error);
}
});
});
});

// Send event that should display the Map Loading Status component
vm.map.dispatchEvent(new MapEvent(MapEventType.LOADSTART, vm.map, frameState));
});

afterEach(() => {
comp.destroy();
});
});
});