kopia lustrzana https://github.com/FacilMap/facilmap
Status commit
rodzic
0fe03d560a
commit
1354c37ddf
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
}
|
||||
|
||||
.btn-toolbar {
|
||||
* + * {
|
||||
> * + * {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
|
@ -23,6 +23,9 @@ Vue.use(BootstrapVue, {
|
|||
modifiers: {
|
||||
preventOverflow: {
|
||||
enabled: false
|
||||
},
|
||||
hide: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
.input-group {
|
||||
position: static;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -35,6 +35,10 @@
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.tabs, .tab-content, .tab-pane {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
.fm-elevation-plot {
|
||||
flex-grow: 1;
|
||||
flex-basis: 12rem;
|
||||
overflow: hidden;
|
||||
|
||||
.heightgraph-toggle, .heightgraph-close-icon {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<div class="fm-elevation-plot" ref="container"></div>
|
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -33,4 +33,8 @@ declare module "leaflet" {
|
|||
namespace control {
|
||||
export const graphicScale: any;
|
||||
}
|
||||
|
||||
namespace Control {
|
||||
const Heightgraph: any;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
:local(.className) {
|
||||
.fm-heightgraph {
|
||||
|
||||
&.heightgraph-container {
|
||||
display: block;
|
|
@ -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))
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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`);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" : ""
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
Ładowanie…
Reference in New Issue