diff --git a/js/src/lib/ol.js b/js/src/lib/ol.js index e953773..5c024b3 100644 --- a/js/src/lib/ol.js +++ b/js/src/lib/ol.js @@ -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 }; diff --git a/js/src/mapapi/api_gmaps.js b/js/src/mapapi/api_gmaps.js index aa0e6bd..3b0a059 100644 --- a/js/src/mapapi/api_gmaps.js +++ b/js/src/mapapi/api_gmaps.js @@ -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 */ diff --git a/js/src/mapapi/api_openlayers.js b/js/src/mapapi/api_openlayers.js index 1175ffd..7d1ad43 100644 --- a/js/src/mapapi/api_openlayers.js +++ b/js/src/mapapi/api_openlayers.js @@ -587,16 +587,36 @@ export default class OpenLayersApi { } /** - * Fit to extent, respect max zoom + * Fit to extent, respect max zoom, add padding * @param {Array.} extent * @return {Array.} */ 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 ]; diff --git a/js/src/mapviewmodel.js b/js/src/mapviewmodel.js index 6cd7b8a..15588f1 100644 --- a/js/src/mapviewmodel.js +++ b/js/src/mapviewmodel.js @@ -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); diff --git a/js/test/api_gmaps.test.js b/js/test/api_gmaps.test.js index 8129d79..8187cc0 100644 --- a/js/test/api_gmaps.test.js +++ b/js/test/api_gmaps.test.js @@ -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); diff --git a/js/test/api_openlayers.test.js b/js/test/api_openlayers.test.js index 86bf665..60bad44 100644 --- a/js/test/api_openlayers.test.js +++ b/js/test/api_openlayers.test.js @@ -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); diff --git a/js/test/helpers/googlemaps.stub.js b/js/test/helpers/googlemaps.stub.js index 57ba245..92d4c0d 100644 --- a/js/test/helpers/googlemaps.stub.js +++ b/js/test/helpers/googlemaps.stub.js @@ -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; } diff --git a/js/test/mapviewmodel.test.js b/js/test/mapviewmodel.test.js index ead4678..f5ac567 100644 --- a/js/test/mapviewmodel.test.js +++ b/js/test/mapviewmodel.test.js @@ -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();