From 1354c37ddfee0dbf5979631f0c7e29fe31fd3a18 Mon Sep 17 00:00:00 2001 From: Candid Dauth Date: Fri, 19 Mar 2021 22:16:57 +0100 Subject: [PATCH] Status commit --- frontend/app/map/lines/lines.js | 25 +- frontend/app/map/route/route.js | 34 +-- frontend/package.json | 2 +- frontend/src/map/line-info/line-info.ts | 10 +- frontend/src/map/line-info/line-info.vue | 21 +- frontend/src/map/map.scss | 2 +- frontend/src/map/map.ts | 3 + .../src/map/route-form/route-form-tab.scss | 1 + frontend/src/map/route-form/route-form.scss | 7 + frontend/src/map/route-form/route-form.ts | 49 +++- frontend/src/map/route-form/route-form.vue | 51 ++-- frontend/src/map/search-box/search-box.scss | 4 + frontend/src/map/search-box/search-box.ts | 11 +- .../src/map/ui/colour-field/colour-field.vue | 2 +- .../map/ui/elevation-plot/elevation-plot.scss | 9 + .../map/ui/elevation-plot/elevation-plot.ts | 61 +++++ .../map/ui/elevation-plot/elevation-plot.vue | 1 + .../map/ui/elevation-stats/elevation-stats.ts | 14 +- .../ui/elevation-stats/elevation-stats.vue | 12 +- frontend/src/map/ui/icon/icon.ts | 4 +- .../src/map/ui/route-mode/route-mode.scss | 18 +- frontend/src/map/ui/route-mode/route-mode.ts | 31 +-- frontend/src/map/ui/route-mode/route-mode.vue | 2 +- .../src/map/ui/shape-field/shape-field.ts | 2 +- .../src/map/ui/shape-field/shape-field.vue | 2 +- frontend/src/type-fixup.d.ts | 4 + frontend/src/utils/decorators.ts | 5 + .../leaflet => src/utils}/heightgraph.scss | 2 +- .../utils/heightgraph.ts} | 254 +++++++++--------- leaflet/src/utils/icons.ts | 2 +- server/src/routing/ors.ts | 7 +- types/src/line.ts | 2 +- utils/src/format.ts | 4 +- utils/src/routing.ts | 15 +- yarn.lock | 5 + 35 files changed, 392 insertions(+), 286 deletions(-) create mode 100644 frontend/src/map/ui/elevation-plot/elevation-plot.scss create mode 100644 frontend/src/map/ui/elevation-plot/elevation-plot.ts create mode 100644 frontend/src/map/ui/elevation-plot/elevation-plot.vue rename frontend/{app/leaflet => src/utils}/heightgraph.scss (69%) rename frontend/{app/leaflet/heightgraph.js => src/utils/heightgraph.ts} (59%) diff --git a/frontend/app/map/lines/lines.js b/frontend/app/map/lines/lines.js index 7c295980..c34528e0 100644 --- a/frontend/app/map/lines/lines.js +++ b/frontend/app/map/lines/lines.js @@ -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); }, diff --git a/frontend/app/map/route/route.js b/frontend/app/map/route/route.js index 75dd200f..66b19eea 100644 --- a/frontend/app/map/route/route.js +++ b/frontend/app/map/route/route.js @@ -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) { diff --git a/frontend/package.json b/frontend/package.json index 1820d84e..41aee98e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/map/line-info/line-info.ts b/frontend/src/map/line-info/line-info.ts index 4746117c..63de6554 100644 --- a/frontend/src/map/line-info/line-info.ts +++ b/frontend/src/map/line-info/line-info.ts @@ -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 { this.$bvToast.hide("fm-line-info-delete"); diff --git a/frontend/src/map/line-info/line-info.vue b/frontend/src/map/line-info/line-info.vue index 6b65dd18..1df6c2ef 100644 --- a/frontend/src/map/line-info/line-info.vue +++ b/frontend/src/map/line-info/line-info.vue @@ -1,23 +1,34 @@
-

{{line.name}}

+
+

{{line.name}}

+ +
+
Distance
{{line.distance | round(2)}} km ({{line.time | fmFormatTime}} h {{line.mode | fmRouteMode}})
-
\ No newline at end of file diff --git a/frontend/src/map/search-box/search-box.scss b/frontend/src/map/search-box/search-box.scss index 2ca6f0ec..e3388496 100644 --- a/frontend/src/map/search-box/search-box.scss +++ b/frontend/src/map/search-box/search-box.scss @@ -35,6 +35,10 @@ min-height: 0; } + .tabs, .tab-content, .tab-pane { + flex-grow: 1; + } + .card-header { display: flex; flex-direction: column; diff --git a/frontend/src/map/search-box/search-box.ts b/frontend/src/map/search-box/search-box.ts index 66b25d08..44a32239 100644 --- a/frontend/src/map/search-box/search-box.ts +++ b/frontend/src/map/search-box/search-box.ts @@ -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 { diff --git a/frontend/src/map/ui/colour-field/colour-field.vue b/frontend/src/map/ui/colour-field/colour-field.vue index 6bfa47bb..9497f752 100644 --- a/frontend/src/map/ui/colour-field/colour-field.vue +++ b/frontend/src/map/ui/colour-field/colour-field.vue @@ -2,7 +2,7 @@ - + diff --git a/frontend/src/map/ui/elevation-plot/elevation-plot.scss b/frontend/src/map/ui/elevation-plot/elevation-plot.scss new file mode 100644 index 00000000..78bfd46c --- /dev/null +++ b/frontend/src/map/ui/elevation-plot/elevation-plot.scss @@ -0,0 +1,9 @@ +.fm-elevation-plot { + flex-grow: 1; + flex-basis: 12rem; + overflow: hidden; + + .heightgraph-toggle, .heightgraph-close-icon { + display: none !important; + } +} \ No newline at end of file diff --git a/frontend/src/map/ui/elevation-plot/elevation-plot.ts b/frontend/src/map/ui/elevation-plot/elevation-plot.ts new file mode 100644 index 00000000..a21c37bf --- /dev/null +++ b/frontend/src/map/ui/elevation-plot/elevation-plot.ts @@ -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 }); + } + +} \ No newline at end of file diff --git a/frontend/src/map/ui/elevation-plot/elevation-plot.vue b/frontend/src/map/ui/elevation-plot/elevation-plot.vue new file mode 100644 index 00000000..dd47929e --- /dev/null +++ b/frontend/src/map/ui/elevation-plot/elevation-plot.vue @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/frontend/src/map/ui/elevation-stats/elevation-stats.ts b/frontend/src/map/ui/elevation-stats/elevation-stats.ts index 8de696dd..776ece7e 100644 --- a/frontend/src/map/ui/elevation-stats/elevation-stats.ts +++ b/frontend/src/map/ui/elevation-stats/elevation-stats.ts @@ -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'); } } \ No newline at end of file diff --git a/frontend/src/map/ui/elevation-stats/elevation-stats.vue b/frontend/src/map/ui/elevation-stats/elevation-stats.vue index d9439003..ec5dfc53 100644 --- a/frontend/src/map/ui/elevation-stats/elevation-stats.vue +++ b/frontend/src/map/ui/elevation-stats/elevation-stats.vue @@ -2,15 +2,15 @@  {{route.ascent}} m /  {{route.descent}} m
-
Total ascent
-
{{route.ascent}} m
+
Total ascent
+
{{route.ascent}} m
-
Total descent
-
{{route.descent}} m
+
Total descent
+
{{route.descent}} m
diff --git a/frontend/src/map/ui/icon/icon.ts b/frontend/src/map/ui/icon/icon.ts index 91886f3f..6922d5c9 100644 --- a/frontend/src/map/ui/icon/icon.ts +++ b/frontend/src/map/ui/icon/icon.ts @@ -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); } } \ No newline at end of file diff --git a/frontend/src/map/ui/route-mode/route-mode.scss b/frontend/src/map/ui/route-mode/route-mode.scss index 1414dec3..d54938a1 100644 --- a/frontend/src/map/ui/route-mode/route-mode.scss +++ b/frontend/src/map/ui/route-mode/route-mode.scss @@ -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%; } } } \ No newline at end of file diff --git a/frontend/src/map/ui/route-mode/route-mode.ts b/frontend/src/map/ui/route-mode/route-mode.ts index 880a5ede..2d04fb19 100644 --- a/frontend/src/map/ui/route-mode/route-mode.ts +++ b/frontend/src/map/ui/route-mode/route-mode.ts @@ -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" } } diff --git a/frontend/src/map/ui/route-mode/route-mode.vue b/frontend/src/map/ui/route-mode/route-mode.vue index b17c9529..c1c989ba 100644 --- a/frontend/src/map/ui/route-mode/route-mode.vue +++ b/frontend/src/map/ui/route-mode/route-mode.vue @@ -12,11 +12,11 @@ Load route details (elevation, road types, …) diff --git a/frontend/src/map/ui/shape-field/shape-field.ts b/frontend/src/map/ui/shape-field/shape-field.ts index 636ce1bb..d620ceca 100644 --- a/frontend/src/map/ui/shape-field/shape-field.ts +++ b/frontend/src/map/ui/shape-field/shape-field.ts @@ -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[] { diff --git a/frontend/src/map/ui/shape-field/shape-field.vue b/frontend/src/map/ui/shape-field/shape-field.vue index 8cbee019..c7cac7ac 100644 --- a/frontend/src/map/ui/shape-field/shape-field.vue +++ b/frontend/src/map/ui/shape-field/shape-field.vue @@ -1,7 +1,7 @@
- + diff --git a/frontend/src/type-fixup.d.ts b/frontend/src/type-fixup.d.ts index e36095fc..bc395b33 100644 --- a/frontend/src/type-fixup.d.ts +++ b/frontend/src/type-fixup.d.ts @@ -33,4 +33,8 @@ declare module "leaflet" { namespace control { export const graphicScale: any; } + + namespace Control { + const Heightgraph: any; + } } \ No newline at end of file diff --git a/frontend/src/utils/decorators.ts b/frontend/src/utils/decorators.ts index 96f9715a..10c8cf46 100644 --- a/frontend/src/utils/decorators.ts +++ b/frontend/src/utils/decorators.ts @@ -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); } \ No newline at end of file diff --git a/frontend/app/leaflet/heightgraph.scss b/frontend/src/utils/heightgraph.scss similarity index 69% rename from frontend/app/leaflet/heightgraph.scss rename to frontend/src/utils/heightgraph.scss index b5782255..1fe9b3aa 100644 --- a/frontend/app/leaflet/heightgraph.scss +++ b/frontend/src/utils/heightgraph.scss @@ -1,4 +1,4 @@ -:local(.className) { +.fm-heightgraph { &.heightgraph-container { display: block; diff --git a/frontend/app/leaflet/heightgraph.js b/frontend/src/utils/heightgraph.ts similarity index 59% rename from frontend/app/leaflet/heightgraph.js rename to frontend/src/utils/heightgraph.ts index 5ab8e7b0..5b52787b 100644 --- a/frontend/app/leaflet/heightgraph.js +++ b/frontend/src/utils/heightgraph.ts @@ -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= 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; + }; +} + +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 { + const ret: Record = { }; + + 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 | 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= 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)) - }; - } } \ No newline at end of file diff --git a/leaflet/src/utils/icons.ts b/leaflet/src/utils/icons.ts index 3a402d09..292721c8 100644 --- a/leaflet/src/utils/icons.ts +++ b/leaflet/src/utils/icons.ts @@ -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`); } diff --git a/server/src/routing/ors.ts b/server/src/routing/ors.ts index a29741e9..180a0533 100644 --- a/server/src/routing/ors.ts +++ b/server/src/routing/ors.ts @@ -9,11 +9,12 @@ const ROUTING_URL = `https://api.openrouteservice.org/v2/directions`; const ROUTING_MODES: Record = { "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 }; diff --git a/types/src/line.ts b/types/src/line.ts index f40adb94..49511f32 100644 --- a/types/src/line.ts +++ b/types/src/line.ts @@ -1,7 +1,7 @@ import { Bbox, Colour, ID, Point, RouteMode, ZoomLevel } from "./base"; import { PadId } from "./padData"; -export type ExtraInfo = Record; +export type ExtraInfo = Record>; interface LineBase { id: ID; diff --git a/utils/src/format.ts b/utils/src/format.ts index 88ecce99..ac734a88 100644 --- a/utils/src/format.ts +++ b/utils/src/format.ts @@ -99,6 +99,8 @@ export function renderOsmTag(key: string, value: string): string { return m[1] + '' + quoteHtml(m[2]) + '' + m[3]; }).join(";"); } else { - return linkifyStr(value); + return linkifyStr(value, { + target: (href, type) => type === "url" ? "_blank" : "" + }); } } \ No newline at end of file diff --git a/utils/src/routing.ts b/utils/src/routing.ts index 26b9fb37..9bddd8c6 100644 --- a/utils/src/routing.ts +++ b/utils/src/routing.ts @@ -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: diff --git a/yarn.lock b/yarn.lock index b664ae87..4d878fc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"