diff --git a/images/altitude.svg b/images/altitude.svg
new file mode 100644
index 0000000..7a7e8a7
--- /dev/null
+++ b/images/altitude.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/speed.svg b/images/speed.svg
new file mode 100644
index 0000000..a83a1f7
--- /dev/null
+++ b/images/speed.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/index.php b/index.php
index b2ebcc3..4201515 100644
--- a/index.php
+++ b/index.php
@@ -85,8 +85,14 @@
diff --git a/js/src/chartviewmodel.js b/js/src/chartviewmodel.js
index c98d4b3..9d629b0 100644
--- a/js/src/chartviewmodel.js
+++ b/js/src/chartviewmodel.js
@@ -87,7 +87,7 @@ export default class ChartViewModel extends ViewModel {
plugins: [
ctAxisTitle({
axisY: {
- axisTitle: `${$._('altitude')} (${$.unit('unitDistance')})`,
+ axisTitle: `${$._('altitude')} (${$.unit('unitDistance')} ${$.unit('unitAltitude')})`,
axisClass: 'ct-axis-title',
offset: {
x: 0,
@@ -162,9 +162,9 @@ export default class ChartViewModel extends ViewModel {
*/
renderButton(isVisible) {
if (isVisible) {
- this.buttonElement.style.visibility = 'visible';
+ this.buttonElement.classList.remove('menu-hidden');
} else {
- this.buttonElement.style.visibility = 'hidden';
+ this.buttonElement.classList.add('menu-hidden');
}
}
diff --git a/js/src/config.js b/js/src/config.js
index 60d36de..3b281c0 100644
--- a/js/src/config.js
+++ b/js/src/config.js
@@ -103,6 +103,7 @@ export default class uConfig {
this.unitDistanceMajor = 'unitkm';
}
this.unitDay = 'unitday';
+ this.unitAltitude = 'unitamsl';
}
/**
diff --git a/js/src/lang.js b/js/src/lang.js
index 51d92fa..1a09b39 100644
--- a/js/src/lang.js
+++ b/js/src/lang.js
@@ -67,59 +67,67 @@ export default class uLang {
/**
* Get speed converted to locale units
* @param {number} ms Speed in meters per second
- * @param {boolean} withUnit
- * @return {(number|string)} String when with unit
+ * @param {?boolean=false} withUnit
+ * @return {string}
*/
- getLocaleSpeed(ms, withUnit) {
+ getLocaleSpeed(ms, withUnit = false) {
const value = Math.round(ms * this.config.factorSpeed * 100) / 100;
+ let localized = value.toLocaleString(this.config.lang);
if (withUnit) {
- return `${value.toLocaleString(this.config.lang)} ${this.unit('unitSpeed')}`;
+ localized += ` ${this.unit('unitSpeed')}`;
}
- return value;
+ return localized;
}
/**
* Get distance converted to locale units
* @param {number} m Distance in meters
- * @param {boolean} withUnit
- * @return {(number|string)} String when with unit
+ * @param {?boolean=false} withUnit
+ * @return {string}
*/
- getLocaleDistanceMajor(m, withUnit) {
+ getLocaleDistanceMajor(m, withUnit = false) {
const value = Math.round(m * this.config.factorDistanceMajor / 10) / 100;
+ let localized = value.toLocaleString(this.config.lang);
if (withUnit) {
- return `${value.toLocaleString(this.config.lang)} ${this.unit('unitDistanceMajor')}`
+ localized += ` ${this.unit('unitDistanceMajor')}`;
}
- return value;
+ return localized;
}
/**
* @param {number} m Distance in meters
- * @param {boolean} withUnit
- * @return {(number|string)} String when with unit
+ * @param {?boolean=false} withUnit
+ * @return {string}
*/
- getLocaleDistance(m, withUnit) {
+ getLocaleDistance(m, withUnit = false) {
const value = Math.round(m * this.config.factorDistance * 100) / 100;
+ let localized = value.toLocaleString(this.config.lang);
if (withUnit) {
- return `${value.toLocaleString(this.config.lang)} ${this.unit('unitDistance')}`;
+ localized += ` ${this.unit('unitDistance')}`;
}
- return value;
+ return localized;
}
/**
- * @param {number} m Distance in meters
- * @param {boolean} withUnit
- * @return {(number|string)} String when with unit
+ * @param {number} m Altitude in meters
+ * @param {?boolean=false} withUnit
+ * @return {string}
*/
- getLocaleAltitude(m, withUnit) {
- return this.getLocaleDistance(m, withUnit);
+ getLocaleAltitude(m, withUnit = false) {
+ let localized = this.getLocaleDistance(m, withUnit);
+ if (withUnit) {
+ localized += ` ${this.unit('unitAltitude')}`;
+ }
+ // use typographic minus
+ return localized.replace('-', '−');
}
/**
- * @param {number} m Distance in meters
- * @param {boolean} withUnit
- * @return {(number|string)} String when with unit
+ * @param {number} m Accuracy in meters
+ * @param {?boolean=false} withUnit
+ * @return {string}
*/
- getLocaleAccuracy(m, withUnit) {
+ getLocaleAccuracy(m, withUnit = false) {
return this.getLocaleDistance(m, withUnit);
}
diff --git a/js/src/mapapi/api_gmaps.js b/js/src/mapapi/api_gmaps.js
index c4dd038..aa0e6bd 100644
--- a/js/src/mapapi/api_gmaps.js
+++ b/js/src/mapapi/api_gmaps.js
@@ -142,8 +142,10 @@ export default class GoogleMapsApi {
const promise = new Promise((resolve) => {
google.maps.event.addListenerOnce(this.map, 'tilesloaded', () => {
console.log('tilesloaded');
- this.saveState();
- this.map.addListener('idle', this.saveState);
+ if (this.map) {
+ this.saveState();
+ this.map.addListener('idle', this.saveState);
+ }
resolve();
})
});
@@ -370,7 +372,26 @@ export default class GoogleMapsApi {
// ignore for google API
}
- static get loadTimeoutMs() {
+ /**
+ * Set default track style
+ */
+ // eslint-disable-next-line class-methods-use-this
+ setTrackDefaultStyle() {
+ // ignore for google API
+ }
+
+ /**
+ * Set gradient style for given track property and scale
+ * @param {uTrack} track
+ * @param {string} property
+ * @param {{ minValue: number, maxValue: number, minColor: number[], maxColor: number[] }} scale
+ */
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
+ setTrackGradientStyle(track, property, scale) {
+ // ignore for google API
+ }
+
+ static get loadTimeoutMs() {
return 10000;
}
diff --git a/js/src/mapapi/api_openlayers.js b/js/src/mapapi/api_openlayers.js
index 5793c29..1175ffd 100644
--- a/js/src/mapapi/api_openlayers.js
+++ b/js/src/mapapi/api_openlayers.js
@@ -179,17 +179,10 @@ export default class OpenLayersApi {
}
// add track and markers layers
- const lineStyle = new ol.style.Style({
- stroke: new ol.style.Stroke({
- color: uUtils.hexToRGBA(config.strokeColor, config.strokeOpacity),
- width: config.strokeWeight
- })
- });
this.layerTrack = new ol.layer.VectorLayer({
name: 'Track',
type: 'data',
- source: new ol.source.Vector(),
- style: lineStyle
+ source: new ol.source.Vector()
});
this.layerMarkers = new ol.layer.VectorLayer({
name: 'Markers',
@@ -251,6 +244,107 @@ export default class OpenLayersApi {
};
}
+ /**
+ * Set default track style
+ */
+ setTrackDefaultStyle() {
+ const lineStyle = new ol.style.Style({
+ stroke: new ol.style.Stroke({
+ color: uUtils.hexToRGBA(config.strokeColor, config.strokeOpacity),
+ width: config.strokeWeight
+ })
+ })
+ this.layerTrack.setStyle(lineStyle);
+ }
+
+ /**
+ * @param {CanvasRenderingContext2D} context
+ * @param {Coordinate[]} coordinates
+ * @param {string[]} colors
+ * @return {Style}
+ */
+ getGradientStyle(context, coordinates, colors) {
+ const pixelStart = this.map.getPixelFromCoordinate(coordinates[0]);
+ const pixelEnd = this.map.getPixelFromCoordinate(coordinates[1]);
+ const scale = window.devicePixelRatio;
+ const x0 = pixelStart[0] * scale;
+ const y0 = pixelStart[1] * scale;
+ const x1 = pixelEnd[0] * scale;
+ const y1 = pixelEnd[1] * scale;
+ const gradient = context.createLinearGradient(x0, y0, x1, y1);
+ gradient.addColorStop(0, colors[0]);
+ gradient.addColorStop(1, colors[1]);
+ return new ol.style.Style({
+ geometry: new ol.geom.LineString(coordinates),
+ stroke: new ol.style.Stroke({
+ color: gradient,
+ width: config.strokeWeight * 2
+ })
+ })
+ }
+
+ /**
+ * Set gradient style for given track property and scale
+ * @param {uTrack} track
+ * @param {string} property
+ * @param {{ minValue: number, maxValue: number, minColor: number[], maxColor: number[] }} scale
+ */
+ setTrackGradientStyle(track, property, scale) {
+ const minValue = scale.minValue;
+ const maxValue = scale.maxValue;
+ const minColor = scale.minColor;
+ const maxColor = scale.maxColor;
+ if (track.length < 2 || maxValue < minValue) {
+ this.setTrackDefaultStyle();
+ return;
+ }
+ const canvas = document.createElement('canvas', { alpha: false, desynchronized: true });
+ const ctx = canvas.getContext('2d');
+ /**
+ * @param {Feature} feature
+ * @return {Style[]}
+ */
+ const lineStyle = (feature) => {
+ const styles = [
+ new ol.style.Style({
+ stroke: new ol.style.Stroke({
+ color: 'grey',
+ width: config.strokeWeight * 2 + 2
+ })
+ })
+ ];
+ const geometry = feature.getGeometry();
+ if (minValue === maxValue) {
+ styles.push(new ol.style.Style({
+ geometry: geometry,
+ stroke: new ol.style.Stroke({
+ color: uUtils.getScaleColor(minColor, maxColor, 0.5),
+ width: config.strokeWeight * 2
+ })
+ }));
+ return styles;
+ }
+ let position = track.positions[0];
+ const value = position[property] !== null ? position[property] : 0;
+ let colorStart = uUtils.getScaleColor(minColor, maxColor, (value - minValue) / (maxValue - minValue));
+ let index = 1;
+ geometry.forEachSegment((start, end) => {
+ position = track.positions[index];
+ let colorStop;
+ if (position[property] !== null) {
+ colorStop = uUtils.getScaleColor(minColor, maxColor, (position[property] - minValue) / (maxValue - minValue));
+ } else {
+ colorStop = colorStart;
+ }
+ styles.push(this.getGradientStyle(ctx, [ start, end ], [ colorStart, colorStop ]));
+ colorStart = colorStop;
+ index++;
+ });
+ return styles;
+ };
+ this.layerTrack.setStyle(lineStyle);
+ }
+
initPopups() {
const popupContainer = document.createElement('div');
popupContainer.id = 'popup-container';
@@ -438,8 +532,10 @@ export default class OpenLayersApi {
const promise = new Promise((resolve) => {
this.map.once('rendercomplete', () => {
console.log('rendercomplete');
- this.saveState();
- this.map.on('moveend', this.saveState);
+ if (this.map) {
+ this.saveState();
+ this.map.on('moveend', this.saveState);
+ }
resolve();
});
});
diff --git a/js/src/mapviewmodel.js b/js/src/mapviewmodel.js
index e178d34..1a7e74c 100644
--- a/js/src/mapviewmodel.js
+++ b/js/src/mapviewmodel.js
@@ -32,19 +32,20 @@ import uUtils from './utils.js';
* @interface
* @memberOf MapViewModel
* @type {Object}
- * @property {function(MapViewModel)} init
- * @property {function} cleanup
- * @property {function(uTrack, boolean)} displayTrack
- * @property {function} clearMap
* @property {function(number)} animateMarker
+ * @property {function(uTrack, boolean)} displayTrack
+ * @property {function} cleanup
+ * @property {function} clearMap
* @property {function} getBounds
- * @property {function} zoomToExtent
- * @property {function} zoomToBounds
+ * @property {function(MapViewModel)} init
+ * @property {function} setTrackDefaultStyle
+ * @property {function(uTrack, string, Object)} setTrackGradientStyle
* @property {function} updateSize
* @property {function} updateState
+ * @property {function} zoomToBounds
+ * @property {function} zoomToExtent
*/
-
/**
* @typedef {Object} MapParams
* @property {number[]} center
@@ -66,12 +67,19 @@ export default class MapViewModel extends ViewModel {
/** @type {?number} */
markerSelect: null,
// click handler
- onMenuToggle: null
+ onMenuToggle: null,
+ speedVisible: false,
+ altitudeVisible: false
});
- this.model.onMenuToggle = () => this.onMapResize();
this.state = state;
/** @type HTMLElement */
this.mapElement = document.querySelector('#map-canvas');
+ /** @type HTMLInputElement */
+ this.speedEl = this.getBoundElement('speedVisible');
+ /** @type HTMLInputElement */
+ this.altitudeEl = this.getBoundElement('altitudeVisible');
+ /** @type HTMLElement */
+ this.styleEl = this.getBoundElement('trackColor');
this.savedBounds = null;
this.api = null;
}
@@ -138,6 +146,8 @@ export default class MapViewModel extends ViewModel {
setObservers() {
config.onChanged('mapApi', (mapApi) => {
this.loadMapAPI(mapApi);
+ this.toggleStyleOptions();
+ this.toggleStyleMenu();
});
this.state.onChanged('currentTrack', (track) => {
if (!this.api) {
@@ -151,6 +161,7 @@ export default class MapViewModel extends ViewModel {
});
this.displayTrack(track, true);
}
+ this.toggleStyleOptions();
});
this.state.onChanged('history', () => {
const history = this.state.history;
@@ -167,6 +178,19 @@ export default class MapViewModel extends ViewModel {
}
}
});
+ this.model.onMenuToggle = () => this.onMapResize();
+ this.onChanged('speedVisible', (visible) => {
+ if (visible) {
+ this.model.altitudeVisible = false;
+ }
+ this.setTrackStyle();
+ });
+ this.onChanged('altitudeVisible', (visible) => {
+ if (visible) {
+ this.model.speedVisible = false;
+ }
+ this.setTrackStyle();
+ });
}
/**
@@ -180,10 +204,67 @@ export default class MapViewModel extends ViewModel {
update = false;
}
this.state.history = null;
+ this.setTrackStyle();
this.api.displayTrack(track, update)
.finally(() => this.state.jobStop());
}
+ onMapResize() {
+ if (this.api) {
+ this.api.updateSize();
+ }
+ }
+
+ toggleStyleOptions() {
+ const track = this.state.currentTrack;
+ this.speedEl.disabled = !track || !track.hasSpeeds || track.length <= 1;
+ this.altitudeEl.disabled = !track || !track.hasAltitudes || track.length <= 1;
+ }
+
+ toggleStyleMenu() {
+ if (config.mapApi === 'openlayers') {
+ this.styleEl.style.display = 'block';
+ } else {
+ this.styleEl.style.display = 'none';
+ }
+ }
+
+ setTrackStyle() {
+ const track = this.state.currentTrack;
+ if (!this.api || !track) {
+ return;
+ }
+ if (this.model.speedVisible && track.hasSpeeds) {
+ this.setSpeedStyle();
+ } else if (this.model.altitudeVisible && track.hasAltitudes) {
+ this.setAltitudeStyle();
+ } else {
+ this.api.setTrackDefaultStyle();
+ }
+ }
+
+ setSpeedStyle() {
+ const track = this.state.currentTrack;
+ const scale = {
+ minValue: 0,
+ maxValue: track.maxSpeed,
+ minColor: [ 0, 255, 0 ],
+ maxColor: [ 255, 0, 0 ]
+ };
+ this.api.setTrackGradientStyle(track, 'speed', scale);
+ }
+
+ setAltitudeStyle() {
+ const track = this.state.currentTrack;
+ const scale = {
+ minValue: track.minAltitude,
+ maxValue: track.maxAltitude,
+ minColor: [ 0, 255, 0 ],
+ maxColor: [ 255, 0, 0 ]
+ };
+ this.api.setTrackGradientStyle(track, 'altitude', scale);
+ }
+
/**
* Get popup html
* @param {number} id Position index
@@ -302,11 +383,4 @@ export default class MapViewModel extends ViewModel {
${isExtra ? MapViewModel.getMarkerExtra(isLarge) : ''}`;
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}
-
- onMapResize() {
- if (this.api) {
- this.api.updateSize();
- }
- }
-
}
diff --git a/js/src/position.js b/js/src/position.js
index 68e3ddd..f71cbc4 100644
--- a/js/src/position.js
+++ b/js/src/position.js
@@ -85,6 +85,20 @@ export default class uPosition {
return (this.image != null && this.image.length > 0);
}
+ /**
+ * @return {boolean}
+ */
+ hasSpeed() {
+ return this.speed != null;
+ }
+
+ /**
+ * @return {boolean}
+ */
+ hasAltitude() {
+ return this.altitude != null;
+ }
+
/**
* @return {?string}
*/
diff --git a/js/src/track.js b/js/src/track.js
index d8ea507..607ccd7 100644
--- a/js/src/track.js
+++ b/js/src/track.js
@@ -49,6 +49,9 @@ export default class uTrack extends uPositionSet {
this.user = user;
this.plotData = [];
this.maxId = 0;
+ this.maxSpeed = 0;
+ this.maxAltitude = null;
+ this.minAltitude = null;
this.totalMeters = 0;
this.totalSeconds = 0;
this.listItem(id, name);
@@ -66,6 +69,9 @@ export default class uTrack extends uPositionSet {
clearTrackCounters() {
this.maxId = 0;
+ this.maxSpeed = 0;
+ this.maxAltitude = null;
+ this.minAltitude = null;
this.plotData.length = 0;
this.totalMeters = 0;
this.totalSeconds = 0;
@@ -86,6 +92,20 @@ export default class uTrack extends uPositionSet {
return this.plotData.length > 0;
}
+ /**
+ * @return {boolean}
+ */
+ get hasAltitudes() {
+ return this.maxAltitude !== null;
+ }
+
+ /**
+ * @return {boolean}
+ */
+ get hasSpeeds() {
+ return this.maxSpeed > 0;
+ }
+
/**
* Get track data from json
* @param {Object[]} posArr Positions data
@@ -268,11 +288,20 @@ export default class uTrack extends uPositionSet {
this.totalSeconds += position.seconds;
position.totalMeters = this.totalMeters;
position.totalSeconds = this.totalSeconds;
- if (position.altitude != null) {
+ if (position.hasAltitude()) {
this.plotData.push({ x: position.totalMeters, y: position.altitude });
+ if (this.maxAltitude === null || position.altitude > this.maxAltitude) {
+ this.maxAltitude = position.altitude;
+ }
+ if (this.minAltitude === null || position.altitude < this.minAltitude) {
+ this.minAltitude = position.altitude;
+ }
}
if (position.id > this.maxId) {
this.maxId = position.id;
}
+ if (position.hasSpeed() && position.speed > this.maxSpeed) {
+ this.maxSpeed = position.speed;
+ }
}
}
diff --git a/js/src/trackviewmodel.js b/js/src/trackviewmodel.js
index 61b34ac..57fa35d 100644
--- a/js/src/trackviewmodel.js
+++ b/js/src/trackviewmodel.js
@@ -345,11 +345,12 @@ export default class TrackViewModel extends ViewModel {
}
renderSummary() {
- if (!this.state.currentTrack || !this.state.currentTrack.hasPositions) {
+ const track = this.state.currentTrack;
+ if (!track || !track.hasPositions) {
this.model.summary = '';
return;
}
- const last = this.state.currentTrack.positions[this.state.currentTrack.length - 1];
+ const last = track.positions[track.length - 1];
if (this.state.showLatest) {
const today = new Date();
@@ -362,10 +363,21 @@ export default class TrackViewModel extends ViewModel {
${dateString}
${timeString}`;
} else {
- this.model.summary = `
+ let summary = `
${$.getLocaleDistanceMajor(last.totalMeters, true)}
${$.getLocaleDuration(last.totalSeconds)}
`;
+ if (track.hasSpeeds) {
+ summary += `
➚ ${$.getLocaleSpeed(track.maxSpeed, true)}
`;
+ }
+ if (track.hasAltitudes) {
+ let altitudes = `${$.getLocaleAltitude(track.maxAltitude, true)}`;
+ if (track.minAltitude !== track.maxAltitude) {
+ altitudes = `${$.getLocaleAltitude(track.minAltitude)}–${altitudes}`;
+ }
+ summary += `
${altitudes}
`;
+ }
+ this.model.summary = summary;
}
}
diff --git a/js/src/utils.js b/js/src/utils.js
index 694469a..c641cb3 100644
--- a/js/src/utils.js
+++ b/js/src/utils.js
@@ -147,6 +147,27 @@ export default class uUtils {
.concat(opacity).join(',')})`;
}
+ /**
+ * Get rgb color for given scale and intensity
+ * @param {number[]} start Minimum scale color as [ r, g, b ]
+ * @param {number[]} end Maximum scale color as [ r, g, b ]
+ * @param {number} intensity Intensity from 0 to 1
+ * @return {string} Color as rgb() string
+ */
+ static getScaleColor(start, end, intensity) {
+ if (intensity < 0 || intensity > 1) {
+ throw new Error('Invalid value');
+ }
+ const rgb = [];
+ for (let i = 0; i < 3; i++) {
+ if (start[i] < 0 || start[i] > 255 || end[i] < 0 || end[i] > 255) {
+ throw new Error('Invalid value');
+ }
+ rgb[i] = Math.round((end[i] - start[i]) * intensity + start[i]);
+ }
+ return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
+ }
+
/**
* Add link tag with type css
* @param {string} url attribute
diff --git a/js/test/api_openlayers.test.js b/js/test/api_openlayers.test.js
index ef2ac0a..35aca89 100644
--- a/js/test/api_openlayers.test.js
+++ b/js/test/api_openlayers.test.js
@@ -112,8 +112,6 @@ describe('Openlayers map API tests', () => {
expect(_layer.getVisible()).toBe(true);
expect(_layer).toEqual(jasmine.any(ol.layer.VectorLayer));
expect(_layer.getSource()).toEqual(jasmine.any(ol.source.Vector));
- expect(_layer.getStyle().getStroke().getColor()).toBe(uUtils.hexToRGBA(config.strokeColor, config.strokeOpacity));
- expect(_layer.getStyle().getStroke().getWidth()).toBe(config.strokeWeight);
expect(_layer.get('type')).toBe('data');
expect(api.layerTrack).toEqual(_layer);
break;
@@ -570,4 +568,63 @@ describe('Openlayers map API tests', () => {
expect(api.popup.getElement().firstElementChild.innerHTML).toBe('');
expect(mockViewModel.model.markerSelect).toBe(null);
});
+
+ it('should create gradient style', () => {
+ // given
+ const ctx = document.createElement('canvas').getContext('2d');
+ const coordinates = [ [ 0, 0 ], [ 1, 1 ] ];
+ const colors = [ 'white', 'red' ];
+ api.map = mockMap;
+ spyOn(api.map, 'getPixelFromCoordinate').and.callFake((coord) => coord);
+ // when
+ const style = api.getGradientStyle(ctx, coordinates, colors);
+ // then
+ expect(style).toBeInstanceOf(ol.style.Style);
+ expect(style.getGeometry().getCoordinates()).toEqual(coordinates);
+ expect(style.getStroke().getColor()).toBeInstanceOf(CanvasGradient);
+ });
+
+ it('should set default style for track', () => {
+ // given
+ api.layerTrack = new ol.layer.VectorLayer();
+ config.strokeWeight = 1234;
+ config.strokeColor = 'test color';
+ config.strokeOpacity = 0.1234;
+ const color = 'rgba(1, 1, 1, 1)';
+ spyOn(uUtils, 'hexToRGBA').and.returnValue(color);
+ // when
+ api.setTrackDefaultStyle();
+ // then
+ expect(api.layerTrack.getStyle()).toBeInstanceOf(ol.style.Style);
+ expect(api.layerTrack.getStyle().getStroke().getWidth()).toBe(config.strokeWeight);
+ expect(api.layerTrack.getStyle().getStroke().getColor()).toBe(color);
+ expect(uUtils.hexToRGBA).toHaveBeenCalledWith(config.strokeColor, config.strokeOpacity);
+ });
+
+ it('should set gradient style for track', () => {
+ // given
+ const track = TrackFactory.getTrack(3);
+ track.positions[0].speed = 0;
+ track.positions[1].speed = 1;
+ track.positions[2].speed = 2;
+ api.map = mockMap;
+ api.layerTrack = new ol.layer.VectorLayer({
+ source: new ol.source.Vector()
+ });
+ spyOn(uUtils, 'getScaleColor').and.returnValue('test color');
+ spyOn(api, 'getGradientStyle').and.returnValue(new ol.style.Style());
+ const lineFeature = new ol.Feature({ geometry: new ol.geom.LineString([]) });
+ for (let i = 0; i < track.length; i++) {
+ lineFeature.getGeometry().appendCoordinate(ol.proj.fromLonLat([ 0, 0 ]));
+ }
+ api.layerTrack.getSource().addFeature(lineFeature);
+ // when
+ api.setTrackGradientStyle(track, 'speed', { minValue: 0, maxValue: 2, minColor: [ 255, 255, 255 ], maxColor: [ 0, 0, 0 ] });
+ api.layerTrack.getStyle()(lineFeature);
+ // then
+ expect(api.layerTrack.getStyle()).toBeInstanceOf(Function);
+ expect(uUtils.getScaleColor).toHaveBeenCalledTimes(track.length);
+ expect(api.getGradientStyle).toHaveBeenCalledTimes(track.length - 1);
+ });
+
});
diff --git a/js/test/chartviewmodel.test.js b/js/test/chartviewmodel.test.js
index ad9a307..e9828aa 100644
--- a/js/test/chartviewmodel.test.js
+++ b/js/test/chartviewmodel.test.js
@@ -152,14 +152,14 @@ describe('ChartViewModel tests', () => {
];
state.currentTrack = null;
vm.model.buttonVisible = false;
- buttonEl.style.visibility = 'hidden';
+ buttonEl.classList.add('menu-hidden');
// when
vm.setObservers();
state.currentTrack = TrackFactory.getTrack(positions);
// then
expect(vm.render).toHaveBeenCalledTimes(1);
expect(vm.model.buttonVisible).toBe(true);
- expect(buttonEl.style.visibility).toBe('visible');
+ expect(buttonEl.classList.contains('menu-hidden')).toBeFalse();
});
it('should render chart on null track and hide altitudes button', () => {
@@ -171,14 +171,14 @@ describe('ChartViewModel tests', () => {
];
state.currentTrack = TrackFactory.getTrack(positions);
vm.model.buttonVisible = true;
- buttonEl.style.visibility = 'visible';
+ buttonEl.classList.remove('menu-hidden');
// when
vm.setObservers();
state.currentTrack = null;
// then
expect(vm.render).toHaveBeenCalledTimes(1);
expect(vm.model.buttonVisible).toBe(false);
- expect(buttonEl.style.visibility).toBe('hidden');
+ expect(buttonEl.classList.contains('menu-hidden')).toBeTrue();
});
it('should render chart on empty track and hide altitudes button', () => {
@@ -186,36 +186,36 @@ describe('ChartViewModel tests', () => {
spyOn(vm, 'render');
state.currentTrack = TrackFactory.getTrack(2);
vm.model.buttonVisible = true;
- buttonEl.style.visibility = 'visible';
+ buttonEl.classList.remove('menu-hidden');
// when
vm.setObservers();
state.currentTrack = TrackFactory.getTrack(0);
// then
expect(vm.render).toHaveBeenCalledTimes(1);
expect(vm.model.buttonVisible).toBe(false);
- expect(buttonEl.style.visibility).toBe('hidden');
+ expect(buttonEl.classList.contains('menu-hidden')).toBeTrue();
});
it('should render button visible', () => {
// given
vm.model.buttonVisible = false;
- buttonEl.style.visibility = 'hidden';
+ buttonEl.classList.add('menu-hidden');
// when
vm.setObservers();
vm.model.buttonVisible = true;
// then
- expect(buttonEl.style.visibility).toBe('visible');
+ expect(buttonEl.classList.contains('menu-hidden')).toBeFalse();
});
it('should render button hidden', () => {
// given
vm.model.buttonVisible = true;
- buttonEl.style.visibility = 'visible';
+ buttonEl.classList.remove('menu-hidden');
// when
vm.setObservers();
vm.model.buttonVisible = false;
// then
- expect(buttonEl.style.visibility).toBe('hidden');
+ expect(buttonEl.classList.contains('menu-hidden')).toBeTrue();
});
it('should render chart container visible and render chart', () => {
diff --git a/js/test/configdialogmodel.test.js b/js/test/configdialogmodel.test.js
index 01d959e..de912e0 100644
--- a/js/test/configdialogmodel.test.js
+++ b/js/test/configdialogmodel.test.js
@@ -153,8 +153,8 @@ describe('ConfigDialogModel tests', () => {
// given
spyOn(cm, 'validate').and.returnValue(true);
spyOn(config, 'save').and.returnValue(Promise.resolve());
- cm.model.layerId = '1';
cm.init();
+ cm.model.layerId = '1';
const button = cm.dialog.element.querySelector("[data-bind='onSave']");
// when
button.click();
diff --git a/js/test/lang.test.js b/js/test/lang.test.js
index 7bd576e..7ca9695 100644
--- a/js/test/lang.test.js
+++ b/js/test/lang.test.js
@@ -37,7 +37,8 @@ describe('Lang tests', () => {
unitDistance: 'unitd',
factorDistanceMajor: 0.55,
unitDistanceMajor: 'unitdm',
- unitDay: 'unitday'
+ unitDay: 'unitday',
+ unitAltitude: 'unitamsl'
};
mockStrings = {
string1: 'łańcuch1',
@@ -45,7 +46,8 @@ describe('Lang tests', () => {
units: 'jp',
unitd: 'jo',
unitdm: 'jo / 1000',
- unitday: 'd'
+ unitday: 'd',
+ unitamsl: 'a.s.l.'
}
});
@@ -96,7 +98,7 @@ describe('Lang tests', () => {
// when
lang.init(mockConfig, mockStrings);
// then
- expect(lang.getLocaleSpeed(value, false)).toBe(330);
+ expect(lang.getLocaleSpeed(value, false)).toBe('330');
});
it('should return localized speed value with unit', () => {
@@ -110,7 +112,7 @@ describe('Lang tests', () => {
// when
lang.init(mockConfig, mockStrings);
// then
- expect(lang.getLocaleDistanceMajor(value, false)).toBe(0.55);
+ expect(lang.getLocaleDistanceMajor(value, false)).toBe('0.55');
});
it('should return localized distance major value with unit', () => {
@@ -124,7 +126,7 @@ describe('Lang tests', () => {
// when
lang.init(mockConfig, mockStrings);
// then
- expect(lang.getLocaleDistance(value, false)).toBe(1300);
+ expect(lang.getLocaleDistance(value, false)).toBe('1,300');
});
it('should return localized distance value with unit', () => {
@@ -138,28 +140,28 @@ describe('Lang tests', () => {
// when
lang.init(mockConfig, mockStrings);
// then
- expect(lang.getLocaleDistance(value, false)).toBe(1300);
+ expect(lang.getLocaleAltitude(value, false)).toBe('1,300');
});
it('should return localized altitude value with unit', () => {
// when
lang.init(mockConfig, mockStrings);
// then
- expect(lang.getLocaleDistance(value, true)).toBe(`1,300 ${mockStrings.unitd}`);
+ expect(lang.getLocaleAltitude(value, true)).toBe(`1,300 ${mockStrings.unitd} ${mockStrings.unitamsl}`);
});
it('should return localized accuracy value', () => {
// when
lang.init(mockConfig, mockStrings);
// then
- expect(lang.getLocaleDistance(value, false)).toBe(1300);
+ expect(lang.getLocaleAccuracy(value, false)).toBe('1,300');
});
it('should return localized accuracy value with unit', () => {
// when
lang.init(mockConfig, mockStrings);
// then
- expect(lang.getLocaleDistance(value, true)).toBe(`1,300 ${mockStrings.unitd}`);
+ expect(lang.getLocaleAccuracy(value, true)).toBe(`1,300 ${mockStrings.unitd}`);
});
it('should return localized time duration', () => {
diff --git a/js/test/mapviewmodel.test.js b/js/test/mapviewmodel.test.js
index bba25fa..ea18a68 100644
--- a/js/test/mapviewmodel.test.js
+++ b/js/test/mapviewmodel.test.js
@@ -57,6 +57,8 @@ describe('MapViewModel tests', () => {
'zoomToBounds': { /* ignored */ },
'zoomToExtent': { /* ignored */ },
'displayTrack': Promise.resolve(),
+ 'setTrackDefaultStyle': { /* ignored */ },
+ 'setTrackGradientStyle': { /* ignored */ },
'clearMap': { /* ignored */ },
'updateSize': { /* ignored */ }
});
@@ -206,6 +208,7 @@ describe('MapViewModel tests', () => {
// given
vm.api = mockApi;
vm.bindAll();
+ vm.setObservers();
// when
menuButtonEl.click();
// then
diff --git a/js/test/utils.test.js b/js/test/utils.test.js
index 1fea76c..dc1a5c8 100644
--- a/js/test/utils.test.js
+++ b/js/test/utils.test.js
@@ -366,4 +366,15 @@ describe('Utils tests', () => {
expect(result).toBeFalse();
});
+ it('should create color from scale and intensity', () => {
+ // given
+ const start = [ 0, 128, 255 ];
+ const stop = [ 255, 128, 0 ];
+ const intensity = 0.5;
+ // when
+ const color = uUtils.getScaleColor(start, stop, intensity);
+ // then
+ expect(color).toBe('rgb(128, 128, 128)');
+ });
+
});
diff --git a/lang/en.php b/lang/en.php
index ed6b78d..5ebb67d 100644
--- a/lang/en.php
+++ b/lang/en.php
@@ -137,6 +137,7 @@ $lang["allusers"] = "All users";
$lang["unitday"] = "d"; // abbreviation for days, like 4 d 11:11:11
$lang["unitkmh"] = "km/h"; // kilometer per hour
$lang["unitm"] = "m"; // meter
+$lang["unitamsl"] = "a.s.l."; // above mean see level
$lang["unitkm"] = "km"; // kilometer
$lang["unitmph"] = "mph"; // mile per hour
$lang["unitft"] = "ft"; // feet
@@ -169,4 +170,5 @@ $lang["add"] = "Add";
$lang["edit"] = "Edit";
$lang["delete"] = "Delete";
$lang["settings"] = "Settings";
+$lang["trackcolor"] = "Track color";
?>