pull/147/head
Candid Dauth 2021-03-01 06:10:28 +01:00
rodzic 98079eb28e
commit cc87787a37
27 zmienionych plików z 399 dodań i 127 usunięć

Wyświetl plik

@ -0,0 +1,19 @@
.fm-about {
ul {
margin-left: 0;
padding-left: 0;
display: grid;
grid-template-columns: repeat(auto-fit, 180px);
gap: 5px;
li {
border: 1px solid rgba(0,0,0,.125);
display: flex;
a {
flex-grow: 1;
padding: 5px 10px;
}
}
}
}

Wyświetl plik

@ -4,6 +4,7 @@ import packageJson from "../../../package.json";
import WithRender from "./about.vue";
import { baseLayers, overlays } from "facilmap-leaflet";
import { Prop } from "vue-property-decorator";
import "./about.scss";
@WithRender
@Component({

Wyświetl plik

@ -1,17 +1,48 @@
<b-modal :id="id" :title="`About FacilMap ${fmVersion}`" ok-only ok-title="Close" size="lg">
<b-modal :id="id" :title="`About FacilMap ${fmVersion}`" ok-only ok-title="Close" size="lg" dialog-class="fm-about">
<p><a :href="fmHomepage" target="_blank"><strong>FacilMap</strong></a> is available under the <a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">GNU Affero General Public License, Version 3</a>.</p>
<p>If something does not work or you have a suggestion for improvement, please report on the <a :href="fmBugTracker" target="_blank">issue tracker</a>.</p>
<h4>Privacy</h4>
<p>
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.
</p>
<p>
FacilMap combines multiple third-party services (listed below under Map data) into one versatile map. When you use FacilMap,
your browser will retrieve the information provided by these services directly from there. FacilMap will send the current position
of the map and (for the search/route service) the search terms to those services. Your browser will send your IP address and
potentially some cookies that these services have set. It remains at the discretion of those third-party services what they
do with this data.
</p>
<h4>Map data</h4>
<dl class="row">
<template v-for="layer in layers" v-if="layer.options.attribution">
<dt class="col-sm-3">{{layer.options.fmName}}</dt>
<dd class="col-sm-9" v-html="layer.options.attribution"></dd>
</template>
<dt class="col-sm-3">Search</dt>
<dd class="col-sm-9"><a href="https://nominatim.openstreetmap.org/" target="_blank">Nominatim</a> / <a href="http://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
<dt class="col-sm-3">Directions</dt>
<dd class="col-sm-9"><a href="https://www.mapbox.com/api-documentation/#directions">Mapbox Directions API</a> / <a href="https://openrouteservice.org/">OpenRouteService</a> / <a href="http://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
<dt class="col-sm-3">GeoIP</dt>
<dd class="col-sm-9">This product includes GeoLite2 data created by MaxMind, available from <a href="https://www.maxmind.com">https://www.maxmind.com</a>.</dd>
</dl>
<h4>Programs/libraries</h4>
<ul class="list-inline">
<ul>
<li><a href="https://nodejs.org/" target="_blank">Node.js</a></li>
<li><a href="http://sequelizejs.com/" target="_blank">Sequelize</a></li>
<li><a href="https://sequelize.org/" target="_blank">Sequelize</a></li>
<li><a href="https://socket.io/" target="_blank">socket.io</a></li>
<li><a href="https://www.typescriptlang.org/" target="_blank">TypeScript</a></li>
<li><a href="https://webpack.js.org/" target="_blank">Webpack</a></li>
<li><a href="https://jquery.com/" target="_blank">jQuery</a></li>
<li><a href="https://angularjs.org/" target="_blank">AngularJS</a></li>
<li><a href="https://vuejs.org/" target="_blank">Vue.js</a></li>
<li><a href="https://github.com/chjj/marked" target="_blank">Marked</a></li>
<li><a href="https://getbootstrap.com/" target="_blank">Bootstrap</a></li>
<li><a href="https://angular-ui.github.io/bootstrap/" target="_blank">AngularUI Bootstrap</a></li>
<li><a href="https://bootstrap-vue.org/" target="_blank">BootstrapVue</a></li>
<li><a href="https://leafletjs.com/" target="_blank">Leaflet</a></li>
<li><a href="http://project-osrm.org/" target="_blank">OSRM</a></li>
<li><a href="https://openrouteservice.org/" target="_blank">OpenRouteService</a></li>
@ -19,25 +50,9 @@
<li><a href="https://github.com/joewalnes/filtrex" target="_blank">Filtrex</a></li>
</ul>
<h4>Icons</h4>
<ul class="list-inline">
<ul>
<li><a href="https://github.com/twain47/Open-SVG-Map-Icons/" target="_blank">Open SVG Map Icons</a></li>
<li><a href="https://glyphicons.com/" target="_blank">Glyphicons</a></li>
<li><a href="https://zavoloklom.github.io/material-design-iconic-font/index.html" target="_blank">Material Design Iconic Font</a></li>
</ul>
<h4>Map data</h4>
<dl class="dl-horizontal">
<template v-for="layer in layers" v-if="layer.attribution">
<dt>{{layer.name}}</dt>
<dd v-html="layer.attribution"></dd>
</template>
<dt>Search</dt>
<dd><a href="https://nominatim.openstreetmap.org/" target="_blank">Nominatim</a> / <a href="http://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
<dt>Directions</dt>
<dd><a href="https://www.mapbox.com/api-documentation/#directions">Mapbox Directions API</a> / <a href="https://openrouteservice.org/">OpenRouteService</a> / <a href="http://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
<dt>GeoIP</dt>
<dd>This product includes GeoLite2 data created by MaxMind, available from <a href="https://www.maxmind.com">https://www.maxmind.com</a>.</dd>
</dl>
</b-modal>

Wyświetl plik

@ -5,24 +5,11 @@ import "./client.scss";
import WithRender from "./client.vue";
const CLIENT_KEY = "fm-client";
const CLIENT_CONTEXT_KEY = "fm-client-context";
export function InjectClient() {
return InjectReactive(CLIENT_KEY);
}
export function InjectClientContext() {
return InjectReactive(CLIENT_CONTEXT_KEY);
}
export interface ClientContext {
loading: number;
/* padId: string | undefined;
disconnected: boolean;
serverError: Error | undefined;
deleted: boolean; */
}
@WithRender
@Component({})
export class ClientProvider extends Vue {
@ -30,22 +17,14 @@ export class ClientProvider extends Vue {
@Prop({ type: String, default: "/" }) readonly serverUrl!: string;
@Prop({ type: String }) readonly padId: string | undefined;
@ProvideReactive(CLIENT_KEY) client!: Client;
@ProvideReactive(CLIENT_CONTEXT_KEY) clientContext: ClientContext = {
loading: 0
};
@ProvideReactive(CLIENT_KEY) client: Client = null as any;
created(): void {
const client = new Client(this.serverUrl, this.padId);
client._set = Vue.set;
client._delete = Vue.delete;
this.client = client;
client.on("loadStart", () => {
this.clientContext.loading++;
});
client.on("loadEnd", () => {
this.clientContext.loading--;
});
}
}

Wyświetl plik

@ -1,17 +1,25 @@
import { decodeQueryString, encodeQueryString } from "facilmap-utils";
import Vue from "vue";
const queryParams = decodeQueryString(location.search);
const toBoolean = (val: string, def: boolean) => (val == null ? def : val != "0" && val != "false" && val != "no");
const context = {
const isNarrow = () => window.innerWidth < 768;
const context = Vue.observable({
activePadId: decodeURIComponent(location.pathname.match(/[^/]*$/)![0]),
urlPrefix: location.protocol + "//" + location.host + location.pathname.replace(/[^/]*$/, ""),
toolbox: toBoolean(queryParams.toolbox, true),
search: toBoolean(queryParams.search, true),
autofocus: toBoolean(queryParams.autofocus, parent === window),
legend: toBoolean(queryParams.legend, true),
interactive: toBoolean(queryParams.interactive, parent === window)
};
interactive: toBoolean(queryParams.interactive, parent === window),
isNarrow: isNarrow()
});
window.addEventListener("resize", () => {
context.isNarrow = isNarrow();
});
export default context;

Wyświetl plik

@ -47,6 +47,13 @@
text-shadow: 0 0 3px #fff, 0 0 5px #fff, 0 0 10px #fff;
}
}
.leaflet-control-locate.leaflet-control-locate a {
font-size: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
}
}
@ -75,20 +82,21 @@
.fm-leaflet-map-spinner {
position:absolute;
bottom: 7px;
left: 102px;
height:28px;
width:29px;
padding:16px 16px 16px 15px;
background:url(./spinner-background.png);
box-sizing: content-box;
bottom: 20px;
left: 115px;
color: #00272a;
}
.fm-attribution-icon {
.fm-logo {
position: absolute;
bottom: -19px;
bottom: 0;
left: -25px;
pointer-events: none;
overflow: hidden;
img {
margin-bottom: -19px;
}
}
.fm-open-external {

Wyświetl plik

@ -1,18 +1,19 @@
import WithRender from "./leaflet-map.vue";
import Vue from "vue";
import { Component, Inject, InjectReactive, Provide, ProvideReactive } from "vue-property-decorator";
import { Component, InjectReactive, ProvideReactive } from "vue-property-decorator";
import Client from 'facilmap-client';
import "./leaflet-map.scss";
import { ClientContext, InjectClient, InjectClientContext } from "../client/client";
import L, { LatLng, Map } from "leaflet";
import { InjectClient } from "../client/client";
import L, { Control, LatLng, Map } from "leaflet";
import "leaflet/dist/leaflet.css";
import { displayView, getInitialView, getVisibleLayers, HashHandler, setVisibleLayers, VisibleLayers } from "facilmap-leaflet";
import { createSymbolHtml, displayView, getInitialView, getVisibleLayers, HashHandler, VisibleLayers } from "facilmap-leaflet";
import "leaflet.locatecontrol";
import "leaflet.locatecontrol/dist/L.Control.Locate.css";
import "leaflet-graphicscale";
import "leaflet-graphicscale/src/Leaflet.GraphicScale.scss";
import "leaflet-mouse-position";
import "leaflet-mouse-position/src/L.Control.MousePosition.css";
import $ from "jquery";
const MAP_COMPONENTS_KEY = "fm-map-components";
const MAP_CONTEXT_KEY = "fm-map-context";
@ -25,6 +26,24 @@ export function InjectMapContext() {
return InjectReactive(MAP_CONTEXT_KEY);
}
function createButton(symbol: string, onClick: () => void): Control {
return Object.assign(new Control(), {
onAdd() {
const div = document.createElement('div');
div.className = "leaflet-bar";
const a = document.createElement('a');
a.href = "javascript:";
a.innerHTML = createSymbolHtml("currentColor", "1.5em", symbol);
a.addEventListener("click", (e) => {
e.preventDefault();
onClick();
});
div.appendChild(a);
return div;
}
});
}
export interface MapComponents {
map: Map;
hashHandler: HashHandler;
@ -36,6 +55,7 @@ export interface MapContext {
layers: VisibleLayers;
filter: string | undefined;
hash: string;
showToolbox: boolean;
}
@WithRender
@ -45,13 +65,12 @@ export interface MapContext {
export default class LeafletMap extends Vue {
@InjectClient() client!: Client;
@InjectClientContext() clientContext!: ClientContext;
@ProvideReactive(MAP_COMPONENTS_KEY) mapComponents!: MapComponents;
@ProvideReactive(MAP_CONTEXT_KEY) mapContext: MapContext = null as any;
isInFrame = (parent !== window);
loaded = true;
loaded = false;
get selfUrl() {
return `${location.origin}${location.pathname}${this.mapContext.hash ? `#${this.mapContext.hash}` : ''}`;
@ -71,6 +90,8 @@ export default class LeafletMap extends Vue {
iconLoading: "a"
}).addTo(map);
$(locateControl._container).find("a").append(createSymbolHtml("currentColor", "1.5em", "screenshot"));
// $compile($('<fm-icon fm-icon="screenshot" alt="Locate"/>').appendTo($("a", locateControl._container)))($scope);
L.control.mousePosition({
@ -84,27 +105,35 @@ export default class LeafletMap extends Vue {
position: "bottomcenter"
}).addTo(map);
/* createButton("menu-hamburger", () => {
this.mapContext.showToolbox = true;
}).addTo(map); */
this.mapComponents = {
map,
hashHandler
};
if (!map._loaded) {
// Initial view was not set by hash handler
getInitialView(this.client).then((view) => {
displayView(map, view);
}).catch((error) => {
console.error(error);
// TODO
});
}
(async () => {
if (!map._loaded) {
try {
// Initial view was not set by hash handler
displayView(map, await getInitialView(this.client));
} catch (error) {
console.error(error);
displayView(map);
}
}
this.loaded = true;
})();
this.mapContext = {
center: map._loaded ? map.getCenter() : L.latLng(0, 0),
zoom: map._loaded ? map.getZoom() : 1,
layers: getVisibleLayers(map),
filter: map.fmFilter,
hash: location.hash.replace(/^#/, "")
hash: location.hash.replace(/^#/, ""),
showToolbox: false
};
map.on("moveend", () => {

Wyświetl plik

@ -2,15 +2,15 @@
<div class="fm-leaflet-map"></div>
<a v-if="isInFrame" :href="selfUrl" target="_blank" class="fm-open-external" uib-tooltip="Open FacilMap in full size" tooltip-placement="right"></a>
<img src="./logo.png" class="fm-attribution-icon"/>
<div class="fm-leaflet-map-spinner" v-show="clientContext.loading > 0">
<img src="./spinner.gif" alt="Loading…">
</div>
<div class="fm-logo">
<img src="./logo.png"/>
</div>
<b-spinner class="fm-leaflet-map-spinner" v-show="client.loading > 0"></b-spinner>
<slot v-if="mapContext"></slot>
<!-- <div class="fm-leaflet-map-disabled-cover" v-show="client.padId && (client.disconnected || client.serverError || client.deleted)"></div>
<div class="fm-leaflet-map-disabled-cover" v-show="client.padId && (client.disconnected || client.serverError || client.deleted)"></div>
<div class="fm-leaflet-map-loading" v-show="!loaded && !client.serverError">
Loading...
</div> -->
</div>
</div>

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.1 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -7,10 +7,11 @@ import "./main.scss";
import { InjectClient } from "../client/client";
import Toolbox from "../toolbox/toolbox";
import context from "../context";
import SearchBox from "../search-box/search-box";
@WithRender
@Component({
components: { LeafletMap, Toolbox }
components: { LeafletMap, SearchBox, Toolbox }
})
export default class Main extends Vue {

Wyświetl plik

@ -1,7 +1,7 @@
<div class="fm-main">
<leaflet-map>
<toolbox v-if="context.toolbox" :interactive="context.interactive"></toolbox>
<!-- <search v-if="context.search" :autofocus="context.autofocus"></search>
<legend v-if="context.legend"></legend> -->
</leaflet-map>
<LeafletMap>
<Toolbox v-if="context.toolbox" :interactive="context.interactive"></Toolbox>
<SearchBox v-if="context.search" :autofocus="context.autofocus"></SearchBox>
<!--<Legend v-if="context.legend"></Legend> -->
</LeafletMap>
</div>

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -8,8 +8,12 @@ import context from './context';
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import withRender from "./map.vue";
import Vue2TouchEvents from 'vue2-touch-events'
import PortalVue from 'portal-vue'
Vue.use(BootstrapVue);
Vue.use(Vue2TouchEvents);
Vue.use(PortalVue);
/*fm.app.config(function($compileProvider, $uibTooltipProvider) {
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|javascript):/);

Wyświetl plik

@ -0,0 +1,22 @@
.fm-search-box {
position: absolute;
&:not(.isNarrow) {
top: 10px;
left: 52px;
transition: opacity .7s,width .7s,max-width .7s,background-color .7s,border-color .7s;
opacity: .7;
&:hover,&.fm-hasFocus {
opacity: 1;
}
}
&.isNarrow {
top: calc(100% - 70px);
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
}
}

Wyświetl plik

@ -0,0 +1,44 @@
import WithRender from "./search-box.vue";
import Vue from "vue";
import { Component } from "vue-property-decorator";
import "./search-box.scss";
import context from "../context";
import $ from "jquery";
@WithRender
@Component({
components: { }
})
export default class SearchBox extends Vue {
tab: number = 0;
touchStartY: number | null = null;
get isNarrow() {
return context.isNarrow;
}
handleTouchStart(event: TouchEvent) {
if(context.isNarrow && event.touches && event.touches[0] && $(event.target as EventTarget).closest("[draggable=true]").length == 0) {
const top = (this.$el as HTMLElement).offsetTop;
this.touchStartY = event.touches[0].clientY - top;
$(this.$el).css("top", `${top}px`);
}
}
handleTouchMove(event: TouchEvent) {
if(this.touchStartY != null && event.touches[0]) {
const minTop = Math.max(0, ((this.$el as HTMLElement).offsetParent as HTMLElement).offsetHeight - (this.$el as HTMLElement).scrollHeight);
const maxTop = ((this.$el as HTMLElement).offsetParent as HTMLElement).offsetHeight - 70;
const top = Math.max(minTop, Math.min(maxTop, event.touches[0].clientY - this.touchStartY));
$(this.$el).css("top", `${top}px`);
}
}
handleTouchEnd(event: TouchEvent) {
if(this.touchStartY != null && event.changedTouches[0]) {
this.touchStartY = null;
}
}
}

Wyświetl plik

@ -0,0 +1,14 @@
<div class="fm-search-box" :class="{ isNarrow }" v-model="tab" v-touch:start="handleTouchStart" v-touch:moving="handleTouchMove" v-touch:end="handleTouchEnd">
<b-card no-body>
<b-tabs card align="center">
<b-tab title="" title-item-class="d-none"></b-tab>
<b-tab title="Test 1">
<p>Test 1</p>
</b-tab>
<b-tab title="Test 2">
<p>Test 2</p>
</b-tab>
<portal-target name="fm-search-box"></portal-target>
</b-tabs>
</b-card>
</div>

Wyświetl plik

@ -2,23 +2,30 @@
position: absolute;
top: 10px;
right: 10px;
opacity: .5;
transition: opacity .7s;
&:hover {
opacity: 1;
.fm-toolbox-toggle {
color: #444;
border-radius: 4px;
background: #fff;
border: 2px solid rgba(0,0,0,0.2);
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background: #f4f4f4;
}
}
.mobile-menu-button {
margin-left: auto;
}
.fm-sidebar:not(.isNarrow) {
opacity: .5;
transition: opacity .7s;
.navbar {
margin-bottom: 0; /* So that the legend can calculate the height of the toolbox properly */
}
a.disabled {
color: #aaa !important;
&:hover {
opacity: 1;
}
}
@media print {

Wyświetl plik

@ -2,17 +2,20 @@ import Component from "vue-class-component";
import Vue from "vue";
import WithRender from "./toolbox.vue";
import "./toolbox.scss";
import { Prop } from "vue-property-decorator";
import { Prop, Ref } from "vue-property-decorator";
import Client from "facilmap-client";
import { InjectClient } from "../client/client";
import { InjectMapComponents, InjectMapContext, MapComponents, MapContext } from "../leaflet-map/leaflet-map";
import { baseLayers, displayView, overlays, setBaseLayer, toggleOverlay } from "facilmap-leaflet";
import { View } from "facilmap-types";
import About from "../about/about";
import Sidebar from "../ui/sidebar/sidebar";
import Icon from "../ui/icon/icon";
import context from "../context";
@WithRender
@Component({
components: { About }
components: { About, Icon, Sidebar }
})
export default class Toolbox extends Vue {
@ -22,7 +25,11 @@ export default class Toolbox extends Vue {
@Prop({ type: Boolean, default: true }) readonly interactive!: boolean;
hasImportUi = true; // TODO
get isNarrow() {
return context.isNarrow;
}
get links() {
const v = this.mapContext;
return {
@ -121,4 +128,5 @@ export default class Toolbox extends Vue {
showHistory() {
map.historyUi.openHistoryDialog();
} */
}

Wyświetl plik

@ -1,22 +1,23 @@
<div class="fm-toolbox">
<b-navbar variant="light">
<b-navbar-nav>
<b-nav-item v-if="!client.padId && interactive" @click="startPad()">Start collaborative map</b-nav-item>
<a v-if="isNarrow" href="javascript:" class="fm-toolbox-toggle" v-b-toggle.fm-toolbox-sidebar><Icon icon="menu-hamburger"></Icon></a>
<Sidebar id="fm-toolbox-sidebar">
<b-nav-item v-if="!client.padId && interactive" href="javascript:" @click="startPad()">Start collaborative map</b-nav-item>
<b-nav-item-dropdown v-if="!client.readonly && client.padId" text="Add" :disabled="!!client.interaction" right>
<b-dropdown-item v-for="type in client.types" :disabled="!!client.interaction" @click="addObject(type)">{{type.name}}</b-dropdown-item>
<b-dropdown-item v-for="type in client.types" :disabled="!!client.interaction" href="javascript:" @click="addObject(type)">{{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="!!client.interaction" @click="editObjectTypes()">Manage types</b-dropdown-item>
<b-dropdown-item v-if="client.writable == 2" :disabled="!!client.interaction" href="javascript:" @click="editObjectTypes()">Manage types</b-dropdown-item>
</b-nav-item-dropdown>
<b-nav-item-dropdown v-if="client.padId && (!client.readonly || (client.views | fmPropertyCount) != 0)" text="Views" right>
<b-dropdown-item v-for="(id, view) in client.views" @click="displayView(view)">{{view.name}}</b-dropdown-item>
<b-dropdown-item v-for="(id, view) in client.views" href="javascript:" @click="displayView(view)">{{view.name}}</b-dropdown-item>
<b-dropdown-divider v-if="client.writable == 2"></b-dropdown-divider>
<b-dropdown-item v-if="client.writable == 2" @click="saveView()">Save current view</b-dropdown-item>
<b-dropdown-item v-if="client.writable == 2" @click="manageViews()">Manage views</b-dropdown-item>
<b-dropdown-item v-if="client.writable == 2" href="javascript:" @click="saveView()">Save current view</b-dropdown-item>
<b-dropdown-item v-if="client.writable == 2" href="javascript:" @click="manageViews()">Manage views</b-dropdown-item>
</b-nav-item-dropdown>
<b-nav-item-dropdown text="Map style" right>
<b-dropdown-item v-for="layerInfo in baseLayers" :active="layerInfo.active" @click="setBaseLayer(layerInfo.key)">{{layerInfo.name}}</b-dropdown-item>
<b-dropdown-item v-for="layerInfo in baseLayers" :active="layerInfo.active" href="javascript:" @click="setBaseLayer(layerInfo.key)">{{layerInfo.name}}</b-dropdown-item>
<b-dropdown-divider v-if="baseLayers.length > 0 && overlays.length > 0"></b-dropdown-divider>
<b-dropdown-item v-for="layerInfo in overlays" :active="layerInfo.active" @click="toggleOverlay(layerInfo.key)">{{layerInfo.name}}</b-dropdown-item>
<b-dropdown-item v-for="layerInfo in overlays" :active="layerInfo.active" href="javascript:" @click="toggleOverlay(layerInfo.key)">{{layerInfo.name}}</b-dropdown-item>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item :href="links.osm" target="_blank">Open this on OpenStreetMap</b-dropdown-item>
<b-dropdown-item :href="links.google" target="_blank">Open this on Google Maps</b-dropdown-item>
@ -24,22 +25,20 @@
</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="hasImportUi && interactive" @click="importFile()">Open file</b-dropdown-item>
<b-dropdown-item v-if="hasImportUi && interactive" href="javascript:" @click="importFile()">Open file</b-dropdown-item>
<b-dropdown-item v-if="client.padId" :href="`${client.padData.id}/geojson${filterQuery.q}`" title="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.padId" :href="`${client.padData.id}/gpx?useTracks=1${filterQuery.a}`" title="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.padId" :href="`${client.padData.id}/gpx?useTracks=0${filterQuery.a}`" title="GPX files can be opened with most navigation software. In route mode, only the start/end/via points are saved in the file, and the navigation software needs to recalculate the routes.">Export as GPX (routes)</b-dropdown-item>
<b-dropdown-item v-if="client.padId" :href="`${client.padData.id}/table${filterQuery.q}`" target="_blank">Export as table</b-dropdown-item>
<b-dropdown-divider v-if="client.padId"></b-dropdown-divider>
<b-dropdown-item v-if="client.padId" @click="filter()">Filter</b-dropdown-item>
<b-dropdown-item v-if="client.writable == 2 && client.padId" @click="editPadSettings()">Settings</b-dropdown-item>
<b-dropdown-item v-if="!client.readonly && client.padId" @click="showHistory()">Show edit history</b-dropdown-item>
<b-dropdown-item v-if="client.padId" href="javascript:" @click="filter()">Filter</b-dropdown-item>
<b-dropdown-item v-if="client.writable == 2 && client.padId" href="javascript:" @click="editPadSettings()">Settings</b-dropdown-item>
<b-dropdown-item v-if="!client.readonly && client.padId" href="javascript:" @click="showHistory()">Show edit history</b-dropdown-item>
<b-dropdown-divider v-if="client.padId"></b-dropdown-divider>
<b-dropdown-item v-b-modal.fm-toolbox-about>About FacilMap</b-dropdown-item>
<b-dropdown-item v-b-modal.fm-toolbox-about v-b-toggle.fm-toolbox-sidebar href="javascript:">About FacilMap</b-dropdown-item>
<b-dropdown-item v-if="client.padId" :href="links.facilmap">Exit collaborative map</b-dropdown-item>
</b-nav-item-dropdown>
</b-nav>
</b-navbar-nav>
</b-navbar>
</Sidebar>
<About id="fm-toolbox-about"></About>
</div>

Wyświetl plik

@ -0,0 +1,18 @@
import WithRender from "./icon.vue";
import Vue from "vue";
import { Component, Prop } from "vue-property-decorator";
import { createSymbolHtml } from "facilmap-leaflet";
@WithRender
@Component({
components: { }
})
export default class Icon extends Vue {
@Prop({ type: String }) icon!: string;
get iconCode() {
return createSymbolHtml("currentColor", "1.5em", this.icon);
}
}

Wyświetl plik

@ -0,0 +1 @@
<span class="fm-icon" v-html="iconCode"></span>

Wyświetl plik

@ -0,0 +1,55 @@
import WithRender from "./sidebar.vue";
import Vue from "vue";
import { Component, Prop, Ref } from "vue-property-decorator";
import context from "../../context";
import $ from "jquery";
@WithRender
@Component({
components: { }
})
export default class Sidebar extends Vue {
@Prop({ type: String }) readonly id!: string;
touchStartX: number | null = null;
sidebarVisible = false;
get isNarrow() {
return context.isNarrow;
}
handleTouchStart(event: TouchEvent) {
if(event.touches && event.touches[0] && $(event.target as EventTarget).closest("[draggable=true]").length == 0) {
this.touchStartX = event.touches[0].clientX;
$(this.$el).find(".b-sidebar").css("transition", "none");
}
}
handleTouchMove(event: TouchEvent) {
if(this.touchStartX != null && event.touches[0]) {
const right = Math.min(this.touchStartX - event.touches[0].clientX, 0);
$(this.$el).find(".b-sidebar").css("margin-right", `${right}px`);
}
}
handleTouchEnd(event: TouchEvent) {
if(this.touchStartX != null && event.changedTouches[0]) {
const right = Math.min(this.touchStartX - event.changedTouches[0].clientX, 0);
if(right < -($(this.$el).find(".b-sidebar").width() as number / 2)) {
this.sidebarVisible = false;
setTimeout(() => {
$(this.$el).find(".b-sidebar").css("margin-right", "");
}, 0);
} else {
$(this.$el).find(".b-sidebar").css({
"transition": "margin-right 0.4s",
"margin-right": ""
});
}
this.touchStartX = null;
}
}
}

Wyświetl plik

@ -0,0 +1,15 @@
<div class="fm-sidebar" :class="{ isNarrow }">
<b-sidebar shadow backdrop right v-if="isNarrow" :id="id" v-touch:start="handleTouchStart" v-touch:moving="handleTouchMove" v-touch:end="handleTouchEnd" v-model="sidebarVisible">
<b-navbar toggleable="true">
<b-navbar-nav>
<slot></slot>
</b-navbar-nav>
</b-navbar>
</b-sidebar>
<b-navbar variant="light" v-if="!isNarrow">
<b-navbar-nav>
<slot></slot>
</b-navbar-nav>
</b-navbar>
</div>

Wyświetl plik

@ -11,6 +11,10 @@ declare module "leaflet" {
_loaded?: true;
}
interface Control {
_container: HTMLElement;
}
namespace control {
export const graphicScale: any;
}