Feature: track color by speed, altitude; extended summary

This commit is contained in:
Bartek Fabiszewski 2020-06-16 19:19:01 +02:00
parent d34cbbc9f0
commit 5ae7753353
20 changed files with 445 additions and 83 deletions

1
images/altitude.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M21.103 14.598c-.509-1.504-2.306-2.497-3.806-1.91l-6.235-10.688-11.062 20h20.25c2.067 0 3.75-1.682 3.75-3.75 0-1.774-1.239-3.265-2.897-3.652zm-.853 5.402h-3.26c-1.515-.008-2.505-1.653-1.708-3.009l-3.595-6.334-1.078 1.906-1-1.906-3.026 3.635 4.521-8.344 5.521 9.552c.875-1.781 3.328-.688 2.688 1.104 1.271-.5 2.687.224 2.687 1.646 0 .965-.785 1.75-1.75 1.75zm-2.236-8.579l-2.656-4.625.867-.498 2.656 4.625-.867.498zm3.298 1.579l-2.656-4.625.867-.498 2.656 4.625-.867.498z"/></svg>

After

Width:  |  Height:  |  Size: 584 B

4
images/speed.svg Normal file
View File

@ -0,0 +1,4 @@
<svg
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="white" d="M20.043 11.76c-.141-.427-.314-.844-.516-1.242l-2.454 1.106c.217.393.39.81.517 1.242l2.453-1.106zm-12.572-.904c.271-.354.579-.674.918-.957l-1.89-1.968c-.328.293-.637.614-.919.957l1.891 1.968zm1.714-1.514c.38-.221.781-.396 1.198-.523l-1.033-2.569c-.412.142-.813.317-1.2.524l1.035 2.568zm-2.759 3.615c.121-.435.287-.854.498-1.25l-2.47-1.066c-.196.403-.364.823-.498 1.25l2.47 1.066zm9.434-6.2c-.387-.205-.79-.379-1.2-.519l-1.023 2.573c.418.125.82.299 1.2.519l1.023-2.573zm2.601 2.131c-.281-.342-.59-.664-.918-.957l-1.891 1.968c.34.283.648.604.919.957l1.89-1.968zm-5.791-3.06c-.219-.017-.437-.026-.648-.026-.213 0-.432.009-.65.026v2.784c.216-.025.434-.038.65-.038.215 0 .434.013.648.038v-2.784zm11.33 8.172c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 2.583.816 5.042 2.205 7h19.59c1.389-1.958 2.205-4.417 2.205-7zm-9.08 5c-.007-1.086-.606-2.031-1.496-2.522l-1.402-6.571-1.402 6.571c-.889.491-1.489 1.436-1.496 2.522h-5.821c-.845-1.5-1.303-3.242-1.303-5 0-5.514 4.486-10 10-10s10 4.486 10 10c0 1.758-.458 3.5-1.303 5h-5.777z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -85,8 +85,14 @@
<div id="summary" class="section" data-bind="summary"></div>
<div class="section" data-bind="trackColor">
<div class="menu-title"><?= $lang['trackcolor'] ?></div>
<input id="color-speed" type="checkbox" data-bind="speedVisible"> <label for="color-speed"><?= $lang['speed'] ?></label><br>
<input id="color-altitude" type="checkbox" data-bind="altitudeVisible"> <label for="color-altitude"><?= $lang['altitude'] ?></label><br>
</div>
<div id="other" class="section">
<a id="altitudes" data-bind="onChartToggle"><?= $lang['chart'] ?></a>
<a id="altitudes" class="menu-link menu-hidden" data-bind="onChartToggle"><?= $lang['chart'] ?></a>
</div>
<div>

View File

@ -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');
}
}

View File

@ -103,6 +103,7 @@ export default class uConfig {
this.unitDistanceMajor = 'unitkm';
}
this.unitDay = 'unitday';
this.unitAltitude = 'unitamsl';
}
/**

View File

@ -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);
}

View File

@ -142,8 +142,10 @@ export default class GoogleMapsApi {
const promise = new Promise((resolve) => {
google.maps.event.addListenerOnce(this.map, 'tilesloaded', () => {
console.log('tilesloaded');
if (this.map) {
this.saveState();
this.map.addListener('idle', this.saveState);
}
resolve();
})
});
@ -370,6 +372,25 @@ export default class GoogleMapsApi {
// ignore for google API
}
/**
* 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;
}

View File

@ -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');
if (this.map) {
this.saveState();
this.map.on('moveend', this.saveState);
}
resolve();
});
});

View File

@ -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 {
<g><path stroke="black" fill="${fill}" d="${MapViewModel.getMarkerPath(isLarge)}"/>${isExtra ? MapViewModel.getMarkerExtra(isLarge) : ''}</g></svg>`;
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}
onMapResize() {
if (this.api) {
this.api.updateSize();
}
}
}

View File

@ -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}
*/

View File

@ -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;
}
}
}

View File

@ -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 = `
<div class="menu-title">${$._('summary')}</div>
<div><img class="icon" alt="${$._('tdistance')}" title="${$._('tdistance')}" src="images/distance.svg"> ${$.getLocaleDistanceMajor(last.totalMeters, true)}</div>
<div><img class="icon" alt="${$._('ttime')}" title="${$._('ttime')}" src="images/time.svg"> ${$.getLocaleDuration(last.totalSeconds)}</div>`;
if (track.hasSpeeds) {
summary += `<div><img class="icon" alt="${$._('speed')}" title="${$._('speed')}" src="images/speed.svg"><b>&#10138;</b> ${$.getLocaleSpeed(track.maxSpeed, true)}</div>`;
}
if (track.hasAltitudes) {
let altitudes = `${$.getLocaleAltitude(track.maxAltitude, true)}`;
if (track.minAltitude !== track.maxAltitude) {
altitudes = `${$.getLocaleAltitude(track.minAltitude)}&ndash;${altitudes}`;
}
summary += `<div><img class="icon" alt="${$._('altitude')}" title="${$._('altitude')}" src="images/altitude.svg"> ${altitudes}</div>`;
}
this.model.summary = summary;
}
}

View File

@ -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

View File

@ -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);
});
});

View File

@ -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', () => {

View File

@ -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();

View File

@ -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', () => {

View File

@ -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

View File

@ -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)');
});
});

View File

@ -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";
?>