kopia lustrzana https://github.com/FacilMap/facilmap
Status commit
rodzic
3dbe7500e8
commit
952f07fcca
|
@ -1,53 +0,0 @@
|
|||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="$dismiss()"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="modal-title">Custom Import</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" ng-submit="save()">
|
||||
<div uib-alert class="alert-danger" ng-show="error">{{error.message || error}}</div>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Map to…</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="(importTypeId, objectCount) in importTypeCounts">
|
||||
<td><label for="map-type-{{importTypeId}}">{{results.types[importTypeId].type == 'marker' ? 'Markers' : 'Lines'}} of type “{{results.types[importTypeId].name}}” ({{objectCount}})</label></td>
|
||||
<td><select id="map-type-{{importTypeId}}" ng-model="mapping[importTypeId]" class="form-control">
|
||||
<option ng-if="client.writable == 2 && importTypeId" ng-value="'i' + importTypeId">Import type “{{results.types[importTypeId].name}}”</option>
|
||||
<option ng-repeat="type in client.types" ng-if="type.name == results.types[importTypeId].name && type.type == results.types[importTypeId].type" ng-value="'e' + type.id">Existing type “{{type.name}}”</option>
|
||||
<option ng-value="false">Do not import</option>
|
||||
<option disabled>──────────</option>
|
||||
<option ng-repeat="(typeId, type) in results.types" ng-if="client.writable == 2 && type.type == results.types[importTypeId].type && typeId != importTypeId" ng-value="'i' + typeId">Import type “{{type.name}}”</option>
|
||||
<option ng-repeat="type in client.types" ng-if="type.type == results.types[importTypeId].type && type.name != results.types[importTypeId].name" ng-value="'e' + type.id">Existing type “{{type.name}}”</option>
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr ng-if="untypedMarkers > 0">
|
||||
<td><label for="map-untyped-markers">Untyped markers ({{untypedMarkers}})</label></td>
|
||||
<td><select id="map-untyped-markers" ng-model="mapUntypedMarkers" class="form-control">
|
||||
<option ng-value="false">Do not import</option>
|
||||
<option ng-repeat="(typeId, type) in results.types" ng-if="client.writable == 2 && type.type == 'marker'" ng-value="'i' + typeId">Import type “{{type.name}}”</option>
|
||||
<option ng-repeat="type in client.types" ng-if="type.type == 'marker'" ng-value="'e' + type.id">Existing type “{{type.name}}”</option>
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr ng-if="untypedLines > 0">
|
||||
<td><label for="map-untyped-lines">Untyped lines/polygons ({{untypedLines}})</label></td>
|
||||
<td><select id="map-untyped-lines" ng-model="mapUntypedLines" class="form-control">
|
||||
<option ng-value="false">Do not import</option>
|
||||
<option ng-repeat="(typeId, type) in results.types" ng-if="client.writable == 2 && type.type == 'line'" ng-value="'i' + typeId">Import type “{{type.name}}”</option>
|
||||
<option ng-repeat="type in client.types" ng-if="type.type == 'marker'" ng-value="'e' + type.id">Existing type “{{type.name}}”</option>
|
||||
</select></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<button type="submit" class="hidden"></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" ng-click="$dismiss()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" ng-click="save()">Import</button>
|
||||
</div>
|
|
@ -1,125 +0,0 @@
|
|||
import fm from '../app';
|
||||
|
||||
fm.app.factory("fmSearchImport", function($uibModal, $rootScope) {
|
||||
return function(map) {
|
||||
let importUi = {
|
||||
openImportDialog(results) {
|
||||
let scope = $rootScope.$new();
|
||||
|
||||
let dialog = $uibModal.open({
|
||||
template: require("./custom-import.html"),
|
||||
scope: scope,
|
||||
controller: "fmSearchCustomImportController",
|
||||
size: "lg",
|
||||
resolve: {
|
||||
map: function() { return map; },
|
||||
results: function() { return results; },
|
||||
importUi: function() { return importUi; }
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return importUi;
|
||||
}
|
||||
});
|
||||
|
||||
fm.app.controller("fmSearchCustomImportController", function(map, results, $scope, $q, importUi) {
|
||||
$scope.results = results;
|
||||
$scope.client = map.client;
|
||||
|
||||
$scope.importTypeCounts = {};
|
||||
$scope.untypedMarkers = 0;
|
||||
$scope.untypedLines = 0;
|
||||
for(let feature of results.features) {
|
||||
if(feature.fmTypeId == null) {
|
||||
if(feature.isMarker)
|
||||
$scope.untypedMarkers++;
|
||||
if(feature.isLine)
|
||||
$scope.untypedLines++;
|
||||
} else if($scope.importTypeCounts[feature.fmTypeId] == null)
|
||||
$scope.importTypeCounts[feature.fmTypeId] = 1;
|
||||
else
|
||||
$scope.importTypeCounts[feature.fmTypeId]++;
|
||||
}
|
||||
|
||||
$scope.mapping = {};
|
||||
for(let importTypeId in results.types) {
|
||||
for(let typeId in map.client.types) {
|
||||
if(results.types[importTypeId].name == map.client.types[typeId].name) {
|
||||
$scope.mapping[importTypeId] = `e${typeId}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!$scope.mapping[importTypeId])
|
||||
$scope.mapping[importTypeId] = `i${importTypeId}`;
|
||||
}
|
||||
|
||||
$scope.mapUntypedMarkers = false;
|
||||
$scope.mapUntypedLines = false;
|
||||
|
||||
|
||||
$scope.save = function() {
|
||||
$scope.error = null;
|
||||
|
||||
let resolvedMapping = {};
|
||||
let resolvedUntypedMarkerMapping;
|
||||
let resolvedUntypedLineMapping;
|
||||
let createTypes = {};
|
||||
for(let mapping in $scope.mapping) {
|
||||
if(!$scope.mapping[mapping])
|
||||
continue;
|
||||
|
||||
let m = $scope.mapping[mapping].match(/^([ei])(.*)$/);
|
||||
if(m[1] == "e")
|
||||
resolvedMapping[mapping] = m[2];
|
||||
else if(!createTypes[m[2]]) {
|
||||
createTypes[m[2]] = map.client.addType(results.types[m[2]]).then((newType) => {
|
||||
resolvedMapping[mapping] = newType.id;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if($scope.mapUntypedMarkers) {
|
||||
let m = $scope.mapUntypedMarkers.match(/^([ei])(.*)$/);
|
||||
if(m[1] == "e")
|
||||
resolvedUntypedMarkerMapping = m[2];
|
||||
else if(!createTypes[m[2]]) {
|
||||
createTypes[m[2]] = map.client.addType(results.types[m[2]]).then((newType) => {
|
||||
resolvedUntypedMarkerMapping = newType.id;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if($scope.mapUntypedLines) {
|
||||
let m = $scope.mapUntypedLines.match(/^([ei])(.*)$/);
|
||||
if(m[1] == "e")
|
||||
resolvedUntypedLineMapping = m[2];
|
||||
else if(!createTypes[m[2]]) {
|
||||
createTypes[m[2]] = map.client.addType(results.types[m[2]]).then((newType) => {
|
||||
resolvedUntypedLineMapping = newType.id;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$q.all(createTypes).then(() => {
|
||||
let createObjects = [];
|
||||
for(let feature of results.features) {
|
||||
if(feature.fmTypeId == null) {
|
||||
if(feature.isMarker && resolvedUntypedMarkerMapping)
|
||||
createObjects.push(importUi.addResultToMap(feature, map.client.types[resolvedUntypedMarkerMapping]));
|
||||
if(feature.isLine && resolvedUntypedLineMapping)
|
||||
createObjects.push(importUi.addResultToMap(feature, map.client.types[resolvedUntypedLineMapping]));
|
||||
} else if(resolvedMapping[feature.fmTypeId])
|
||||
createObjects.push(importUi.addResultToMap(feature, map.client.types[resolvedMapping[feature.fmTypeId]]));
|
||||
}
|
||||
|
||||
return $q.all(createObjects);
|
||||
}).then(() => {
|
||||
$scope.$close();
|
||||
}).catch((err) => {
|
||||
$scope.error = err;
|
||||
});
|
||||
};
|
||||
});
|
|
@ -6,7 +6,8 @@
|
|||
FacilMap aims to be a privacy-friendly open-source alternative to commercial maps that track your data. When you use FacilMap,
|
||||
anything you do on the map (for example pan/zoom the map, search for something, calculate a route, add a marker) is sent to the
|
||||
FacilMap server and persisted there only if necessary for the interaction. No personally identifiable information is persisted
|
||||
(for example your IP address). FacilMap does not set any cookies.
|
||||
(for example your IP address). FacilMap stores the zoom preferences that you set in the search bar (auto-zoom, etc.) in the
|
||||
localStorage of your browser, but it does not set any cookies.
|
||||
</p>
|
||||
<p>
|
||||
FacilMap combines multiple third-party services (listed below under “Map data”) into one versatile map. When you use FacilMap,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import WithRender from "./click-marker.vue";
|
||||
import Vue from "vue";
|
||||
import { Component } from "vue-property-decorator";
|
||||
import { Component, Watch } from "vue-property-decorator";
|
||||
import { InjectClient, InjectMapComponents, InjectMapContext } from "../../utils/decorators";
|
||||
import { MapComponents, MapContext } from "../leaflet-map/leaflet-map";
|
||||
import { LineCreate, MarkerCreate, Point, SearchResult, Type } from "facilmap-types";
|
||||
|
@ -23,6 +23,8 @@ export default class ClickMarker extends Vue {
|
|||
@InjectMapComponents() mapComponents!: MapComponents;
|
||||
@InjectClient() client!: Client;
|
||||
|
||||
lastClick = 0;
|
||||
|
||||
results: SearchResult[] = [];
|
||||
layers!: SearchResultsLayer[]; // Don't make layer objects reactive
|
||||
|
||||
|
@ -43,6 +45,14 @@ export default class ClickMarker extends Vue {
|
|||
});
|
||||
}
|
||||
|
||||
@Watch("mapContext.selection")
|
||||
handleSelectionChange(): void {
|
||||
for (let i = this.results.length - 1; i >= 0; i--) {
|
||||
if (!this.mapContext.selection.some((item) => item.type == "searchResult" && item.layerId == this.layerIds[i]))
|
||||
this.close(this.results[i]);
|
||||
}
|
||||
}
|
||||
|
||||
handleOpenSelection(): void {
|
||||
for (let i = 0; i < this.layerIds.length; i++) {
|
||||
if (this.mapContext.selection.some((item) => item.type == "searchResult" && item.layerId == this.layerIds[i])) {
|
||||
|
@ -53,11 +63,32 @@ export default class ClickMarker extends Vue {
|
|||
}
|
||||
|
||||
async handleMapClick(pos: Point): Promise<void> {
|
||||
const results = await this.client.find({
|
||||
query: `geo:${round(pos.lat, 5)},${round(pos.lon, 5)}?z=${this.mapContext.zoom}`,
|
||||
loadUrls: false,
|
||||
elevation: true
|
||||
});
|
||||
const now = Date.now();
|
||||
if (now - this.lastClick < 500) {
|
||||
// Hacky solution to avoid markers being created when the user double-clicks the map. If multiple clicks happen less than 500 ms from each
|
||||
// other, all those clicks are ignored.
|
||||
this.lastClick = now;
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastClick = now;
|
||||
|
||||
const [results] = await Promise.all([
|
||||
this.client.find({
|
||||
query: `geo:${round(pos.lat, 5)},${round(pos.lon, 5)}?z=${this.mapContext.zoom}`,
|
||||
loadUrls: false,
|
||||
elevation: true
|
||||
}),
|
||||
new Promise((resolve) => {
|
||||
// Specify the minimum time the search will take to allow for some time for the double-click detection
|
||||
setTimeout(resolve, 500);
|
||||
})
|
||||
]);
|
||||
|
||||
if (now !== this.lastClick) {
|
||||
// There has been another click since the one we are reacting to.
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
const layer = new SearchResultsLayer([results[0]]).addTo(this.mapComponents.map);
|
||||
|
@ -85,6 +116,11 @@ export default class ClickMarker extends Vue {
|
|||
this.layers.splice(idx, 1);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
for (let i = this.results.length - 1; i >= 0; i--)
|
||||
this.close(this.results[i]);
|
||||
}
|
||||
|
||||
async addToMap(result: SearchResult, type: Type): Promise<void> {
|
||||
this.$bvToast.hide("fm-click-marker-add-error");
|
||||
|
||||
|
|
|
@ -137,6 +137,6 @@ export default class EditType extends Vue {
|
|||
}
|
||||
|
||||
canControl(what: keyof Marker | keyof Line): boolean {
|
||||
return canControl(this.type, what);
|
||||
return canControl(this.type, what, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.fm-file-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
|
@ -11,6 +11,7 @@ import Icon from "../ui/icon/icon";
|
|||
import SearchResults from "../search-results/search-results";
|
||||
import { displayView } from "facilmap-leaflet";
|
||||
import { MapComponents } from "../leaflet-map/leaflet-map";
|
||||
import "./file-results.scss";
|
||||
|
||||
type ViewImport = FileResultObject["views"][0];
|
||||
type TypeImport = FileResultObject["types"][0];
|
||||
|
@ -30,6 +31,11 @@ export default class FileResults extends Vue {
|
|||
@Prop({ type: Number, required: true }) layerId!: number;
|
||||
@Prop({ type: Object, required: true }) file!: FileResultObject;
|
||||
|
||||
/** When clicking a search result, union zoom to it. Normal zoom is done when clicking the zoom button. */
|
||||
@Prop({ type: Boolean, default: false }) unionZoom!: boolean;
|
||||
/** When clicking or selecting a search result, zoom to it. */
|
||||
@Prop({ type: Boolean, default: false }) autoZoom!: boolean;
|
||||
|
||||
get hasViews(): boolean {
|
||||
return this.file.views.length > 0;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<SearchResults
|
||||
:search-results="file.features"
|
||||
:layer-id="layerId"
|
||||
@click-result="$emit('click-result', $event)"
|
||||
:auto-zoom="autoZoom"
|
||||
:union-zoom="unionZoom"
|
||||
:custom-types="file.types"
|
||||
>
|
||||
<template #before>
|
||||
<template v-if="hasViews">
|
||||
|
@ -37,20 +39,5 @@
|
|||
</template>
|
||||
</template>
|
||||
|
||||
<!-- <div class="fm-search-buttons" ng-show="file.features.length > 0">
|
||||
<button type="button" class="btn btn-default" ng-model="showAll" ng-click="showAll && zoomToAll()" uib-btn-checkbox ng-show="file.features.length > 1">Show all</button>
|
||||
|
||||
<button type="button" class="btn btn-link" ng-click="reset()"><fm-icon fm-icon="remove" alt="Remove"></fm-icon></button>
|
||||
|
||||
<div uib-dropdown keyboard-nav="true" class="pull-right dropup" ng-if="client.padId && !client.readonly">
|
||||
<button id="search-add-all-button" type="button" class="btn btn-default" uib-dropdown-toggle>Add all to map <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="search-add-all-button">
|
||||
<li ng-if="(file.types | fmPropertyCount) > 0" role="menuitem"><a href="javascript:" ng-click="customImport()">Custom type mapping…</a></li>
|
||||
<li ng-if="(file.features | filter:{isMarker: true}).length > 0" role="menuitem" ng-repeat="type in client.types | fmObjectFilter:{type:'marker'}"><a href="javascript:" ng-click="addAllToMap(type)">Add all markers as {{type.name}}</a></li>
|
||||
<li ng-if="(file.features | filter:{isLine: true}).length > 0" role="menuitem" ng-repeat="type in client.types | fmObjectFilter:{type:'line'}"><a href="javascript:" ng-click="addAllToMap(type)">Add all lines/polygons as {{type.name}}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</SearchResults>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
.fm-import-tab.fm-import-tab.fm-import-tab {
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
|
@ -4,14 +4,13 @@ import { Component, Ref } from "vue-property-decorator";
|
|||
import { InjectMapComponents, InjectMapContext } from "../../utils/decorators";
|
||||
import { MapComponents, MapContext } from "../leaflet-map/leaflet-map";
|
||||
import { showErrorToast } from "../../utils/toasts";
|
||||
import { FileResult, FileResultObject, parseFiles } from "../../utils/files";
|
||||
import { FileResultObject, parseFiles } from "../../utils/files";
|
||||
import pluralize from "pluralize";
|
||||
import Icon from "../ui/icon/icon";
|
||||
import "./import.scss";
|
||||
import { SearchResultsLayer } from "facilmap-leaflet";
|
||||
import { Util } from "leaflet";
|
||||
import FileResults from "../file-results/file-results";
|
||||
import { flyTo } from "../../utils/zoom";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
|
@ -79,18 +78,6 @@ export default class Import extends Vue {
|
|||
this.importFiles(event.dataTransfer?.files);
|
||||
}
|
||||
|
||||
zoomToResult(result: FileResult): void {
|
||||
const layer = this.layers.find((layer, idx) => this.files[idx].features.includes(result));
|
||||
if (!layer)
|
||||
return;
|
||||
|
||||
const featureLayer = layer.getLayers().find((l) => l._fmSearchResult === result) as any;
|
||||
if (!featureLayer)
|
||||
return;
|
||||
|
||||
flyTo(this.mapComponents.map, { bounds: featureLayer.getBounds() });
|
||||
}
|
||||
|
||||
async importFiles(files: FileList | undefined): Promise<void> {
|
||||
this.$bvToast.hide("fm-import-error");
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div>
|
||||
<input type="file" multiple="multiple" class="d-none" ref="fileInput" @change="importFiles(fileInput.files)">
|
||||
<portal to="fm-search-box">
|
||||
<b-tab v-for="(file, idx) in files" :id="`fm-import-tab-${idx}`">
|
||||
<b-tab v-for="(file, idx) in files" :id="`fm-import-tab-${idx}`" class="fm-import-tab">
|
||||
<template #title>
|
||||
<span class="closeable-tab-title">
|
||||
<span>{{file.title}}</span>
|
||||
|
@ -11,7 +11,7 @@
|
|||
<FileResults
|
||||
:file="file"
|
||||
:layer-id="layerIds[idx]"
|
||||
@click-result="zoomToResult"
|
||||
auto-zoom
|
||||
></FileResults>
|
||||
</b-tab>
|
||||
</portal>
|
||||
|
|
|
@ -33,8 +33,9 @@ export default class SearchBox extends Vue {
|
|||
cardHeader!: HTMLElement;
|
||||
|
||||
tab = 0;
|
||||
panStartTop: number | null = null;
|
||||
restoreTop: number | null = null;
|
||||
tabHistory = [0];
|
||||
panStartHeight: number | null = null;
|
||||
restoreHeight: number | null = null;
|
||||
resizeStartHeight: number | null = null;
|
||||
resizeStartWidth: number | null = null;
|
||||
hasFocus = false;
|
||||
|
@ -70,37 +71,44 @@ export default class SearchBox extends Vue {
|
|||
}
|
||||
|
||||
handlePanStart(event: any): void {
|
||||
this.restoreTop = null;
|
||||
this.panStartTop = parseInt($(this.searchBox).css("flex-basis"));
|
||||
this.restoreHeight = null;
|
||||
this.panStartHeight = parseInt($(this.searchBox).css("flex-basis"));
|
||||
}
|
||||
|
||||
handlePanMove(event: any): void {
|
||||
if (this.isNarrow && this.panStartTop != null && event.srcEvent.type != "pointercancel")
|
||||
$(this.searchBox).stop().css("flexBasis", `${this.getSanitizedTop(this.panStartTop - event.deltaY)}px`);
|
||||
if (this.isNarrow && this.panStartHeight != null && event.srcEvent.type != "pointercancel")
|
||||
$(this.searchBox).stop().css("flexBasis", `${this.getSanitizedHeight(this.panStartHeight - event.deltaY)}px`);
|
||||
}
|
||||
|
||||
handlePanEnd(): void {
|
||||
this.mapComponents.map.invalidateSize({ animate: true });
|
||||
this.mapComponents.map.invalidateSize({ pan: false });
|
||||
}
|
||||
|
||||
getTopFromBottom(bottom: number): number {
|
||||
return (this.searchBox.offsetParent as HTMLElement).offsetHeight - bottom;
|
||||
getSanitizedHeight(height: number): number {
|
||||
const maxHeight = (this.searchBox.offsetParent as HTMLElement).offsetHeight - 45;
|
||||
return Math.max(0, Math.min(maxHeight, height));
|
||||
}
|
||||
|
||||
getSanitizedTop(top: number): number {
|
||||
const maxTop = (this.searchBox.offsetParent as HTMLElement).offsetHeight - 45;
|
||||
return Math.max(0, Math.min(maxTop, top));
|
||||
}
|
||||
|
||||
handleActivateTab(): void {
|
||||
this.restoreTop = null;
|
||||
handleActivateTab(idx: number): void {
|
||||
this.restoreHeight = null;
|
||||
this.tabHistory = [
|
||||
...this.tabHistory.filter((tab) => tab != idx),
|
||||
idx
|
||||
];
|
||||
}
|
||||
|
||||
handleChanged(newTabs: BTab[], oldTabs: BTab[]): void {
|
||||
if (this.restoreTop != null && newTabs.length < oldTabs.length) {
|
||||
$(this.searchBox).animate({ top: this.restoreTop });
|
||||
this.restoreTop = null;
|
||||
if (this.restoreHeight != null && newTabs.length < oldTabs.length) {
|
||||
$(this.searchBox).animate({ flexBasis: this.restoreHeight }, () => {
|
||||
this.mapComponents.map.invalidateSize({ pan: false });
|
||||
});
|
||||
this.restoreHeight = null;
|
||||
}
|
||||
|
||||
const lastActiveTab = this.tabHistory[this.tabHistory.length - 1];
|
||||
this.tabHistory = this.tabHistory.map((idx) => newTabs.indexOf(oldTabs[idx])).filter((idx) => idx != -1);
|
||||
if (!newTabs.includes(oldTabs[lastActiveTab]))
|
||||
this.tab = this.tabHistory[this.tabHistory.length - 1];
|
||||
}
|
||||
|
||||
handleShowTab(id: string, expand = true): void {
|
||||
|
@ -110,11 +118,12 @@ export default class SearchBox extends Vue {
|
|||
|
||||
if (this.isNarrow && expand) {
|
||||
setTimeout(() => {
|
||||
const maxTop = this.getSanitizedTop(this.getTopFromBottom(300));
|
||||
const currentTop = this.searchBox.offsetTop;
|
||||
if (currentTop > maxTop) {
|
||||
this.restoreTop = currentTop;
|
||||
$(this.searchBox).animate({ top: maxTop });
|
||||
const currentHeight = parseInt($(this.searchBox).css("flex-basis"));
|
||||
if (currentHeight < 120) {
|
||||
this.restoreHeight = currentHeight;
|
||||
$(this.searchBox).animate({ flexBasis: 120 }, () => {
|
||||
this.mapComponents.map.invalidateSize({ pan: false });
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
@ -146,4 +155,13 @@ export default class SearchBox extends Vue {
|
|||
this.searchBoxContext.$emit("resizereset");
|
||||
}
|
||||
|
||||
handleFocusIn(e: FocusEvent): void {
|
||||
if ((e.target as HTMLElement).closest("input,textarea"))
|
||||
this.hasFocus = true;
|
||||
}
|
||||
|
||||
handleFocusOut(e: FocusEvent): void {
|
||||
this.hasFocus = false;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<b-card no-body ref="searchBox" class="fm-search-box" :class="{ isNarrow, hasFocus }" @focusin="hasFocus = true" @focusout="hasFocus = false">
|
||||
<b-card no-body ref="searchBox" class="fm-search-box" :class="{ isNarrow, hasFocus }" @focusin="handleFocusIn" @focusout="handleFocusOut">
|
||||
<b-tabs card align="center" v-model="tab" ref="tabsComponent" @changed="handleChanged" @activate-tab="handleActivateTab" no-fade>
|
||||
<SearchFormTab></SearchFormTab>
|
||||
<RouteFormTab></RouteFormTab>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#fm-search-form-tab {
|
||||
.fm-search-form-tab.fm-search-form-tab.fm-search-form-tab {
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<b-tab title="Search" id="fm-search-form-tab">
|
||||
<b-tab title="Search" id="fm-search-form-tab" class="fm-search-form-tab">
|
||||
<SearchForm></SearchForm>
|
||||
</b-tab>
|
|
@ -10,9 +10,11 @@ import { showErrorToast } from "../../utils/toasts";
|
|||
import { FindOnMapResult, SearchResult } from "facilmap-types";
|
||||
import SearchResults from "../search-results/search-results";
|
||||
import context from "../context";
|
||||
import { combineZoomDestinations, flyTo, getZoomDestinationForMapResult, getZoomDestinationForResults, getZoomDestinationForSearchResult } from "../../utils/zoom";
|
||||
import { flyTo, getZoomDestinationForMapResult, getZoomDestinationForResults, getZoomDestinationForSearchResult } from "../../utils/zoom";
|
||||
import { MapComponents, MapContext } from "../leaflet-map/leaflet-map";
|
||||
import { Util } from "leaflet";
|
||||
import { isMapResult } from "../../utils/search";
|
||||
import storage from "../../utils/storage";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
|
@ -29,8 +31,6 @@ export default class SearchForm extends Vue {
|
|||
autofocus = !context.isNarrow && context.autofocus;
|
||||
searchString = "";
|
||||
loadedSearchString = "";
|
||||
autoZoom = true;
|
||||
zoomToAll = false;
|
||||
searchCounter = 0;
|
||||
layerId: number = null as any;
|
||||
|
||||
|
@ -41,6 +41,22 @@ export default class SearchForm extends Vue {
|
|||
this.layerId = Util.stamp(this.mapComponents.searchResultsLayer);
|
||||
}
|
||||
|
||||
get autoZoom(): boolean {
|
||||
return storage.autoZoom;
|
||||
}
|
||||
|
||||
set autoZoom(autoZoom: boolean) {
|
||||
storage.autoZoom = autoZoom;
|
||||
}
|
||||
|
||||
get zoomToAll(): boolean {
|
||||
return storage.zoomToAll;
|
||||
}
|
||||
|
||||
set zoomToAll(zoomToAll: boolean) {
|
||||
storage.zoomToAll = zoomToAll;
|
||||
}
|
||||
|
||||
async search(): Promise<void> {
|
||||
this.searchInput.blur();
|
||||
|
||||
|
@ -120,26 +136,8 @@ export default class SearchForm extends Vue {
|
|||
this.mapComponents.searchResultsLayer.setResults([]);
|
||||
};
|
||||
|
||||
handleClickResult(result: SearchResult | FindOnMapResult): void {
|
||||
if (this.autoZoom) {
|
||||
if (this.zoomToAll)
|
||||
this.unionZoomToResult(result);
|
||||
else
|
||||
this.zoomToResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
zoomToResult(result: SearchResult | FindOnMapResult): void {
|
||||
const dest = "kind" in result ? getZoomDestinationForMapResult(result) : getZoomDestinationForSearchResult(result);
|
||||
if (dest)
|
||||
flyTo(this.mapComponents.map, dest);
|
||||
}
|
||||
|
||||
unionZoomToResult(result: SearchResult | FindOnMapResult): void {
|
||||
// Zoom to item, keep current map bounding box in view
|
||||
let dest = "kind" in result ? getZoomDestinationForMapResult(result) : getZoomDestinationForSearchResult(result);
|
||||
if (dest)
|
||||
dest = combineZoomDestinations([dest, { bounds: this.mapComponents.map.getBounds() }]);
|
||||
const dest = isMapResult(result) ? getZoomDestinationForMapResult(result) : getZoomDestinationForSearchResult(result);
|
||||
if (dest)
|
||||
flyTo(this.mapComponents.map, dest);
|
||||
}
|
||||
|
@ -163,19 +161,6 @@ export default class SearchForm extends Vue {
|
|||
}
|
||||
|
||||
/* TODO
|
||||
scope.addAllToMap = function(type) {
|
||||
for(let result of scope.searchResults.features) {
|
||||
if((type.type == "marker" && result.isMarker) || (type.type == "line" && result.isLine))
|
||||
scope.addResultToMap(result, type, true);
|
||||
}
|
||||
};
|
||||
|
||||
scope.customImport = function() {
|
||||
importUi.openImportDialog(scope.searchResults);
|
||||
};
|
||||
|
||||
var layerGroup = L.featureGroup([]).addTo(map.map);
|
||||
|
||||
function getZoomDestination(result, unionZoom) {
|
||||
let forBounds = (bounds) => ([
|
||||
bounds.getCenter(),
|
||||
|
@ -201,88 +186,10 @@ export default class SearchForm extends Vue {
|
|||
}
|
||||
}
|
||||
|
||||
function prepareResults(results) {
|
||||
for(let result of results) {
|
||||
if((result.lat != null && result.lon != null) || result.geojson && result.geojson.type == "Point")
|
||||
result.isMarker = true;
|
||||
if([ "LineString", "MultiLineString", "Polygon", "MultiPolygon" ].indexOf(result.geojson && result.geojson.type) != -1)
|
||||
result.isLine = true;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSearchResults() {
|
||||
if(scope.searchResults && scope.searchResults.features.length > 0) {
|
||||
prepareResults(scope.searchResults.features);
|
||||
|
||||
scope.searchResults.features.forEach(function(result) {
|
||||
renderResult(scope.submittedSearchString, scope.searchResults.features, result, false, layerGroup);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearRenders() {
|
||||
layerGroup.clearLayers();
|
||||
if(scope.searchResults) {
|
||||
scope.searchResults.features.forEach((result) => {
|
||||
result.marker = null;
|
||||
result.layer = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var queryUi = searchUi.queryUi = {
|
||||
show: function() {
|
||||
el.show();
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
scope.reset();
|
||||
el.hide();
|
||||
},
|
||||
|
||||
search: function(query, noZoom, showAll) {
|
||||
if(query != null)
|
||||
scope.searchString = query;
|
||||
|
||||
if(showAll != null)
|
||||
scope.showAll = showAll;
|
||||
|
||||
scope.search(noZoom);
|
||||
},
|
||||
|
||||
showFiles: function(files) {
|
||||
scope.submittedSearchString = "";
|
||||
scope.showAll = true;
|
||||
scope.searchResults = filesUi.parseFiles(files);
|
||||
scope.mapResults = null;
|
||||
renderSearchResults();
|
||||
|
||||
scope.zoomToAll();
|
||||
},
|
||||
|
||||
getSubmittedSearch: function() {
|
||||
return scope.submittedSearchString;
|
||||
},
|
||||
|
||||
isZoomedToSubmittedSearch: function() {
|
||||
if(scope.searchResults && scope.searchResults.features.length > 0) {
|
||||
let [center, zoom] = getZoomDestination();
|
||||
return map.map.getZoom() == zoom && fmUtils.pointsEqual(map.map.getCenter(), center, map.map);
|
||||
}
|
||||
},
|
||||
|
||||
hasResults: function() {
|
||||
return !!scope.searchResults;
|
||||
}
|
||||
};
|
||||
|
||||
scope.$on("$destroy", () => {
|
||||
scope.reset();
|
||||
searchUi.searchUi = null;
|
||||
});
|
||||
|
||||
var filesUi = fmSearchFiles(map);
|
||||
var importUi = fmSearchImport(map);
|
||||
}
|
||||
};
|
||||
}); */
|
||||
|
|
|
@ -20,9 +20,8 @@
|
|||
v-if="searchResults || mapResults"
|
||||
:search-results="searchResults"
|
||||
:map-results="mapResults"
|
||||
:show-zoom="!autoZoom || zoomToAll"
|
||||
:auto-zoom="autoZoom"
|
||||
:union-zoom="zoomToAll"
|
||||
:layer-id="layerId"
|
||||
@click-result="handleClickResult"
|
||||
@zoom-result="zoomToResult"
|
||||
></SearchResults>
|
||||
</div>
|
|
@ -10,6 +10,7 @@ import "./search-result-info.scss";
|
|||
import { FileResult } from "../../utils/files";
|
||||
import { MapComponents, MapContext } from "../leaflet-map/leaflet-map";
|
||||
import { isLineResult, isMarkerResult } from "../../utils/search";
|
||||
import { flyTo, getZoomDestinationForSearchResult } from "../../utils/zoom";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
|
@ -39,6 +40,12 @@ export default class SearchResultInfo extends Vue {
|
|||
return Object.values(this.client.types).filter((type) => (this.isMarker && type.type == "marker") || (this.isLine && type.type == "line"));
|
||||
}
|
||||
|
||||
zoomToResult(): void {
|
||||
const dest = getZoomDestinationForSearchResult(this.result);
|
||||
if (dest)
|
||||
flyTo(this.mapComponents.map, dest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* function showResultInfoBox(query, results, result, onClose) {
|
||||
|
|
|
@ -23,10 +23,13 @@
|
|||
</dl>
|
||||
|
||||
<b-button-toolbar>
|
||||
<b-button v-b-tooltip="'Zoom to search result'" @click="zoomToResult()" size="sm"><Icon icon="zoom-in" alt="Zoom to search result"></Icon></b-button>
|
||||
|
||||
<b-dropdown v-if="!client.readonly && types.length > 1" text="Add to map" size="sm">
|
||||
<b-dropdown-item v-for="type in types" href="javascript:" @click="$emit('add-to-map', type)">{{type.name}}</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
<b-button v-if="!client.readonly && types.length == 1" @click="$emit('add-to-map', types[0])" size="sm">Add to map</b-button>
|
||||
|
||||
<b-dropdown v-if="isMarker" text="Use as" size="sm">
|
||||
<b-dropdown-item href="javascript:" @click="$emit('use-as-from')">Route start</b-dropdown-item>
|
||||
<b-dropdown-item href="javascript:" @click="$emit('use-as-via')">Route via</b-dropdown-item>
|
||||
|
|
|
@ -36,4 +36,8 @@
|
|||
.fm-search-box-collapse-point {
|
||||
min-height: 3em;
|
||||
}
|
||||
|
||||
.btn-toolbar {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import WithRender from "./search-results.vue";
|
||||
import Vue from "vue";
|
||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||
import { FindOnMapResult, LineCreate, MarkerCreate, SearchResult, Type } from "facilmap-types";
|
||||
import { FindOnMapResult, ID, LineCreate, MarkerCreate, SearchResult, Type } from "facilmap-types";
|
||||
import "./search-results.scss";
|
||||
import Icon from "../ui/icon/icon";
|
||||
import Client from "facilmap-client";
|
||||
|
@ -11,14 +11,17 @@ import SearchResultInfo from "../search-result-info/search-result-info";
|
|||
import { MapComponents, MapContext } from "../leaflet-map/leaflet-map";
|
||||
import { SelectedItem } from "../../utils/selection";
|
||||
import { Point } from "geojson";
|
||||
import { FileResult } from "../../utils/files";
|
||||
import { FileResult, FileResultObject } from "../../utils/files";
|
||||
import { showErrorToast } from "../../utils/toasts";
|
||||
import { lineStringToTrackPoints, mapSearchResultToType } from "./utils";
|
||||
import { isFileResult, isSearchResult } from "../../utils/search";
|
||||
import { isFileResult, isLineResult, isMapResult, isMarkerResult } from "../../utils/search";
|
||||
import { combineZoomDestinations, flyTo, getZoomDestinationForMapResult, getZoomDestinationForResults, getZoomDestinationForSearchResult } from "../../utils/zoom";
|
||||
import { mapValues, pickBy, uniq } from "lodash";
|
||||
import FormModal from "../ui/form-modal/form-modal";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
components: { Icon, SearchResultInfo }
|
||||
components: { FormModal, Icon, SearchResultInfo }
|
||||
})
|
||||
export default class SearchResults extends Vue {
|
||||
|
||||
|
@ -28,8 +31,12 @@ export default class SearchResults extends Vue {
|
|||
|
||||
@Prop({ type: Array }) searchResults?: Array<SearchResult | FileResult>;
|
||||
@Prop({ type: Array }) mapResults?: FindOnMapResult[];
|
||||
@Prop({ type: Boolean, default: false }) showZoom!: boolean;
|
||||
@Prop({ type: Number, required: true }) layerId!: number;
|
||||
/** When clicking a search result, union zoom to it. Normal zoom is done when clicking the zoom button. */
|
||||
@Prop({ type: Boolean, default: false }) unionZoom!: boolean;
|
||||
/** When clicking or selecting a search result, zoom to it. */
|
||||
@Prop({ type: Boolean, default: false }) autoZoom!: boolean;
|
||||
@Prop({ type: Object, default: () => ({}) }) customTypes!: FileResultObject["types"];
|
||||
|
||||
activeTab = 0;
|
||||
|
||||
|
@ -37,14 +44,18 @@ export default class SearchResults extends Vue {
|
|||
return context.isNarrow;
|
||||
}
|
||||
|
||||
get openResult(): SearchResult | undefined {
|
||||
if (this.activeResults.length == 1 && !("kind" in this.activeResults[0]))
|
||||
get showZoom(): boolean {
|
||||
return !this.autoZoom || this.unionZoom;
|
||||
}
|
||||
|
||||
get openResult(): SearchResult | FileResult | undefined {
|
||||
if (this.activeResults.length == 1 && !isMapResult(this.activeResults[0]))
|
||||
return this.activeResults[0];
|
||||
else
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get activeResults(): Array<SearchResult | FindOnMapResult> {
|
||||
get activeResults(): Array<SearchResult | FileResult | FindOnMapResult> {
|
||||
return [
|
||||
...(this.searchResults || []).filter((result) => this.mapContext.selection.some((item) => item.type == "searchResult" && item.result === result)),
|
||||
...(this.mapResults || []).filter((result) => {
|
||||
|
@ -58,30 +69,71 @@ export default class SearchResults extends Vue {
|
|||
];
|
||||
}
|
||||
|
||||
get isAllSelected(): boolean {
|
||||
return !this.searchResults?.some((result) => !this.activeResults.includes(result));
|
||||
}
|
||||
|
||||
get activeSearchResults(): Array<SearchResult | FileResult> {
|
||||
return this.activeResults.filter((result) => !isMapResult(result)) as any;
|
||||
}
|
||||
|
||||
get activeMarkerSearchResults(): Array<SearchResult | FileResult> {
|
||||
return this.activeSearchResults.filter((result) => isMarkerResult(result)) as any;
|
||||
}
|
||||
|
||||
get activeLineSearchResults(): Array<SearchResult | FileResult> {
|
||||
return this.activeSearchResults.filter((result) => isLineResult(result)) as any;
|
||||
}
|
||||
|
||||
get markerTypes(): Array<Type> {
|
||||
return Object.values(this.client.types).filter((type) => type.type == "marker");
|
||||
}
|
||||
|
||||
get lineTypes(): Array<Type> {
|
||||
return Object.values(this.client.types).filter((type) => type.type == "line");
|
||||
}
|
||||
|
||||
get hasCustomTypes(): boolean {
|
||||
return Object.keys(this.customTypes).length > 0;
|
||||
}
|
||||
|
||||
closeResult(): void {
|
||||
this.activeTab = 0;
|
||||
}
|
||||
|
||||
@Watch("openResult")
|
||||
handleOpenResultChange(openResult: SearchResult | undefined): void {
|
||||
handleOpenResultChange(openResult: SearchResult | FileResult | undefined): void {
|
||||
if (!openResult && this.activeTab != 0)
|
||||
this.activeTab = 0;
|
||||
}
|
||||
|
||||
handleClick(result: SearchResult | FindOnMapResult, event: MouseEvent): void {
|
||||
this.selectResult(result, event.ctrlKey || event.shiftKey);
|
||||
this.$emit('click-result', result);
|
||||
handleClick(result: SearchResult | FileResult | FindOnMapResult, event: MouseEvent): void {
|
||||
const toggle = event.ctrlKey || event.shiftKey;
|
||||
this.selectResult(result, toggle);
|
||||
|
||||
if (this.autoZoom)
|
||||
this.zoomToSelectedResults(this.unionZoom || toggle);
|
||||
}
|
||||
|
||||
handleZoom(result: SearchResult | FindOnMapResult, event: MouseEvent): void {
|
||||
this.$emit('zoom-result', result);
|
||||
zoomToSelectedResults(unionZoom: boolean): void {
|
||||
let dest = getZoomDestinationForResults(this.activeResults);
|
||||
if (dest && unionZoom)
|
||||
dest = combineZoomDestinations([dest, { bounds: this.mapComponents.map.getBounds() }]);
|
||||
if (dest)
|
||||
flyTo(this.mapComponents.map, dest);
|
||||
}
|
||||
|
||||
handleOpen(result: SearchResult | FindOnMapResult, event: MouseEvent): void {
|
||||
zoomToResult(result: SearchResult | FileResult | FindOnMapResult): void {
|
||||
const dest = isMapResult(result) ? getZoomDestinationForMapResult(result) : getZoomDestinationForSearchResult(result);
|
||||
if (dest)
|
||||
flyTo(this.mapComponents.map, dest);
|
||||
}
|
||||
|
||||
handleOpen(result: SearchResult | FileResult | FindOnMapResult, event: MouseEvent): void {
|
||||
this.selectResult(result, false);
|
||||
|
||||
setTimeout(async () => {
|
||||
if ("kind" in result) {
|
||||
if (isMapResult(result)) {
|
||||
if (result.kind == "marker" && !this.client.markers[result.id])
|
||||
await this.client.getMarker({ id: result.id });
|
||||
this.mapContext.$emit("fm-search-box-show-tab", "fm-marker-info-tab", false);
|
||||
|
@ -90,19 +142,35 @@ export default class SearchResults extends Vue {
|
|||
}, 0);
|
||||
}
|
||||
|
||||
selectResult(result: SearchResult | FindOnMapResult, toggle: boolean): void {
|
||||
const item: SelectedItem = "kind" in result ? { type: result.kind, id: result.id } : { type: "searchResult", result, layerId: this.layerId };
|
||||
selectResult(result: SearchResult | FileResult | FindOnMapResult, toggle: boolean): void {
|
||||
const item: SelectedItem = isMapResult(result) ? { type: result.kind, id: result.id } : { type: "searchResult", result, layerId: this.layerId };
|
||||
if (toggle)
|
||||
this.mapComponents.selectionHandler.toggleItem(item);
|
||||
else
|
||||
this.mapComponents.selectionHandler.setSelectedItems([item]);
|
||||
}
|
||||
|
||||
async addToMap(results: Array<SearchResult | FileResult>, type: Type): Promise<void> {
|
||||
toggleSelectAll(): void {
|
||||
if (!this.searchResults)
|
||||
return;
|
||||
|
||||
if (this.isAllSelected)
|
||||
this.mapComponents.selectionHandler.setSelectedItems([]);
|
||||
else {
|
||||
this.mapComponents.selectionHandler.setSelectedItems(this.searchResults.map((result) => ({ type: "searchResult", result, layerId: this.layerId })));
|
||||
|
||||
if (this.autoZoom)
|
||||
this.zoomToSelectedResults(true);
|
||||
}
|
||||
}
|
||||
|
||||
async _addToMap(data: Array<{ result: SearchResult | FileResult; type: Type }>): Promise<boolean> {
|
||||
this.$bvToast.hide("fm-search-result-info-add-error");
|
||||
|
||||
try {
|
||||
for (const result of results) {
|
||||
const selection: SelectedItem[] = [];
|
||||
|
||||
for (const { result, type } of data) {
|
||||
const obj: Partial<MarkerCreate & LineCreate> = {
|
||||
name: result.short_name
|
||||
};
|
||||
|
@ -122,7 +190,7 @@ export default class SearchResults extends Vue {
|
|||
typeId: type.id
|
||||
});
|
||||
|
||||
this.mapComponents.selectionHandler.setSelectedItems([{ type: "marker", id: marker.id }], true);
|
||||
selection.push({ type: "marker", id: marker.id });
|
||||
} else if(type.type == "line") {
|
||||
if (obj.routePoints) {
|
||||
const line = await this.client.addLine({
|
||||
|
@ -131,7 +199,7 @@ export default class SearchResults extends Vue {
|
|||
typeId: type.id
|
||||
});
|
||||
|
||||
this.mapComponents.selectionHandler.setSelectedItems([{ type: "line", id: line.id }], true);
|
||||
selection.push({ type: "line", id: line.id });
|
||||
} else {
|
||||
const trackPoints = lineStringToTrackPoints(result.geojson as any);
|
||||
const line = await this.client.addLine({
|
||||
|
@ -142,15 +210,24 @@ export default class SearchResults extends Vue {
|
|||
mode: "track"
|
||||
});
|
||||
|
||||
this.mapComponents.selectionHandler.setSelectedItems([{ type: "line", id: line.id }], true);
|
||||
selection.push({ type: "line", id: line.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.mapComponents.selectionHandler.setSelectedItems(selection, true);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
showErrorToast(this, "fm-search-result-info-add-error", "Error adding to map", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async addToMap(results: Array<SearchResult | FileResult>, type: Type): Promise<void> {
|
||||
this._addToMap(results.map((result) => ({ result, type })));
|
||||
}
|
||||
|
||||
useAs(result: SearchResult | FileResult, event: "fm-route-set-from" | "fm-route-add-via" | "fm-route-set-to"): void {
|
||||
if (isFileResult(result))
|
||||
this.mapContext.$emit(event, `${result.lat},${result.lon}`);
|
||||
|
@ -171,4 +248,130 @@ export default class SearchResults extends Vue {
|
|||
this.useAs(result, "fm-route-set-to");
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Custom mapping
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
customMapping: Record<ID, false | string> = {};
|
||||
untypedMarkerMapping: false | string = false;
|
||||
untypedLineMapping: false | string = false;
|
||||
isCustomImportSaving = false;
|
||||
|
||||
|
||||
get activeFileResults(): Array<FileResult> {
|
||||
return this.activeResults.filter(isFileResult);
|
||||
}
|
||||
|
||||
get activeFileResultsByType(): Record<ID, Array<FileResult>> {
|
||||
return mapValues(this.customTypes, (type, id) => this.activeFileResults.filter((result) => result.fmTypeId != null && `${result.fmTypeId}` == `${id}`));
|
||||
}
|
||||
|
||||
get untypedMarkers(): Array<FileResult> {
|
||||
return this.activeFileResults.filter((result) => (result.fmTypeId == null || !this.customTypes[result.fmTypeId]) && isMarkerResult(result));
|
||||
}
|
||||
|
||||
get untypedLines(): Array<FileResult> {
|
||||
return this.activeFileResults.filter((result) => (result.fmTypeId == null || !this.customTypes[result.fmTypeId]) && isLineResult(result));
|
||||
}
|
||||
|
||||
get customMappingOptions(): Record<ID, Array<{ value: string | false; text: string; disabled?: boolean }>> {
|
||||
return mapValues(pickBy(this.customTypes, (customType, customTypeId) => this.activeFileResultsByType[customTypeId as any].length > 0), (customType, customTypeId) => {
|
||||
const options: Array<{ value: string | false; text: string; disabled?: boolean }> = [];
|
||||
|
||||
for (const type of Object.values(this.client.types)) {
|
||||
if (type.name == customType.name && type.type == customType.type)
|
||||
options.push({ value: `e${type.id}`, text: `Existing type “${type.name}”` });
|
||||
}
|
||||
|
||||
if (this.client.writable == 2)
|
||||
options.push({ value: `i${customTypeId}`, text: `Import type “${customType.name}”` });
|
||||
|
||||
options.push({ value: false, text: "Do not import" });
|
||||
options.push({ value: false, text: "──────────", disabled: true });
|
||||
|
||||
for (const type of Object.values(this.client.types)) {
|
||||
if (type.name != customType.name && type.type == customType.type)
|
||||
options.push({ value: `e${type.id}`, text: `Existing type “${type.name}”` });
|
||||
}
|
||||
|
||||
for (const customTypeId2 of Object.keys(this.customTypes)) {
|
||||
const customType2 = this.customTypes[customTypeId2 as any];
|
||||
if (this.client.writable == 2 && customType2.type == customType.type && customTypeId2 != customTypeId)
|
||||
options.push({ value: `i${customTypeId2}`, text: `Import type “${customType2.name}”` });
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
}
|
||||
|
||||
get untypedMarkerMappingOptions(): Array<{ value: string | false; text: string }> {
|
||||
const options: Array<{ value: string | false; text: string }> = [];
|
||||
options.push({ value: false, text: "Do not import" });
|
||||
|
||||
for (const customTypeId of Object.keys(this.customTypes)) {
|
||||
const customType = this.customTypes[customTypeId as any];
|
||||
if (this.client.writable && customType.type == "marker")
|
||||
options.push({ value: `i${customTypeId}`, text: `Import type “${customType.name}”` });
|
||||
}
|
||||
|
||||
for (const type of Object.values(this.client.types)) {
|
||||
if (type.type == "marker")
|
||||
options.push({ value: `e${type.id}`, text: `Existing type “${type.name}”` });
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
get untypedLineMappingOptions(): Array<{ value: string | false; text: string }> {
|
||||
const options: Array<{ value: string | false; text: string }> = [];
|
||||
options.push({ value: false, text: "Do not import" });
|
||||
|
||||
for (const customTypeId of Object.keys(this.customTypes)) {
|
||||
const customType = this.customTypes[customTypeId as any];
|
||||
if (this.client.writable && customType.type == "line")
|
||||
options.push({ value: `i${customTypeId}`, text: `Import type “${customType.name}”` });
|
||||
}
|
||||
|
||||
for (const type of Object.values(this.client.types)) {
|
||||
if (type.type == "line")
|
||||
options.push({ value: `e${type.id}`, text: `Existing type “${type.name}”` });
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
initializeCustomImport(): void {
|
||||
this.customMapping = mapValues(this.customMappingOptions, (options) => options[0].value);
|
||||
this.untypedMarkerMapping = false;
|
||||
this.untypedLineMapping = false;
|
||||
}
|
||||
|
||||
async customImport(): Promise<void> {
|
||||
this.$bvToast.hide("fm-search-result-info-add-error");
|
||||
|
||||
try {
|
||||
const resolvedMapping: Record<string, Type> = {};
|
||||
for (const id of uniq([...Object.values(this.customMapping), this.untypedMarkerMapping, this.untypedLineMapping])) {
|
||||
if (id !== false) {
|
||||
const m = id.match(/^([ei])(.*)$/);
|
||||
if (m && m[1] == "e")
|
||||
resolvedMapping[id] = this.client.types[m[2] as any];
|
||||
else if (m && m[1] == "i")
|
||||
resolvedMapping[id] = await this.client.addType(this.customTypes[m[2] as any]);
|
||||
}
|
||||
}
|
||||
|
||||
const add = this.activeFileResults.flatMap((result) => {
|
||||
const id = (result.fmTypeId && this.customMapping[result.fmTypeId]) ? this.customMapping[result.fmTypeId] : isMarkerResult(result) ? this.untypedMarkerMapping : this.untypedLineMapping;
|
||||
return id !== false && resolvedMapping[id] ? [{ result, type: resolvedMapping[id] }] : [];
|
||||
});
|
||||
|
||||
if (await this._addToMap(add))
|
||||
this.$bvModal.hide("fm-search-results-custom-import");
|
||||
} catch(err) {
|
||||
showErrorToast(this, "fm-search-result-info-add-error", "Error importing to map", err);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
{{" "}}
|
||||
<span class="result-type">({{client.types[result.typeId].name}})</span>
|
||||
</span>
|
||||
<a v-if="showZoom" href="javascript:" @click="handleZoom(result, $event)" v-b-tooltip.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
|
||||
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-b-tooltip.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
|
||||
<a href="javascript:" @click="handleOpen(result, $event)" v-b-tooltip.left="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
|
@ -27,7 +27,7 @@
|
|||
{{" "}}
|
||||
<span class="result-type" v-if="result.type">({{result.type}})</span>
|
||||
</span>
|
||||
<a v-if="showZoom" href="javascript:" @click="handleZoom(result, $event)" v-b-tooltip.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
|
||||
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-b-tooltip.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
|
||||
<a href="javascript:" @click="handleOpen(result, $event)" v-b-tooltip.right="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
|
@ -35,18 +35,22 @@
|
|||
<slot name="after"></slot>
|
||||
</div>
|
||||
|
||||
<!-- <div class="fm-search-buttons" ng-show="searchResults.features.length > 0">
|
||||
<button type="button" class="btn btn-default" ng-model="showAll" ng-click="showAll && zoomToAll()" uib-btn-checkbox ng-show="searchResults.features.length > 1">Show all</button>
|
||||
<button type="button" class="btn btn-link" ng-click="reset()"><fm-icon fm-icon="remove" alt="Remove"></fm-icon></button>
|
||||
<div uib-dropdown keyboard-nav="true" class="pull-right dropup" ng-if="client.padId && !client.readonly">
|
||||
<button id="search-add-all-button" type="button" class="btn btn-default" uib-dropdown-toggle>Add all to map <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="search-add-all-button">
|
||||
<li ng-if="(searchResults.types | fmPropertyCount) > 0" role="menuitem"><a href="javascript:" ng-click="customImport()">Custom type mapping…</a></li>
|
||||
<li ng-if="(searchResults.features | filter:{isMarker: true}).length > 0" role="menuitem" ng-repeat="type in client.types | fmObjectFilter:{type:'marker'}"><a href="javascript:" ng-click="addAllToMap(type)">Add all markers as {{type.name}}</a></li>
|
||||
<li ng-if="(searchResults.features | filter:{isLine: true}).length > 0" role="menuitem" ng-repeat="type in client.types | fmObjectFilter:{type:'line'}"><a href="javascript:" ng-click="addAllToMap(type)">Add all lines/polygons as {{type.name}}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div> -->
|
||||
<b-button-toolbar v-if="client.padId && !client.readonly && searchResults && searchResults.length > 0">
|
||||
<b-button @click="toggleSelectAll" :pressed="isAllSelected">Select all</b-button>
|
||||
|
||||
<b-dropdown v-if="client.padId && !client.readonly" :disabled="activeSearchResults.length == 0" :text="`Add selected item${activeSearchResults.length == 1 ? '' : 's'} to map`">
|
||||
<template v-if="activeMarkerSearchResults.length > 0 && markerTypes.length ">
|
||||
<b-dropdown-item v-for="type in markerTypes" href="javascript:" @click="addToMap(activeMarkerSearchResults, type)">Marker items as {{type.name}}</b-dropdown-item>
|
||||
</template>
|
||||
<template v-if="activeLineSearchResults.length > 0 && lineTypes.length ">
|
||||
<b-dropdown-item v-for="type in lineTypes" href="javascript:" @click="addToMap(activeLineSearchResults, type)">Line/polygon items as {{type.name}}</b-dropdown-item>
|
||||
</template>
|
||||
<template v-if="hasCustomTypes">
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item href="javascript:" v-b-modal.fm-search-results-custom-import>Custom type mapping…</b-dropdown-item>
|
||||
</template>
|
||||
</b-dropdown>
|
||||
</b-button-toolbar>
|
||||
</b-carousel-slide>
|
||||
|
||||
<b-carousel-slide>
|
||||
|
@ -62,4 +66,39 @@
|
|||
></SearchResultInfo>
|
||||
</b-carousel-slide>
|
||||
</b-carousel>
|
||||
|
||||
|
||||
<FormModal
|
||||
id="fm-search-results-custom-import"
|
||||
title="Custom Import"
|
||||
dialog-class="fm-search-results-custom-import"
|
||||
:is-saving="isCustomImportSaving"
|
||||
is-create
|
||||
ok-title="Import"
|
||||
@submit="customImport"
|
||||
@show="initializeCustomImport"
|
||||
>
|
||||
<b-table-simple striped hover>
|
||||
<b-thead>
|
||||
<b-tr>
|
||||
<b-th>Type</b-th>
|
||||
<b-th>Map to…</b-th>
|
||||
</b-tr>
|
||||
</b-thead>
|
||||
<b-tbody>
|
||||
<b-tr v-for="(options, importTypeId) in customMappingOptions">
|
||||
<b-td><label :for="`map-type-${importTypeId}`">{{customTypes[importTypeId].type == 'marker' ? 'Markers' : 'Lines'}} of type “{{customTypes[importTypeId].name}}” ({{activeFileResultsByType[importTypeId].length}})</label></b-td>
|
||||
<b-td><b-form-select :id="`map-type-${importTypeId}`" v-model="customMapping[importTypeId]" :options="options"></b-form-select></b-td>
|
||||
</b-tr>
|
||||
<b-tr v-if="untypedMarkers.length > 0">
|
||||
<b-td><label for="map-untyped-markers">Untyped markers ({{untypedMarkers}})</label></b-td>
|
||||
<b-td><b-form-select id="map-untyped-markers" v-model="untypedMarkerMapping" :options="untypedMarkerMappingOptions"></b-form-select></b-td>
|
||||
</b-tr>
|
||||
<b-tr v-if="untypedLines.length > 0">
|
||||
<b-td><label for="map-untyped-lines">Untyped lines/polygons ({{untypedLines}})</label></b-td>
|
||||
<b-td><b-form-select id="map-untyped-lines" v-model="untypedLineMapping" :options="untypedLineMappingOptions"></b-form-select></b-td>
|
||||
</b-tr>
|
||||
</b-tbody>
|
||||
</b-table-simple>
|
||||
</FormModal>
|
||||
</div>
|
|
@ -4,12 +4,12 @@
|
|||
<Sidebar id="fm-toolbox-sidebar">
|
||||
<b-nav-item v-if="!client.padId && interactive" href="javascript:" v-b-modal.fm-toolbox-create-pad v-b-toggle.fm-toolbox-sidebar>Start collaborative map</b-nav-item>
|
||||
<b-nav-item-dropdown v-if="!client.readonly && client.padData" text="Add" :disabled="!!mapContext.interaction" right>
|
||||
<b-dropdown-item v-for="type in client.types" :disabled="!!mapContext.interaction" href="javascript:" @click="addObject(type)">{{type.name}}</b-dropdown-item>
|
||||
<b-dropdown-item v-for="type in client.types" :disabled="!!mapContext.interaction" href="javascript:" @click="addObject(type)" v-b-toggle.fm-toolbox-sidebar>{{type.name}}</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="client.writable == 2"></b-dropdown-divider>
|
||||
<b-dropdown-item v-if="client.writable == 2" :disabled="!!mapContext.interaction" href="javascript:" v-b-modal.fm-toolbox-manage-types>Manage types</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.writable == 2" :disabled="!!mapContext.interaction" href="javascript:" v-b-modal.fm-toolbox-manage-types v-b-toggle.fm-toolbox-sidebar>Manage types</b-dropdown-item>
|
||||
</b-nav-item-dropdown>
|
||||
<b-nav-item-dropdown v-if="client.padData && (!client.readonly || Object.keys(client.views).length > 0)" text="Views" right>
|
||||
<b-dropdown-item v-for="view in client.views" href="javascript:" @click="displayView(view)">{{view.name}}</b-dropdown-item>
|
||||
<b-dropdown-item v-for="view in client.views" href="javascript:" @click="displayView(view)" v-b-toggle.fm-toolbox-sidebar>{{view.name}}</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="client.writable == 2"></b-dropdown-divider>
|
||||
<b-dropdown-item v-if="client.writable == 2" href="javascript:" v-b-modal.fm-toolbox-save-view v-b-toggle.fm-toolbox-sidebar>Save current view</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.writable == 2" href="javascript:" v-b-modal.fm-toolbox-manage-views v-b-toggle.fm-toolbox-sidebar>Manage views</b-dropdown-item>
|
||||
|
@ -25,7 +25,7 @@
|
|||
</b-nav-item-dropdown>
|
||||
<b-nav-item-dropdown text="Tools" right>
|
||||
<!--<b-dropdown-item v-if="!client.readonly" @click="openDialog('copy-pad-dialog')">Copy pad</b-dropdown-item>-->
|
||||
<b-dropdown-item v-if="interactive" href="javascript:" @click="importFile()">Open file</b-dropdown-item>
|
||||
<b-dropdown-item v-if="interactive" href="javascript:" @click="importFile()" v-b-toggle.fm-toolbox-sidebar>Open file</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.padData" :href="`${client.padData.id}/geojson${filterQuery.q}`" v-b-tooltip.left="'GeoJSON files store all map information and can thus be used for map backups and be re-imported without any loss.'">Export as GeoJSON</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.padData" :href="`${client.padData.id}/gpx?useTracks=1${filterQuery.a}`" v-b-tooltip.left="'GPX files can be opened with most navigation software. In track mode, any calculated routes are saved in the file.'">Export as GPX (tracks)</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.padData" :href="`${client.padData.id}/gpx?useTracks=0${filterQuery.a}`" v-b-tooltip.left="'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 recalculate the routes.'">Export as GPX (routes)</b-dropdown-item>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
:no-close-on-esc="noCancel" :no-close-on-backdrop="noCancel" :hide-header-close="noCancel" :ok-only="noCancel"
|
||||
:busy="isSaving"
|
||||
:ok-disabled="!isCreate && !isModified"
|
||||
:ok-title="okTitle || (isCreate ? 'Create' : 'Save')"
|
||||
@ok.prevent="observer.handleSubmit(handleSubmit)"
|
||||
@show="$emit('show')"
|
||||
scrollable
|
||||
|
@ -29,7 +28,7 @@
|
|||
</b-button>
|
||||
<b-button v-if="noCancel || isModified || isCreate" variant="primary" @click="ok" :disabled="isSaving">
|
||||
<b-spinner small v-if="isSaving"></b-spinner>
|
||||
{{isCreate ? "Create" : "Save"}}
|
||||
{{okTitle || (isCreate ? 'Create' : 'Save')}}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
|
|
|
@ -41,6 +41,9 @@ export default class SelectionHandler extends Handler {
|
|||
_linesLayer: LinesLayer;
|
||||
_searchResultLayers: SearchResultsLayer[];
|
||||
|
||||
_mapFocusTime: number | undefined = undefined;
|
||||
_mapInteraction: number = 0;
|
||||
|
||||
constructor(map: Map, markersLayer: MarkersLayer, linesLayer: LinesLayer, searchResultsLayer: SearchResultsLayer) {
|
||||
super(map);
|
||||
|
||||
|
@ -55,6 +58,10 @@ export default class SelectionHandler extends Handler {
|
|||
for (const layer of this._searchResultLayers)
|
||||
layer.on("click", this.handleClickSearchResult);
|
||||
this._map.on("click", this.handleClickMap);
|
||||
this._map.on("fmInteractionStart", this.handleMapInteractionStart);
|
||||
this._map.on("fmInteractionEnd", this.handleMapInteractionEnd);
|
||||
this._map.getContainer().addEventListener("focusin", this.handleMapFocusIn);
|
||||
this._map.getContainer().addEventListener("focusout", this.handleMapFocusOut);
|
||||
}
|
||||
|
||||
removeHooks(): void {
|
||||
|
@ -63,6 +70,10 @@ export default class SelectionHandler extends Handler {
|
|||
for (const layer of this._searchResultLayers)
|
||||
layer.off("click", this.handleClickSearchResult);
|
||||
this._map.off("click", this.handleClickMap);
|
||||
this._map.off("fmInteractionStart", this.handleMapInteractionStart);
|
||||
this._map.off("fmInteractionEnd", this.handleMapInteractionEnd);
|
||||
this._map.getContainer().removeEventListener("focusin", this.handleMapFocusIn);
|
||||
this._map.getContainer().removeEventListener("focusout", this.handleMapFocusOut);
|
||||
}
|
||||
|
||||
addSearchResultLayer(layer: SearchResultsLayer): void {
|
||||
|
@ -82,6 +93,11 @@ export default class SelectionHandler extends Handler {
|
|||
|
||||
layer.off("click", this.handleClickSearchResult);
|
||||
this._searchResultLayers.splice(idx, 1);
|
||||
|
||||
const layerId = Util.stamp(layer);
|
||||
const without = this._selection.filter((item) => item.type != "searchResult" || item.layerId != layerId);
|
||||
if (without.length != this._selection.length)
|
||||
this.setSelectedItems(without);
|
||||
}
|
||||
|
||||
getSelection(): SelectedItem[] {
|
||||
|
@ -130,6 +146,9 @@ export default class SelectionHandler extends Handler {
|
|||
}
|
||||
|
||||
handleClickItem(item: SelectedItem, e: LeafletEvent): void {
|
||||
if (this._mapInteraction)
|
||||
return;
|
||||
|
||||
DomEvent.stopPropagation(e);
|
||||
if ((e.originalEvent as any).ctrlKey || (e.originalEvent as any).shiftKey)
|
||||
this.toggleItem(item, true);
|
||||
|
@ -153,14 +172,35 @@ export default class SelectionHandler extends Handler {
|
|||
}
|
||||
|
||||
handleClickMap = (e: LeafletEvent): void => {
|
||||
if (this._mapInteraction)
|
||||
return;
|
||||
|
||||
if (!(e.originalEvent as any).ctrlKey && !(e.originalEvent as any).shiftKey) {
|
||||
if (this._selection.length == 0)
|
||||
this.fire("fmMapClick", e);
|
||||
else
|
||||
if (this._selection.length == 0) {
|
||||
// Focus event is fired before click event. Delay so that our click handlers knows whether the map was focused before it was clicked.
|
||||
if (this._mapFocusTime && Date.now() - this._mapFocusTime > 500)
|
||||
this.fire("fmMapClick", e);
|
||||
} else
|
||||
this.setSelectedItems([]);
|
||||
}
|
||||
}
|
||||
|
||||
handleMapFocusIn = (): void => {
|
||||
this._mapFocusTime = Date.now();
|
||||
}
|
||||
|
||||
handleMapFocusOut = (): void => {
|
||||
this._mapFocusTime = undefined;
|
||||
}
|
||||
|
||||
handleMapInteractionStart = (): void => {
|
||||
this._mapInteraction++;
|
||||
}
|
||||
|
||||
handleMapInteractionEnd = (): void => {
|
||||
this._mapInteraction--;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default interface HashHandler extends Evented {}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import Vue from "vue";
|
||||
|
||||
export interface Storage {
|
||||
zoomToAll: boolean;
|
||||
autoZoom: boolean;
|
||||
}
|
||||
|
||||
const storage = Vue.observable({
|
||||
zoomToAll: false,
|
||||
autoZoom: true
|
||||
});
|
||||
|
||||
export default storage;
|
||||
|
||||
try {
|
||||
const val = localStorage.getItem("facilmap");
|
||||
if (val) {
|
||||
const parsed = JSON.parse(val);
|
||||
storage.zoomToAll = !!parsed.zoomToAll;
|
||||
storage.autoZoom = !!parsed.autoZoom;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error reading local storage", err);
|
||||
}
|
||||
|
||||
const watcher = new Vue({ data: { storage } });
|
||||
watcher.$watch("storage", () => {
|
||||
try {
|
||||
localStorage.setItem("facilmap", JSON.stringify(storage));
|
||||
} catch (err) {
|
||||
console.error("Error saving to local storage", err);
|
||||
}
|
||||
}, { deep: true });
|
|
@ -21,8 +21,8 @@ export function mergeObject<T extends Record<keyof any, any>>(oldObject: T | und
|
|||
}
|
||||
}
|
||||
|
||||
export function canControl(type: Type, what: keyof Marker | keyof Line, ignoreField?: Field): boolean {
|
||||
if((type as any)[what+"Fixed"] && ignoreField != null)
|
||||
export function canControl(type: Type, what: keyof Marker | keyof Line, ignoreField?: Field | null): boolean {
|
||||
if((type as any)[what+"Fixed"] && ignoreField !== null)
|
||||
return false;
|
||||
|
||||
const idx = "control"+what.charAt(0).toUpperCase() + what.slice(1);
|
||||
|
|
|
@ -3,6 +3,8 @@ import { fmToLeafletBbox, HashQuery } from "facilmap-leaflet";
|
|||
import Client, { RouteWithTrackPoints } from "facilmap-client";
|
||||
import { SelectedItem } from "./selection";
|
||||
import { FindOnMapLine, FindOnMapMarker, FindOnMapResult, Line, Marker, SearchResult } from "facilmap-types";
|
||||
import { Geometry } from "geojson";
|
||||
import { isMapResult } from "./search";
|
||||
|
||||
export type ZoomDestination = {
|
||||
center?: LatLng;
|
||||
|
@ -10,6 +12,21 @@ export type ZoomDestination = {
|
|||
bounds?: LatLngBounds;
|
||||
};
|
||||
|
||||
export function getZoomDestinationForGeoJSON(geojson: Geometry): ZoomDestination | undefined {
|
||||
if (geojson.type == "GeometryCollection")
|
||||
return combineZoomDestinations(geojson.geometries.map((geo) => getZoomDestinationForGeoJSON(geo)));
|
||||
else if (geojson.type == "Point")
|
||||
return { center: latLng(geojson.coordinates[1], geojson.coordinates[0]) };
|
||||
else if (geojson.type == "LineString" || geojson.type == "MultiPoint")
|
||||
return combineZoomDestinations(geojson.coordinates.map((pos) => ({ center: latLng(pos[1], pos[0]) })));
|
||||
else if (geojson.type == "Polygon" || geojson.type == "MultiLineString")
|
||||
return combineZoomDestinations(geojson.coordinates.flat().map((pos) => ({ center: latLng(pos[1], pos[0]) })));
|
||||
else if (geojson.type == "MultiPolygon")
|
||||
return combineZoomDestinations(geojson.coordinates.flat().flat().map((pos) => ({ center: latLng(pos[1], pos[0]) })));
|
||||
else
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getZoomDestinationForMarker(marker: Marker | FindOnMapMarker): ZoomDestination {
|
||||
return { center: latLng(marker.lat, marker.lon), zoom: 15 };
|
||||
}
|
||||
|
@ -29,10 +46,17 @@ export function getZoomDestinationForRoute(route: RouteWithTrackPoints): ZoomDes
|
|||
|
||||
export function getZoomDestinationForSearchResult(result: SearchResult): ZoomDestination | undefined {
|
||||
const dest: ZoomDestination = {};
|
||||
|
||||
if (result.boundingbox)
|
||||
dest.bounds = latLngBounds([[result.boundingbox[0], result.boundingbox[3]], [result.boundingbox[1], result.boundingbox[2]]]);
|
||||
else if (result.geojson)
|
||||
dest.bounds = getZoomDestinationForGeoJSON(result.geojson)?.bounds;
|
||||
|
||||
if (result.lat && result.lon)
|
||||
dest.center = latLng(Number(result.lat), Number(result.lon));
|
||||
else if (result.geojson)
|
||||
dest.center = getZoomDestinationForGeoJSON(result.geojson)?.center;
|
||||
|
||||
if (result.zoom != null)
|
||||
dest.zoom = result.zoom;
|
||||
|
||||
|
@ -48,12 +72,12 @@ export function getZoomDestinationForMapResult(result: FindOnMapResult): ZoomDes
|
|||
|
||||
export function getZoomDestinationForResults(results: Array<SearchResult | FindOnMapResult>): ZoomDestination | undefined {
|
||||
return combineZoomDestinations(results
|
||||
.map((result) => (("kind" in result) ? getZoomDestinationForMapResult(result) : getZoomDestinationForSearchResult(result)))
|
||||
.map((result) => (isMapResult(result) ? getZoomDestinationForMapResult(result) : getZoomDestinationForSearchResult(result)))
|
||||
.filter((result) => !!result) as ZoomDestination[]
|
||||
);
|
||||
}
|
||||
|
||||
export function combineZoomDestinations(destinations: ZoomDestination[]): ZoomDestination | undefined {
|
||||
export function combineZoomDestinations(destinations: Array<ZoomDestination | undefined>): ZoomDestination | undefined {
|
||||
if (destinations.length == 0)
|
||||
return undefined;
|
||||
else if (destinations.length == 1)
|
||||
|
@ -61,7 +85,8 @@ export function combineZoomDestinations(destinations: ZoomDestination[]): ZoomDe
|
|||
|
||||
const bounds = latLngBounds(undefined as any);
|
||||
for (const destination of destinations) {
|
||||
bounds.extend((destination.bounds || destination.center)!);
|
||||
if (destination)
|
||||
bounds.extend((destination.bounds || destination.center)!);
|
||||
}
|
||||
return { bounds };
|
||||
}
|
||||
|
@ -71,7 +96,7 @@ export function normalizeZoomDestination(map: Map, destination: ZoomDestination)
|
|||
if (result.center == null)
|
||||
result.center = destination.bounds!.getCenter();
|
||||
if (result.zoom == null)
|
||||
result.zoom = Math.min(15, map.getBoundsZoom(result.bounds!));
|
||||
result.zoom = result.bounds ? Math.min(15, map.getBoundsZoom(result.bounds)) : 15;
|
||||
return result as any;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Point } from "facilmap-types";
|
||||
import { Layer, LeafletMouseEvent, Map } from "leaflet";
|
||||
import { DomEvent, Layer, LeafletMouseEvent, Map } from "leaflet";
|
||||
import "./click-listener.scss";
|
||||
|
||||
class TransparentLayer extends Layer {
|
||||
|
@ -44,6 +44,7 @@ export function addClickListener(map: Map, listener: ClickListener, moveListener
|
|||
cancel();
|
||||
|
||||
e.originalEvent.preventDefault();
|
||||
DomEvent.stopPropagation(e);
|
||||
listener({ lat: e.latlng.lat, lon: e.latlng.lng });
|
||||
};
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ export default class LinesLayer extends FeatureGroup {
|
|||
trackPoints: [ ]
|
||||
};
|
||||
|
||||
let handler: ClickListenerHandle;
|
||||
let handler: ClickListenerHandle | undefined = undefined;
|
||||
|
||||
const addPoint = (pos: Point) => {
|
||||
line.routePoints.push(pos);
|
||||
|
@ -121,6 +121,7 @@ export default class LinesLayer extends FeatureGroup {
|
|||
};
|
||||
|
||||
const handleClick = (pos: Point) => {
|
||||
handler = undefined;
|
||||
if(line.routePoints.length > 0 && pos.lon == line.routePoints[line.routePoints.length-1].lon && pos.lat == line.routePoints[line.routePoints.length-1].lat)
|
||||
finishLine(true);
|
||||
else
|
||||
|
@ -135,7 +136,8 @@ export default class LinesLayer extends FeatureGroup {
|
|||
}
|
||||
|
||||
const finishLine = async (save: boolean) => {
|
||||
handler.cancel();
|
||||
if (handler)
|
||||
handler.cancel();
|
||||
this._deleteLine(line);
|
||||
|
||||
delete this._endDrawLine;
|
||||
|
|
|
@ -30,6 +30,7 @@ interface ShapeInfo {
|
|||
width: number;
|
||||
baseX: number;
|
||||
baseY: number;
|
||||
scale: number;
|
||||
}
|
||||
|
||||
const MARKER_SHAPES: Partial<Record<Shape, ShapeInfo>> = {
|
||||
|
@ -47,7 +48,8 @@ const MARKER_SHAPES: Partial<Record<Shape, ShapeInfo>> = {
|
|||
height: 36,
|
||||
width: 26,
|
||||
baseX: 13,
|
||||
baseY: 36
|
||||
baseY: 36,
|
||||
scale: 1
|
||||
},
|
||||
circle: {
|
||||
svg: (
|
||||
|
@ -63,7 +65,8 @@ const MARKER_SHAPES: Partial<Record<Shape, ShapeInfo>> = {
|
|||
height: 36,
|
||||
width: 36,
|
||||
baseX: 18,
|
||||
baseY: 18
|
||||
baseY: 18,
|
||||
scale: 0.85
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -186,8 +189,8 @@ export function getMarkerHtml(colour: string, height: number, symbol?: Symbol, s
|
|||
}
|
||||
|
||||
export function getMarkerIcon(colour: Colour, height: number, symbol?: Symbol, shape?: Shape, highlight = false): Icon {
|
||||
const scale = height / 31;
|
||||
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES.drop!;
|
||||
const scale = shapeObj.scale * height / shapeObj.height;
|
||||
return L.icon({
|
||||
iconUrl: getMarkerUrl(colour, height, symbol, shape, highlight),
|
||||
iconSize: [shapeObj.width*scale, shapeObj.height*scale],
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { Geometry } from "geojson";
|
||||
|
||||
export type SearchResultType = string;
|
||||
|
||||
export interface SearchResult {
|
||||
|
@ -9,7 +11,7 @@ export interface SearchResult {
|
|||
lon?: number;
|
||||
zoom?: number;
|
||||
extratags?: Record<string, string>;
|
||||
geojson?: GeoJSON.GeoJSON;
|
||||
geojson?: Geometry;
|
||||
icon?: string;
|
||||
type: SearchResultType;
|
||||
id?: string;
|
||||
|
|
Ładowanie…
Reference in New Issue