pull/147/head
Candid Dauth 2021-03-19 22:16:57 +01:00
rodzic 0fe03d560a
commit 1354c37ddf
35 zmienionych plików z 392 dodań i 286 usunięć

Wyświetl plik

@ -2,7 +2,7 @@ import fm from '../../app';
import $ from 'jquery';
import L from 'leaflet';
import ng from 'angular';
import heightgraph from '../../leaflet/heightgraph';
import heightgraph from '../../../src/utils/heightgraph';
import saveAs from 'file-saver';
import css from './lines.scss';
@ -41,8 +41,7 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r
}
});
let elevationPlot = new heightgraph();
elevationPlot._map = map.map;
var linesUi = {
_addLine: function(line, _doNotRerenderPopup) {
@ -105,26 +104,6 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r
if(linesById[line.id])
linesById[line.id].setStyle({ highlight: true });
scope.$watch("line.trackPoints", () => {
scope.elevationStats = null;
if(line.ascent != null && line.trackPoints) {
elevationPlot.addData(line.extraInfo, line.trackPoints);
scope.elevationStats = heightgraph.createElevationStats(line.extraInfo, line.trackPoints);
}
}, true);
let drawElevationPlot = () => {
let el = template.find(".fm-elevation-plot").empty();
if(line.ascent != null) {
let content = template.filter(".content");
elevationPlot.options.width = content.find(".tab-pane.active").width();
elevationPlot.options.height = content.height() - content.find(".tab-pane.active dl").outerHeight(true);
el.append($(elevationPlot.onAdd(map.map)));
}
};
template.filter(".content").on("resizeend", drawElevationPlot);
setTimeout(drawElevationPlot, 0);
},

Wyświetl plik

@ -189,32 +189,6 @@ fm.app.factory("fmMapRoute", function(fmUtils, $uibModal, $compile, $timeout, $r
let scope = $rootScope.$new();
scope.client = map.client;
scope.addToMap = function(type) {
if(openInfoBox) {
openInfoBox.hide();
}
if(type == null) {
for(var i in map.client.types) {
if(map.client.types[i].type == "line") {
type = map.client.types[i];
break;
}
}
}
map.linesUi.createLine(type, map.client.route.routePoints, { mode: map.client.route.mode });
map.mapEvents.$broadcast("routeClear");
map.client.clearRoute().catch((err) => {
map.messages.showMessage("danger", err);
});
};
scope.export = function(useTracks) {
routeUi.exportRoute(useTracks);
};
let template = $(require("./view-route.html"));
openInfoBox = map.infoBox.show({
@ -313,13 +287,7 @@ fm.app.factory("fmMapRoute", function(fmUtils, $uibModal, $compile, $timeout, $r
},
exportRoute(useTracks) {
map.client.exportRoute({
format: useTracks ? "gpx-trk" : "gpx-rte"
}).then((exported) => {
saveAs(new Blob([exported], {type: "application/gpx+xml"}), "FacilMap route.gpx");
}).catch((err) => {
map.messages.showMessage("danger", err);
});
},
getMarker(idx) {

Wyświetl plik

@ -46,7 +46,6 @@
"leaflet-mouse-position": "^1.0.4",
"leaflet.heightgraph": "^1.4.0",
"leaflet.locatecontrol": "^0.73.0",
"linkifyjs": "^3.0.0-beta.3",
"lodash": "^4.17.21",
"markdown": "^0.5.0",
"osmtogeojson": "^3.0.0-beta.4",
@ -63,6 +62,7 @@
},
"devDependencies": {
"@types/copy-webpack-plugin": "^6.4.0",
"@types/file-saver": "^2.0.1",
"@types/hammerjs": "^2.0.39",
"@types/jest": "^26.0.21",
"@types/jquery": "^3.5.5",

Wyświetl plik

@ -9,10 +9,12 @@ import { showErrorToast } from "../../utils/toasts";
import EditLine from "../edit-line/edit-line";
import ElevationStats from "../ui/elevation-stats/elevation-stats";
import { MapComponents, MapContext } from "../leaflet-map/leaflet-map";
import ElevationPlot from "../ui/elevation-plot/elevation-plot";
import Icon from "../ui/icon/icon";
@WithRender
@Component({
components: { EditLine, ElevationStats }
components: { EditLine, ElevationPlot, ElevationStats, Icon }
})
export default class LineInfo extends Vue {
@ -23,16 +25,12 @@ export default class LineInfo extends Vue {
@Prop({ type: IdType, required: true }) lineId!: ID;
isSaving = false;
showElevationPlot = false;
get line(): Line | undefined {
return this.client.lines[this.lineId];
}
get elevationStats(): undefined {
// TODO
return undefined;
}
async deleteLine(): Promise<void> {
this.$bvToast.hide("fm-line-info-delete");

Wyświetl plik

@ -1,23 +1,34 @@
<div class="fm-line-info" v-if="line">
<h2>{{line.name}}</h2>
<div class="d-flex align-items-center">
<h2 class="flex-grow-1">{{line.name}}</h2>
<b-button
v-if="line.ascent != null"
:pressed.sync="showElevationPlot"
:title="`${showElevationPlot ? 'Hide' : 'Show'} elevation plot`"
v-b-tooltip
><Icon icon="chart-line" :alt="`${showElevationPlot ? 'Hide' : 'Show'} elevation plot`"></Icon></b-button>
</div>
<dl>
<dt class="distance">Distance</dt>
<dd class="distance">{{line.distance | round(2)}} km <span v-if="line.time != null">({{line.time | fmFormatTime}} h {{line.mode | fmRouteMode}})</span></dd>
<template v-if="line.ascent != null">
<dt class="elevation">Climb/drop</dt>
<dd class="elevation"><ElevationStats :route="line" :stats="elevationStats"></ElevationStats></dd>
<dd class="elevation"><ElevationStats :route="line"></ElevationStats></dd>
</template>
<template v-for="field in client.types[line.typeId].fields">
<template v-if="line.ascent == null || !showElevationPlot" v-for="field in client.types[line.typeId].fields">
<dt>{{field.name}}</dt>
<dd v-html="$options.filters.fmFieldContent(line.data[field.name], field)"></dd>
</template>
</dl>
<div class="buttons">
<ElevationPlot :route="line" v-if="line.ascent != null && showElevationPlot"></ElevationPlot>
<div class="buttons" v-if="line.ascent == null || !showElevationPlot">
<b-button v-if="!client.readonly" size="sm" v-b-modal.fm-line-info-edit :disabled="isSaving || mapContext.interaction">Edit data</b-button>
<!-- <button ng-if="!client.readonly && canMoveLine" type="button" class="btn btn-default btn-sm" ng-click="move()" ng-disabled="saving || client.interaction">Move</button> -->
<!-- <b-button v-if="!client.readonly" size="sm" @click="move()" :disabled="isSaving || mapContext.interaction">Move</b-button> -->
<b-button v-if="!client.readonly" size="sm" @click="deleteLine()" :disabled="isSaving || mapContext.interaction">Remove</b-button>
<!--
<div uib-dropdown keyboard-nav="true" class="dropup">

Wyświetl plik

@ -12,7 +12,7 @@
}
.btn-toolbar {
* + * {
> * + * {
margin-left: 0.5rem;
}
}

Wyświetl plik

@ -23,6 +23,9 @@ Vue.use(BootstrapVue, {
modifiers: {
preventOverflow: {
enabled: false
},
hide: {
enabled: false
}
}
},

Wyświetl plik

@ -2,6 +2,7 @@
padding: 0.5rem;
display: flex;
flex-direction: column;
flex-grow: 1;
.input-group {
position: static;

Wyświetl plik

@ -2,6 +2,13 @@
display: flex;
flex-direction: column;
min-height: 0;
flex-grow: 1;
form {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.form-group {
margin-bottom: 0;

Wyświetl plik

@ -1,13 +1,13 @@
import WithRender from "./route-form.vue";
import "./route-form.scss";
import Vue from "vue";
import { Component, Prop, Watch } from "vue-property-decorator";
import { Component, Prop, Ref, Watch } from "vue-property-decorator";
import Icon from "../ui/icon/icon";
import { InjectClient, InjectMapComponents, InjectMapContext } from "../../utils/decorators";
import { isSearchId, round } from "facilmap-utils";
import Client from "facilmap-client";
import { showErrorToast } from "../../utils/toasts";
import { FindOnMapResult, SearchResult } from "facilmap-types";
import { ExportFormat, FindOnMapResult, SearchResult, Type } from "facilmap-types";
import { MapComponents, MapContext } from "../leaflet-map/leaflet-map";
import { getMarkerIcon, MarkerLayer, RouteLayer } from "facilmap-leaflet";
import { getZoomDestinationForRoute, flyTo } from "../../utils/zoom";
@ -16,6 +16,9 @@ import draggable from "vuedraggable";
import RouteMode from "../ui/route-mode/route-mode";
import DraggableLines from "leaflet-draggable-lines";
import { throttle } from "lodash";
import ElevationStats from "../ui/elevation-stats/elevation-stats";
import ElevationPlot from "../ui/elevation-plot/elevation-plot";
import { saveAs } from 'file-saver';
type SearchSuggestion = SearchResult;
type MapSuggestion = FindOnMapResult & { kind: "marker" };
@ -70,7 +73,7 @@ function getIcon(i: number, length: number, highlight = false) {
@WithRender
@Component({
components: { draggable, Icon, RouteMode }
components: { draggable, ElevationPlot, ElevationStats, Icon, RouteMode }
})
export default class RouteForm extends Vue {
@ -78,6 +81,8 @@ export default class RouteForm extends Vue {
@InjectClient() client!: Client;
@InjectMapContext() mapContext!: MapContext;
@Ref() submitButton!: HTMLButtonElement;
@Prop({ type: Boolean, default: true }) active!: boolean;
routeLayer!: RouteLayer;
@ -194,6 +199,10 @@ export default class RouteForm extends Vue {
return !!this.client.route;
}
get lineTypes(): Type[] {
return Object.values(this.client.types).filter((type) => type.type == "line");
}
addDestination(): void {
this.destinations.push({
query: ""
@ -402,11 +411,43 @@ export default class RouteForm extends Vue {
this.client.clearRoute();
}
clear(): void {
this.reset();
this.destinations = [
{ query: "" },
{ query: "" }
];
}
handleSubmit(event: Event): void {
(document.activeElement as any)?.blur?.();
this.submitButton.focus();
this.route(true);
}
async addToMap(type: Type): Promise<void> {
this.$bvToast.hide("fm-route-form-add-error");
try {
const line = await this.client.addLine({ typeId: type.id, routePoints: this.client.route!.routePoints, mode: this.client.route!.mode });
this.clear();
this.mapComponents.selectionHandler.setSelectedItems([{ type: "line", id: line.id }], true);
} catch (err) {
showErrorToast(this, "fm-route-form-add-error", "Error adding line", err);
}
}
async exportRoute(format: ExportFormat): Promise<void> {
this.$bvToast.hide("fm-route-form-export-error");
try {
const exported = await this.client.exportRoute({ format });
saveAs(new Blob([exported], { type: "application/gpx+xml" }), "FacilMap route.gpx");
} catch(err) {
showErrorToast(this, "fm-route-form-export-error", "Error exporting route", err);
}
}
/* const routeUi = searchUi.routeUi = {
setQueries: function(queries) {
scope.submittedQueries = null;

Wyświetl plik

@ -63,7 +63,7 @@
<RouteMode v-model="routeMode" :tabindex="destinations.length+2" @input="reroute(false)"></RouteMode>
<b-button type="submit" variant="primary" :tabindex="destinations.length+7" class="flex-grow-1">Go!</b-button>
<b-button type="submit" variant="primary" :tabindex="destinations.length+7" class="flex-grow-1" ref="submitButton">Go!</b-button>
<b-button v-if="hasRoute" type="button" :tabindex="destinations.length+8" @click="reset()" title="Clear route" v-b-tooltip><Icon icon="remove" alt="Clear"></Icon></b-button>
</b-button-toolbar>
@ -79,28 +79,35 @@
<dl>
<dt>Distance</dt>
<dd>{{client.route.distance | round(2)}} km <span v-if="client.route.time != null">({{client.route.time | fmFormatTime}} h {{client.route.mode | fmRouteMode}})</span></dd>
<!-- <dt class="elevation" v-if="client.route.ascent != null">Climb/drop</dt>
<dd class="elevation" v-if="client.route.ascent != null"><ElevationStats :route="client.route" :stats="elevationStats"></ElevationStats></dd> -->
</dl>
<!-- <div class="fm-elevation-plot" ng-show="client.route.ascent != null"></div> -->
<!-- <div class="buttons" ng-if="!client.readonly">
<div uib-dropdown keyboard-nav="true" ng-if="!client._editingLineId && (client.types | fmPropertyCount:{type:'line'}) > 1" class="dropup">
<button id="add-type-button" type="button" class="btn btn-default btn-sm" uib-dropdown-toggle>Add to map <span class="caret"></span></button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="add-type-button">
<li role="menuitem" ng-repeat="type in client.types | fmObjectFilter:{type:'line'}"><a href="javascript:" ng-click="addToMap(type)">{{type.name}}</a></li>
</ul>
</div>
<button ng-if="!client._editingLineId && (client.types | fmPropertyCount:{type:'line'}) == 1" type="button" class="btn btn-default" ng-click="addToMap()">Add to map</button>
<div uib-dropdown keyboard-nav="true" class="dropup">
<button type="button" class="btn btn-default btn-sm" uib-dropdown-toggle>Export <span class="caret"></span></button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu">
<li role="menuitem"><a href="javascript:" ng-click="export(true)" uib-tooltip="GPX files can be opened with most navigation software. In track mode, the calculated route is saved in the file."tooltip-placement="left">Export as GPX track</a></li>
<li role="menuitem"><a href="javascript:" ng-click="export(false)" uib-tooltip="GPX files can be opened with most navigation software. In route mode, only the start/end/via points are saved in the file, and the navigation software needs to calculate the route."tooltip-placement="left">Export as GPX route</a></li>
</ul>
</div>
</div> -->
<template v-if="client.route.ascent != null">
<dt>Climb/drop</dt>
<dd><ElevationStats :route="client.route"></ElevationStats></dd>
</template>
</dl>
<ElevationPlot :route="client.route" v-if="client.route.ascent != null"></ElevationPlot>
<b-button-toolbar v-if="!client.readonly">
<b-dropdown v-if="lineTypes.length > 1" text="Add to map" size="sm">
<b-dropdown-item v-for="type in lineTypes" href="javascript:" @click="addToMap(type)">{{type.name}}</b-dropdown-item>
</b-dropdown>
<b-button v-if="lineTypes.length == 1" @click="addToMap(lineTypes[0])" size="sm">Add to map</b-button>
<b-dropdown text="Export" size="sm">
<b-dropdown-item
href="javascript:"
@click="exportRoute('gpx-trk')"
title="GPX files can be opened with most navigation software. In track mode, the calculated route is saved in the file."
v-b-tooltip
>Export as GPX track</b-dropdown-item>
<b-dropdown-item
href="javascript:"
@click="exportRoute('gpx-rte')"
title="GPX files can be opened with most navigation software. In route mode, only the start/end/via points are saved in the file, and the navigation software needs to calculate the route."
v-b-tooltip
>Export as GPX route</b-dropdown-item>
</b-dropdown>
</b-button-toolbar>
</template>
</b-form>
</div>

Wyświetl plik

@ -35,6 +35,10 @@
min-height: 0;
}
.tabs, .tab-content, .tab-pane {
flex-grow: 1;
}
.card-header {
display: flex;
flex-direction: column;

Wyświetl plik

@ -1,6 +1,6 @@
import WithRender from "./search-box.vue";
import Vue from "vue";
import { Component, Ref } from "vue-property-decorator";
import { Component, ProvideReactive, Ref } from "vue-property-decorator";
import "./search-box.scss";
import context from "../context";
import $ from "jquery";
@ -10,10 +10,12 @@ import SearchFormTab from "../search-form/search-form-tab";
import MarkerInfoTab from "../marker-info/marker-info-tab";
import LineInfoTab from "../line-info/line-info-tab";
import hammer from "hammerjs";
import { InjectMapComponents } from "../../utils/decorators";
import { InjectMapComponents, SEARCH_BOX_CONTEXT_INJECT_KEY } from "../../utils/decorators";
import { MapComponents } from "../leaflet-map/leaflet-map";
import RouteFormTab from "../route-form/route-form-tab";
export type SearchBoxContext = Vue;
@WithRender
@Component({
components: { Icon, LineInfoTab, MarkerInfoTab, RouteFormTab, SearchFormTab }
@ -22,6 +24,8 @@ export default class SearchBox extends Vue {
@InjectMapComponents() mapComponents!: MapComponents;
@ProvideReactive(SEARCH_BOX_CONTEXT_INJECT_KEY) searchBoxContext = new Vue();
@Ref() tabsComponent!: any;
@Ref() searchBox!: HTMLElement;
@Ref() resizeHandle!: HTMLElement;
@ -120,15 +124,18 @@ export default class SearchBox extends Vue {
this.resizeStartWidth = this.searchBox.offsetWidth;
this.resizeStartHeight = this.searchBox.offsetHeight;
this.$root.$emit('bv::hide::tooltip');
this.searchBoxContext.$emit("resizestart");
}
handleResizeMove(event: any): void {
this.searchBox.style.width = `${this.resizeStartWidth + event.deltaX}px`;
this.searchBox.style.height = `${this.resizeStartHeight + event.deltaY}px`;
this.searchBoxContext.$emit("resize");
}
handleResizeEnd(event: any): void {
this.isResizing = false;
this.searchBoxContext.$emit("resizeend");
}
handleResizeClick(): void {

Wyświetl plik

@ -2,7 +2,7 @@
<b-input-group :id="`${effId}-input-group`">
<b-input-group-prepend>
<b-input-group-text :style="{ backgroundColor: `#${value}` }">
<span style="width: 24px"></span>
<span style="width: 1.4em"></span>
</b-input-group-text>
</b-input-group-prepend>
<b-form-input autocomplete="off" v-bind="$props" v-on="$listeners" @keydown.esc="handleEscape"></b-form-input>

Wyświetl plik

@ -0,0 +1,9 @@
.fm-elevation-plot {
flex-grow: 1;
flex-basis: 12rem;
overflow: hidden;
.heightgraph-toggle, .heightgraph-close-icon {
display: none !important;
}
}

Wyświetl plik

@ -0,0 +1,61 @@
import WithRender from "./elevation-plot.vue";
import Vue from "vue";
import { Component, Prop, Ref, Watch } from "vue-property-decorator";
import { InjectMapComponents, InjectSearchBoxContext } from "../../../utils/decorators";
import { MapComponents } from "../../leaflet-map/leaflet-map";
import FmHeightgraph from "../../../utils/heightgraph";
import { LineWithTrackPoints, RouteWithTrackPoints } from "facilmap-client";
import $ from "jquery";
import "./elevation-plot.scss";
import { SearchBoxContext } from "../../search-box/search-box";
@WithRender
@Component({})
export default class ElevationPlot extends Vue {
@InjectMapComponents() mapComponents!: MapComponents;
@InjectSearchBoxContext() searchBoxContext?: SearchBoxContext;
@Ref() container!: HTMLElement;
@Prop({ type: Object, required: true }) route!: RouteWithTrackPoints | LineWithTrackPoints;
elevationPlot!: FmHeightgraph;
mounted(): void {
this.elevationPlot = new FmHeightgraph();
this.elevationPlot._map = this.mapComponents.map;
this.handleTrackPointsChange();
this.container.append(this.elevationPlot.onAdd(this.mapComponents.map));
this.handleResize();
if (this.searchBoxContext)
this.searchBoxContext.$on("resizeend", this.handleResize);
$(window).on("resize", this.handleResize);
}
beforeDestroy(): void {
if (this.searchBoxContext)
this.searchBoxContext.$off("resizeend", this.handleResize);
$(window).off("resize", this.handleResize);
this.elevationPlot.onRemove(this.mapComponents.map);
}
@Watch("route.trackPoints")
handleTrackPointsChange(): void {
if(this.route.trackPoints)
this.elevationPlot.addData(this.route.extraInfo, this.route.trackPoints);
}
handleResize(): void {
this.elevationPlot.resize({ width: this.container.offsetWidth, height: this.container.offsetHeight });
}
}

Wyświetl plik

@ -0,0 +1 @@
<div class="fm-elevation-plot" ref="container"></div>

Wyświetl plik

@ -1,20 +1,24 @@
import { Line, Route } from "facilmap-types";
import Vue from "vue";
import { Component, Prop } from "vue-property-decorator";
import WithRender from "./elevation-stats.vue";
import { sortBy } from "lodash";
import { LineWithTrackPoints, RouteWithTrackPoints } from "facilmap-client";
import { createElevationStats } from "../../../utils/heightgraph";
import Icon from "../icon/icon";
@WithRender
@Component({})
@Component({
components: { Icon }
})
export default class ElevationStats extends Vue {
@Prop({ type: Object, required: true }) route!: Line | Route;
@Prop({ type: Object }) stats: any;
@Prop({ type: Object, required: true }) route!: LineWithTrackPoints | RouteWithTrackPoints;
id = Date.now();
get statsArr(): any {
return this.stats && sortBy(Object.keys(this.stats).map((i) => ({ i: Number(i), distance: this.stats[i] })), 'i');
const stats = createElevationStats(this.route.extraInfo, this.route.trackPoints)
return stats && sortBy((Object.keys(stats) as any as number[]).map((i) => ({ i: Number(i), distance: stats[i] })), 'i');
}
}

Wyświetl plik

@ -2,15 +2,15 @@
<Icon icon="triangle-top" alt="Ascent"></Icon> {{route.ascent}} m / <Icon icon="triangle-bottom" alt="Descent"></Icon> {{route.descent}} m
<b-popover :target="`fm-elevation-stats-${id}`" placement="top" triggers="hover" custom-class="fm-elevation-stats-popover">
<dl class="row">
<dt class="col-sm-4">Total ascent</dt>
<dd class="col-sm-8">{{route.ascent}} m</dd>
<dt class="col-sm-6">Total ascent</dt>
<dd class="col-sm-6">{{route.ascent}} m</dd>
<dt class="col-sm-4">Total descent</dt>
<dd class="col-sm-8">{{route.descent}} m</dd>
<dt class="col-sm-6">Total descent</dt>
<dd class="col-sm-6">{{route.descent}} m</dd>
<template v-for="stat in statsArr">
<dt class="col-sm-4">{{stat.i == 0 ? '0%' : stat.i < 0 ? "≤ "+stat.i+"%" : "≥ "+stat.i+"%"}}</dt>
<dd class="col-sm-8">{{stat.distance | round(2)}} km</dd>
<dt class="col-sm-6">{{stat.i == 0 ? '0%' : stat.i < 0 ? "≤ "+stat.i+"%" : "≥ "+stat.i+"%"}}</dt>
<dd class="col-sm-6">{{stat.distance | round(2)}} km</dd>
</template>
</dl>
</b-popover>

Wyświetl plik

@ -11,10 +11,10 @@ export default class Icon extends Vue {
@Prop({ type: String }) icon!: string | undefined;
@Prop({ type: String }) alt?: string; // TODO
@Prop({ type: String }) size?: string;
@Prop({ type: String, default: "1.4em" }) size!: string;
get iconCode(): string {
return getSymbolHtml("currentColor", this.size || "1.4em", this.icon);
return getSymbolHtml("currentColor", this.size, this.icon);
}
}

Wyświetl plik

@ -3,18 +3,18 @@
padding-left: 10px;
padding-right: 10px;
}
}
.dropdown-menu {
width: 380px;
font-size: 0; /* https://stackoverflow.com/a/5647640/242365 */
.fm-route-mode-customize {
width: 380px;
font-size: 0; /* https://stackoverflow.com/a/5647640/242365 */
li {
font-size: 14px;
li {
font-size: 14px;
&.column {
display: inline-block;
width: 50%;
}
&.column {
display: inline-block;
width: 50%;
}
}
}

Wyświetl plik

@ -46,22 +46,21 @@ const constants: {
},
types: {
car: [""],
bicycle: ["", "road", "safe", "mountain", "tour", "electric"],
car: ["", "hgv"],
bicycle: ["", "road", "mountain", "electric"],
pedestrian: ["", "hiking", "wheelchair"],
"": [""]
},
typeText: {
car: {
"": "Car"
"": "Car",
"hgv": "HGV"
},
bicycle: {
"": "Bicycle",
road: "Road bike",
safe: "Safe cycling",
mountain: "Mountain bike",
tour: "Touring bike",
electric: "Electric bike"
},
pedestrian: {
@ -82,32 +81,26 @@ const constants: {
recommended: "Recommended"
},
avoid: ["highways", "tollways", "ferries", "tunnels", "pavedroads", "unpavedroads", "tracks", "fords", "steps", "hills"],
avoid: ["highways", "tollways", "ferries", "fords", "steps"],
// driving: highways, tollways, ferries
// cycling: ferries, steps, fords
// foot: ferries, fords, steps
// wheelchair: ferries, steps
avoidAllowed: {
highways: (mode) => (mode == "car"),
tollways: (mode) => (mode == "car"),
ferries: (mode) => (!!mode),
tunnels: (mode) => (mode == "car"),
pavedroads: (mode) => (mode == "car" || mode == "bicycle"),
unpavedroads: (mode) => (mode == "car" || mode == "bicycle"),
tracks: (mode) => (mode == "car"),
fords: (mode, type) => (!!mode && (mode != "pedestrian" || type != "wheelchair")),
steps: (mode) => (!!mode && mode != "car"),
hills: (mode, type) => (!!mode && mode != "car" && (mode != "pedestrian" || type != "wheelchair"))
fords: (mode, type) => (mode == "bicycle" || (mode == "pedestrian" && type != "wheelchair")),
steps: (mode) => (mode == "bicycle" || mode == "pedestrian")
},
avoidText: {
highways: "highways",
tollways: "toll roads",
ferries: "ferries",
tunnels: "tunnels",
pavedroads: "paved roads",
unpavedroads: "unpaved roads",
tracks: "tracks",
fords: "fords",
steps: "steps",
hills: "hills"
steps: "steps"
}
}

Wyświetl plik

@ -12,11 +12,11 @@
</b-button>
<b-dropdown
id="fm-route-mode-customise"
:tabindex="tabindex + constants.modes.length"
title="Customise"
v-b-tooltip.hover.top
:disabled="disabled"
menu-class="fm-route-mode-customize"
>
<template #button-content><Icon icon="cog" alt="Custom"/></template>
<b-dropdown-item @click.native.capture.stop.prevent="decodedMode.details = !decodedMode.details"><Icon :icon="decodedMode.details ? 'check' : 'unchecked'"></Icon> Load route details (elevation, road types, )</b-dropdown-item>

Wyświetl plik

@ -37,7 +37,7 @@ export default class ShapeField extends Vue {
}
get valueSrc(): string {
return getMarkerUrl("000000", 25, undefined, this.value);
return getMarkerUrl("000000", 21, undefined, this.value);
}
get filteredShapes(): Shape[] {

Wyświetl plik

@ -1,7 +1,7 @@
<div :id="`${effId}-input-container`" class="fm-shape-field-container">
<b-input-group :id="`${effId}-input-group`">
<b-input-group-prepend>
<b-input-group-text><span style="width: 24px"><img :src="valueSrc"></span></b-input-group-text>
<b-input-group-text><span style="width: 1.4em"><img :src="valueSrc"></span></b-input-group-text>
</b-input-group-prepend>
<b-form-input autocomplete="off" v-bind="$props" v-on="$listeners" @keydown.esc="handleEscape"></b-form-input>
</b-input-group>

Wyświetl plik

@ -33,4 +33,8 @@ declare module "leaflet" {
namespace control {
export const graphicScale: any;
}
namespace Control {
const Heightgraph: any;
}
}

Wyświetl plik

@ -4,6 +4,7 @@ import { InjectReactive } from "vue-property-decorator";
export const CLIENT_INJECT_KEY = "fm-client";
export const MAP_COMPONENTS_INJECT_KEY = "fm-map-components";
export const MAP_CONTEXT_INJECT_KEY = "fm-map-context";
export const SEARCH_BOX_CONTEXT_INJECT_KEY = "fm-search-box-context";
export function InjectMapComponents(): VueDecorator {
return InjectReactive(MAP_COMPONENTS_INJECT_KEY);
@ -15,4 +16,8 @@ export function InjectMapContext(): VueDecorator {
export function InjectClient(): VueDecorator {
return InjectReactive(CLIENT_INJECT_KEY);
}
export function InjectSearchBoxContext(): VueDecorator {
return InjectReactive(SEARCH_BOX_CONTEXT_INJECT_KEY);
}

Wyświetl plik

@ -1,4 +1,4 @@
:local(.className) {
.fm-heightgraph {
&.heightgraph-container {
display: block;

Wyświetl plik

@ -1,14 +1,118 @@
import 'leaflet.heightgraph';
import $ from 'jquery';
import L from 'leaflet';
import { Control, Map, Polyline } from 'leaflet';
import "leaflet.heightgraph/src/L.Control.Heightgraph.css";
import "./heightgraph.scss";
import { TrackPoints } from 'facilmap-client';
import { ExtraInfo, TrackPoint } from 'facilmap-types';
import { FeatureCollection } from "geojson";
import { calculateDistance, round } from 'facilmap-utils';
import css from './heightgraph.scss';
import { calculateDistance } from '../../common/utils';
import { round } from '../../common/format';
function trackSegment(trackPoints: TrackPoints, fromIdx: number, toIdx: number): TrackPoint[] {
let ret: TrackPoint[] = [];
export default class FmHeightgraph extends L.Control.Heightgraph {
constructor(options) {
super(Object.assign({
for(let i=fromIdx; i<trackPoints.length; i++) {
if(trackPoints[i] && trackPoints[i].ele != null) {
ret.push(trackPoints[i]);
if(i >= toIdx) // Makes sure that if toIdx does not exist in trackPoints, the next trackPoint is added, which avoids gaps between the segments, as required by leaflet.heightgraph
break;
}
}
return ret;
}
type Collection = FeatureCollection & {
properties: {
summary: string;
distances: Record<number, number>;
};
}
function createGeoJsonForHeightGraph(extraInfo: ExtraInfo | undefined, trackPoints: TrackPoints): Collection[] {
const geojson: Collection[] = [];
if(!extraInfo || Object.keys(extraInfo).length == 0)
extraInfo = { "": [[ 0, trackPoints.length-1, "" as any ]] };
for(const i of Object.keys(extraInfo)) {
let featureCollection: Collection = {
type: "FeatureCollection",
features: [],
properties: {
summary: i,
distances: {}
}
};
const distances = featureCollection.properties.distances;
for(let segment in extraInfo[i]) {
const segmentPosList = trackSegment(trackPoints, extraInfo[i][segment][0], extraInfo[i][segment][1]);
if (distances[extraInfo[i][segment][2]] == null)
distances[extraInfo[i][segment][2]] = 0;
distances[extraInfo[i][segment][2]] += calculateDistance(segmentPosList);
featureCollection.features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: segmentPosList.map((trackPoint) => ([trackPoint.lon, trackPoint.lat, ...(trackPoint.ele != null ? [trackPoint.ele] : [])]))
},
properties: {
attributeType: extraInfo[i][segment][2]
}
});
}
geojson.push(featureCollection);
}
return geojson;
}
function getDistancesByInfoType(extraInfo: ExtraInfo[string] | undefined, trackPoints: TrackPoints): Record<number, number> {
const ret: Record<number, number> = { };
if (!extraInfo)
return ret;
for(let segment in extraInfo) {
if (ret[extraInfo[segment][2]] == null)
ret[extraInfo[segment][2]] = 0;
ret[extraInfo[segment][2]] += calculateDistance(trackSegment(trackPoints, extraInfo[segment][0], extraInfo[segment][1]));
}
return ret;
}
export function createElevationStats(extraInfo: ExtraInfo | undefined, trackPoints: TrackPoints): Record<number, number> | null {
if (!extraInfo || !extraInfo.steepness)
return null;
const stats = getDistancesByInfoType(extraInfo.steepness, trackPoints);
const sum = (filter: (i: number) => boolean): number => Object.keys(stats).map((i) => parseInt(i, 10)).filter(filter).reduce((acc, cur) => acc + stats[cur], 0);
return {
"-16": sum((i) => (i <= -5)),
"-10": sum((i) => (i <= -4)),
"-7": sum((i) => (i <= -3)),
"-4": sum((i) => (i <= -2)),
"-1": sum((i) => (i <= -1)),
"0": sum((i) => (i == 0)),
"1": sum((i) => (i >= 1)),
"4": sum((i) => (i >= 2)),
"7": sum((i) => (i >= 3)),
"10": sum((i) => (i >= 4)),
"16": sum((i) => (i >= 5))
};
}
export default class FmHeightgraph extends Control.Heightgraph {
constructor(options?: any) {
super({
margins: {
top: 20,
right: 10,
@ -136,36 +240,26 @@ export default class FmHeightgraph extends L.Control.Heightgraph {
"16": { text: "Private", color: "#F64A8A" },
"32": { text: "Permissive", color: "#E0115F" }
}
}
}, options));
},
...options
});
for (const i in this.options.mappings) {
for (const j in this.options.mappings[i]) {
for (const i of Object.keys(this.options.mappings)) {
for (const j of Object.keys(this.options.mappings[i])) {
this.options.mappings[i][j].originalText = this.options.mappings[i][j].text;
}
}
}
onAdd(map) {
// Work around double margins (https://github.com/GIScience/Leaflet.Heightgraph/issues/33)
let sizeBkp = { width: this.options.width, height: this.options.height };
this.options.width = sizeBkp.width + this.options.margins.left + this.options.margins.right;
this.options.height = sizeBkp.height + this.options.margins.top + this.options.margins.bottom;
onAdd(map: Map): Element {
// Initialize renderer on overlay pane because Heightgraph renders the hover overlay there (it appends it to .leaflet-overlay-pane svg)
map.getRenderer(new Polyline([]));
let el = $("svg", super.onAdd(map));
Object.assign(this.options, sizeBkp);
if(this._data)
super.addData(this._data);
el.addClass(css.className);
return el[0];
return super.onAdd(map);
}
addData(extraInfo, trackPoints) {
let data = FmHeightgraph.createGeoJsonForHeightGraph(extraInfo, trackPoints);
addData(extraInfo: ExtraInfo | undefined, trackPoints: TrackPoints): void {
let data = createGeoJsonForHeightGraph(extraInfo, trackPoints);
for (const featureCollection of data) {
for (const i in featureCollection.properties.distances) {
@ -181,106 +275,4 @@ export default class FmHeightgraph extends L.Control.Heightgraph {
this._data = data;
}
_appendScales() {
super._appendScales();
//this._xAxis.ticks(3);
}
static trackSegment(trackPoints, fromIdx, toIdx) {
let ret = [];
for(let i=fromIdx; i<trackPoints.length; i++) {
if(trackPoints[i] && trackPoints[i].ele != null) {
ret.push(trackPoints[i]);
if(i >= toIdx) // Makes sure that if toIdx does not exist in trackPoints, the next trackPoint is added, which avoids gaps between the segments, as required by leaflet.heightgraph
break;
}
}
return ret;
}
static createGeoJsonForHeightGraph(extraInfo, trackPoints) {
let geojson = [];
if(!extraInfo || Object.keys(extraInfo).length == 0)
extraInfo = { "": [[ 0, trackPoints.length-1, "" ]] };
for(let i in extraInfo) {
let featureCollection = {
type: "FeatureCollection",
features: [],
properties: {
summary: i
}
};
const distances = { };
for(let segment in extraInfo[i]) {
const segmentPosList = FmHeightgraph.trackSegment(trackPoints, extraInfo[i][segment][0], extraInfo[i][segment][1]);
if (distances[extraInfo[i][segment][2]] == null)
distances[extraInfo[i][segment][2]] = 0;
distances[extraInfo[i][segment][2]] += calculateDistance(segmentPosList);
featureCollection.features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: segmentPosList.map((trackPoint) => ([trackPoint.lon, trackPoint.lat, trackPoint.ele]))
},
properties: {
attributeType: extraInfo[i][segment][2]
}
});
}
featureCollection.properties.distances = distances;
geojson.push(featureCollection);
}
return geojson;
}
static getDistancesByInfoType(extraInfo, trackPoints) {
const ret = { };
if (!extraInfo)
return ret;
for(let segment in extraInfo) {
if (ret[extraInfo[segment][2]] == null)
ret[extraInfo[segment][2]] = 0;
ret[extraInfo[segment][2]] += calculateDistance(FmHeightgraph.trackSegment(trackPoints, extraInfo[segment][0], extraInfo[segment][1]));
}
return ret;
}
static createElevationStats(extraInfo, trackPoints) {
if (!extraInfo || !extraInfo.steepness)
return null;
const stats = FmHeightgraph.getDistancesByInfoType(extraInfo.steepness, trackPoints);
const sum = (filter) => Object.keys(stats).map((i) => parseInt(i, 10)).filter(filter).reduce((acc, cur) => acc + stats[cur], 0);
return {
"-16": sum((i) => (i <= -5)),
"-10": sum((i) => (i <= -4)),
"-7": sum((i) => (i <= -3)),
"-4": sum((i) => (i <= -2)),
"-1": sum((i) => (i <= -1)),
"0": sum((i) => (i == 0)),
"1": sum((i) => (i >= 1)),
"4": sum((i) => (i >= 2)),
"7": sum((i) => (i >= 3)),
"10": sum((i) => (i >= 4)),
"16": sum((i) => (i >= 5))
};
}
}

Wyświetl plik

@ -15,7 +15,7 @@ for (const key of rawIconsContext.keys() as string[]) {
}
rawIcons["fontawesome"] = {};
for (const name of ["arrow-left", "arrow-right", "biking", "car-alt", "slash", "walking"]) {
for (const name of ["arrow-left", "arrow-right", "biking", "car-alt", "chart-line", "slash", "walking"]) {
rawIcons["fontawesome"][name] = require(`@fortawesome/fontawesome-free/svgs/solid/${name}.svg`);
}

Wyświetl plik

@ -9,11 +9,12 @@ const ROUTING_URL = `https://api.openrouteservice.org/v2/directions`;
const ROUTING_MODES: Record<string, string> = {
"car-": "driving-car",
"car-hgv": "driving-hgv", // TODO
"bicycle-": "cycling-regular",
"bicycle-road": "cycling-road",
"bicycle-safe": "cycling-safe",
// "bicycle-safe": "cycling-safe",
"bicycle-mountain": "cycling-mountain",
"bicycle-tour": "cycling-tour",
// "bicycle-tour": "cycling-tour",
"bicycle-electric": "cycling-electric",
"pedestrian-": "foot-walking",
"pedestrian-hiking": "foot-hiking",
@ -55,7 +56,7 @@ async function calculateRouteInternal(points: Point[], decodedMode: DecodedRoute
results = await Promise.all(coordGroups.map((coords) => {
const req: any = {
coordinates: coords.map((point) => [point.lon, point.lat]),
// + "&geometry_format=polyline"
radiuses: coords.map(() => -1),
instructions: false
};

Wyświetl plik

@ -1,7 +1,7 @@
import { Bbox, Colour, ID, Point, RouteMode, ZoomLevel } from "./base";
import { PadId } from "./padData";
export type ExtraInfo = Record<string, string[]>;
export type ExtraInfo = Record<string, Array<[number, number, number]>>;
interface LineBase {
id: ID;

Wyświetl plik

@ -99,6 +99,8 @@ export function renderOsmTag(key: string, value: string): string {
return m[1] + '<a href="https://wiki.openstreetmap.org/wiki/Image:' + quoteHtml(m[2]) + '" target="_blank">' + quoteHtml(m[2]) + '</a>' + m[3];
}).join(";");
} else {
return linkifyStr(value);
return linkifyStr(value, {
target: (href, type) => type === "url" ? "_blank" : ""
});
}
}

Wyświetl plik

@ -2,10 +2,10 @@ import { RouteMode } from "facilmap-types";
export interface DecodedRouteMode {
mode: "" | "car" | "bicycle" | "pedestrian" | "track";
type: "" | "road" | "safe" | "mountain" | "tour" | "electric" | "hiking" | "wheelchair";
type: "" | "hgv" | "road" | "mountain" | "electric" | "hiking" | "wheelchair";
preference: "fastest" | "shortest" | "recommended";
details: boolean;
avoid: Array<"highways" | "tollways" | "ferries" | "tunnels" | "pavedroads" | "unpavedroads" | "tracks" | "fords" | "steps" | "hills">;
avoid: Array<"highways" | "tollways" | "ferries" | "fords" | "steps">;
}
export const R = 6371; // km
@ -67,7 +67,7 @@ export function decodeRouteMode(encodedMode: RouteMode): DecodedRouteMode {
decodedMode.mode = "pedestrian";
else if(["helicopter", "straight"].includes(part))
decodedMode.mode = "";
else if(["road", "safe", "mountain", "tour", "electric", "hiking", "wheelchair"].includes(part))
else if(["hgv", "road", "mountain", "electric", "hiking", "wheelchair"].includes(part))
decodedMode.type = part as any;
else if(["fastest", "shortest", "recommended"].includes(part))
decodedMode.preference = part as any;
@ -86,15 +86,18 @@ export function formatRouteMode(encodedMode: RouteMode): string {
switch(decodedMode.mode) {
case "car":
return "by car";
switch(decodedMode.type) {
case "hgv":
return "by HGV";
default:
return "by car";
}
case "bicycle":
switch(decodedMode.type) {
case "road":
return "by road bike";
case "mountain":
return "by mountain bike";
case "tour":
return "by touring bike";
case "electric":
return "by electric bike";
default:

Wyświetl plik

@ -791,6 +791,11 @@
"@types/qs" "*"
"@types/serve-static" "*"
"@types/file-saver@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.1.tgz#e18eb8b069e442f7b956d313f4fadd3ef887354e"
integrity sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw==
"@types/geojson@*", "@types/geojson@^7946.0.7":
version "7946.0.7"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"