Don't zoom after new position added, center when last position is not visible (fixes #161)

This commit is contained in:
Bartek Fabiszewski 2021-08-30 22:21:50 +02:00
parent 1180a9fba8
commit f641b68e14
8 changed files with 201 additions and 20 deletions

View File

@ -32,9 +32,11 @@ import Vector from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';
import View from 'ol/View';
import XYZ from 'ol/source/XYZ';
import { containsCoordinate } from 'ol/extent.js';
export { Feature, Map, Overlay, View };
export const control = { Control, Rotate, ScaleLine, Zoom, ZoomToExtent };
export const extent = { containsCoordinate };
export const geom = { LineString, Point };
export const layer = { TileLayer, VectorLayer };
export const proj = { fromLonLat, toLonLat };

View File

@ -364,6 +364,28 @@ export default class GoogleMapsApi {
this.map.fitBounds(latLngBounds);
}
/**
* Is given position within viewport
* @param {number} id
* @return {boolean}
*/
isPositionVisible(id) {
if (id >= this.markers.length) {
return false;
}
return this.map.getBounds().contains(this.markers[id].getPosition());
}
/**
* Center to given position
* @param {number} id
*/
centerToPosition(id) {
if (id < this.markers.length) {
this.map.setCenter(this.markers[id].getPosition());
}
}
/**
* Update size
*/

View File

@ -587,16 +587,36 @@ export default class OpenLayersApi {
}
/**
* Fit to extent, respect max zoom
* Fit to extent, respect max zoom, add padding
* @param {Array.<number>} extent
* @return {Array.<number>}
*/
fitToExtent(extent) {
this.map.getView().fit(extent, { padding: [ 40, 10, 10, 10 ], maxZoom: OpenLayersApi.ZOOM_MAX });
if (this.map.getView().getZoom() === OpenLayersApi.ZOOM_MAX) {
extent = this.map.getView().calculateExtent(this.map.getSize());
this.map.getView().fit(extent, { padding: OpenLayersApi.TRACK_PADDING, maxZoom: OpenLayersApi.ZOOM_MAX });
return this.map.getView().calculateExtent();
}
/**
* Is given position within viewport
* @param {number} id
* @return {boolean}
*/
isPositionVisible(id) {
const mapExtent = this.map.getView().calculateExtent();
const marker = this.layerMarkers.getSource().getFeatureById(id).getGeometry();
return marker ? ol.extent.containsCoordinate(mapExtent, marker.getCoordinates()) : false;
}
/**
* Center to given position
* @param {number} id
*/
centerToPosition(id) {
const marker = this.layerMarkers.getSource().getFeatureById(id).getGeometry();
if (marker) {
console.log(`Setting center to position ${id}`)
this.map.getView().setCenter(marker.getCoordinates());
}
return extent;
}
/**
@ -691,17 +711,17 @@ export default class OpenLayersApi {
* @returns {number[]} Bounds [ lon_sw, lat_sw, lon_ne, lat_ne ]
*/
getBounds() {
const extent = this.map.getView().calculateExtent(this.map.getSize());
const extent = this.map.getView().calculateExtent();
const sw = ol.proj.toLonLat([ extent[0], extent[1] ]);
const ne = ol.proj.toLonLat([ extent[2], extent[3] ]);
return [ sw[0], sw[1], ne[0], ne[1] ];
}
/**
* Zoom to track extent, respect max zoom
* Zoom to track extent, respect max zoom, add padding
*/
zoomToExtent() {
this.map.getView().fit(this.layerMarkers.getSource().getExtent(), { maxZoom: OpenLayersApi.ZOOM_MAX });
this.fitToExtent(this.layerMarkers.getSource().getExtent());
}
/**
@ -760,3 +780,5 @@ export default class OpenLayersApi {
}
/** @type {number} */
OpenLayersApi.ZOOM_MAX = 20;
/** @type {number[]} */
OpenLayersApi.TRACK_PADDING = [ 40, 10, 10, 10 ];

View File

@ -25,6 +25,7 @@ import ViewModel from './viewmodel.js';
import uAlert from './alert.js';
import uDialog from './dialog.js';
import uObserve from './observe.js';
import uTrack from './track.js';
import uUtils from './utils.js';
/**
@ -157,7 +158,10 @@ export default class MapViewModel extends ViewModel {
if (track) {
uObserve.observe(track, 'positions', () => {
this.displayTrack(track, false);
this.api.zoomToExtent();
if (track instanceof uTrack && !this.api.isPositionVisible(track.length - 1)) {
console.log('last track position not visible');
this.api.centerToPosition(track.length - 1);
}
this.toggleStyleOptions();
});
this.displayTrack(track, true);

View File

@ -301,6 +301,70 @@ describe('Google Maps map API tests', () => {
expect(setTimeout).toHaveBeenCalledTimes(1);
});
it('should center map to given marker', () => {
// given
const track = TrackFactory.getTrack(1);
const coordinates = [ 1, 3 ];
spyOn(GoogleMapsApi, 'getMarkerIcon');
spyOn(google.maps.Map.prototype, 'setCenter');
spyOn(google.maps.Marker.prototype, 'getPosition').and.returnValue(coordinates);
spyOn(google.maps, 'Marker').and.callThrough();
api.map = new google.maps.Map(container);
const id = 0;
api.setMarker(id, track);
// when
api.centerToPosition(id);
// then
expect(google.maps.Map.prototype.setCenter).toHaveBeenCalledWith(coordinates);
});
it('should confirm that position is visible', () => {
// given
const track = TrackFactory.getTrack(1);
const coordinates = [ 1, 3 ];
spyOn(GoogleMapsApi, 'getMarkerIcon');
spyOn(google.maps.Map.prototype, 'getBounds').and.returnValue(new google.maps.LatLngBounds());
spyOn(google.maps.LatLngBounds.prototype, 'contains').and.returnValue(true);
spyOn(google.maps.Marker.prototype, 'getPosition').and.returnValue(coordinates);
spyOn(google.maps, 'Marker').and.callThrough();
api.map = new google.maps.Map(container);
const id = 0;
api.setMarker(id, track);
// when
const result = api.isPositionVisible(id);
// then
expect(google.maps.LatLngBounds.prototype.contains).toHaveBeenCalledWith(coordinates);
expect(result).toBeTrue();
});
it('should confirm that position is not visible', () => {
// given
const track = TrackFactory.getTrack(1);
const coordinates = [ 1, 3 ];
spyOn(GoogleMapsApi, 'getMarkerIcon');
spyOn(google.maps.Map.prototype, 'getBounds').and.returnValue(new google.maps.LatLngBounds());
spyOn(google.maps.LatLngBounds.prototype, 'contains').and.returnValue(false);
spyOn(google.maps.Marker.prototype, 'getPosition').and.returnValue(coordinates);
spyOn(google.maps, 'Marker').and.callThrough();
api.map = new google.maps.Map(container);
const id = 0;
api.setMarker(id, track);
// when
const result = api.isPositionVisible(id);
// then
expect(google.maps.LatLngBounds.prototype.contains).toHaveBeenCalledWith(coordinates);
expect(result).toBeFalse();
});
it('should create marker from track position and add it to markers array', () => {
// given
const track = TrackFactory.getTrack(1);

View File

@ -18,7 +18,7 @@
*/
import * as ol from '../src/lib/ol.js';
import OpenlayersApi from '../src/mapapi/api_openlayers.js';
import OpenLayersApi from '../src/mapapi/api_openlayers.js';
import TrackFactory from './helpers/trackfactory.js';
import { config } from '../src/initializer.js'
import uLayer from '../src/layer.js';
@ -35,10 +35,14 @@ describe('Openlayers map API tests', () => {
config.reinitialize();
document.body.innerHTML = '';
container = document.createElement('div');
container.setAttribute('style', 'display: block; width: 100px; height: 100px');
document.body.appendChild(container);
mockViewModel = { mapElement: container, model: {} };
api = new OpenlayersApi(mockViewModel, ol);
mockMap = new ol.Map({ target: container });
api = new OpenLayersApi(mockViewModel, ol);
mockMap = new ol.Map({
target: container,
view: new ol.View({ zoom: 8, center: [ 0, 0 ] })
});
});
it('should load and initialize api scripts', (done) => {
@ -353,13 +357,12 @@ describe('Openlayers map API tests', () => {
// given
api.map = mockMap;
spyOn(ol.View.prototype, 'fit');
spyOn(ol.View.prototype, 'getZoom').and.returnValue(OpenlayersApi.ZOOM_MAX - 1);
spyOn(ol.View.prototype, 'getZoom').and.returnValue(OpenLayersApi.ZOOM_MAX - 1);
const extent = [ 0, 1, 2, 3 ];
// when
const result = api.fitToExtent(extent);
api.fitToExtent(extent);
// then
expect(ol.View.prototype.fit).toHaveBeenCalledWith(extent, jasmine.any(Object));
expect(result).toEqual(extent);
});
it('should fit to extent and zoom to max value', () => {
@ -368,7 +371,7 @@ describe('Openlayers map API tests', () => {
const zoomedExtent = [ 3, 2, 1, 0 ];
api.map = mockMap;
spyOn(ol.View.prototype, 'fit');
spyOn(ol.View.prototype, 'getZoom').and.returnValue(OpenlayersApi.ZOOM_MAX);
spyOn(ol.View.prototype, 'getZoom').and.returnValue(OpenLayersApi.ZOOM_MAX);
spyOn(ol.View.prototype, 'setZoom');
spyOn(ol.View.prototype, 'calculateExtent').and.returnValue(zoomedExtent);
// when
@ -394,6 +397,68 @@ describe('Openlayers map API tests', () => {
expect(marker.getGeometry().getFirstCoordinate()).toEqual(ol.proj.fromLonLat([ track.positions[0].longitude, track.positions[0].latitude ]));
});
it('should center map to given marker', () => {
// given
const track = TrackFactory.getTrack(1);
const coordinates = [ 1, 3 ];
track.positions[0].timestamp = 1;
track.positions[0].longitude = coordinates[0];
track.positions[0].latitude = coordinates[1];
const id = 0;
api.map = mockMap;
api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
spyOn(ol.View.prototype, 'setCenter');
spyOn(api, 'getMarkerStyle');
api.setMarker(id, track);
// when
api.centerToPosition(id);
// then
expect(ol.View.prototype.setCenter).toHaveBeenCalledWith(ol.proj.fromLonLat(coordinates));
});
it('should confirm that position is visible', () => {
// given
const track = TrackFactory.getTrack(1);
const coordinates = [ 1, 3 ];
track.positions[0].timestamp = 1;
track.positions[0].longitude = coordinates[0];
track.positions[0].latitude = coordinates[1];
const id = 0;
api.map = mockMap;
api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
spyOn(ol.extent, 'containsCoordinate').and.returnValue(true);
spyOn(api, 'getMarkerStyle');
api.setMarker(id, track);
// when
const result = api.isPositionVisible(id);
// then
expect(ol.extent.containsCoordinate).toHaveBeenCalledWith(jasmine.any(Array), ol.proj.fromLonLat(coordinates));
expect(result).toBeTrue();
});
it('should confirm that position is not visible', () => {
// given
const track = TrackFactory.getTrack(1);
const coordinates = [ 1, 3 ];
track.positions[0].timestamp = 1;
track.positions[0].longitude = coordinates[0];
track.positions[0].latitude = coordinates[1];
const id = 0;
api.map = mockMap;
api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
spyOn(ol.extent, 'containsCoordinate').and.returnValue(false);
spyOn(api, 'getMarkerStyle');
api.setMarker(id, track);
// when
const result = api.isPositionVisible(id);
// then
expect(ol.extent.containsCoordinate).toHaveBeenCalledWith(jasmine.any(Array), ol.proj.fromLonLat(coordinates));
expect(result).toBeFalse();
});
it('should get different marker style for start, end and normal position', () => {
// given
const track = TrackFactory.getTrack(3);
@ -499,7 +564,7 @@ describe('Openlayers map API tests', () => {
// when
api.zoomToExtent();
// then
expect(ol.View.prototype.fit).toHaveBeenCalledWith(extent, { maxZoom: OpenlayersApi.ZOOM_MAX });
expect(ol.View.prototype.fit).toHaveBeenCalledWith(extent, { padding: OpenLayersApi.TRACK_PADDING, maxZoom: OpenLayersApi.ZOOM_MAX });
});
it('should get map bounds and convert to WGS84 (EPSG:4326)', () => {
@ -510,7 +575,7 @@ describe('Openlayers map API tests', () => {
// when
const bounds = api.getBounds();
// then
expect(ol.View.prototype.calculateExtent).toHaveBeenCalledWith(jasmine.any(Array));
expect(ol.View.prototype.calculateExtent).toHaveBeenCalledTimes(1);
expect(bounds[0]).toBeCloseTo(20.597985430276808);
expect(bounds[1]).toBeCloseTo(52.15547181298076);
expect(bounds[2]).toBeCloseTo(21.363595171488573);

View File

@ -53,6 +53,7 @@ export const setupGmapsStub = () => {
this.sw = sw;
this.ne = ne;
}
contains() {/* ignore */}
extend() {/* ignore */}
getNorthEast() { return this.ne; }
getSouthWest() { return this.sw; }

View File

@ -60,7 +60,9 @@ describe('MapViewModel tests', () => {
'setTrackDefaultStyle': { /* ignored */ },
'setTrackGradientStyle': { /* ignored */ },
'clearMap': { /* ignored */ },
'updateSize': { /* ignored */ }
'updateSize': { /* ignored */ },
'isPositionVisible': { /* ignored */ },
'centerToPosition': { /* ignored */ }
});
state = new uState();
vm = new MapViewModel(state);
@ -251,7 +253,6 @@ describe('MapViewModel tests', () => {
state.currentTrack.positions.push(TrackFactory.getPosition(100));
// then
setTimeout(() => {
expect(mockApi.zoomToExtent).toHaveBeenCalledTimes(1);
expect(mockApi.displayTrack).toHaveBeenCalledTimes(1);
expect(mockApi.displayTrack).toHaveBeenCalledWith(track, false);
done();