I should have commited sooner...
This commit is contained in:
parent
2685b6e734
commit
16612b10ef
35 changed files with 570 additions and 177 deletions
|
@ -8,23 +8,33 @@ import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
|||
import {Translation} from "../../UI/i18n/Translation";
|
||||
import {Img} from "../../UI/Img";
|
||||
import Svg from "../../Svg";
|
||||
import {SubstitutedTranslation} from "../../UI/SpecialVisualizations";
|
||||
import {Utils} from "../../Utils";
|
||||
import Combine from "../../UI/Base/Combine";
|
||||
import {Browser} from "leaflet";
|
||||
|
||||
export default class LayerConfig {
|
||||
|
||||
|
||||
id: string;
|
||||
|
||||
name: Translation
|
||||
|
||||
description: Translation;
|
||||
overpassTags: TagsFilter;
|
||||
doNotDownload: boolean;
|
||||
|
||||
passAllFeatures: boolean;
|
||||
|
||||
minzoom: number;
|
||||
|
||||
title: TagRenderingConfig;
|
||||
title?: TagRenderingConfig;
|
||||
|
||||
titleIcons: TagRenderingConfig[];
|
||||
|
||||
icon: TagRenderingConfig;
|
||||
iconSize: TagRenderingConfig;
|
||||
rotation: TagRenderingConfig;
|
||||
color: TagRenderingConfig;
|
||||
width: TagRenderingConfig;
|
||||
dashArray: TagRenderingConfig;
|
||||
|
@ -53,10 +63,11 @@ export default class LayerConfig {
|
|||
this.name = Translations.T(json.name);
|
||||
this.description = Translations.T(json.name);
|
||||
this.overpassTags = FromJSON.Tag(json.overpassTags, context + ".overpasstags");
|
||||
this.doNotDownload = json.doNotDownload ?? false,
|
||||
this.passAllFeatures = json.passAllFeatures ?? false;
|
||||
this.minzoom = json.minzoom;
|
||||
this.wayHandling = json.wayHandling ?? 0;
|
||||
this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
|
||||
this.title = new TagRenderingConfig(json.title);
|
||||
this.presets = (json.presets ?? []).map(pr =>
|
||||
({
|
||||
title: Translations.T(pr.title),
|
||||
|
@ -93,7 +104,10 @@ export default class LayerConfig {
|
|||
|
||||
function tr(key, deflt) {
|
||||
const v = json[key];
|
||||
if (v === undefined) {
|
||||
if (v === undefined || v === null) {
|
||||
if (deflt === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new TagRenderingConfig(deflt);
|
||||
}
|
||||
if (typeof v === "string") {
|
||||
|
@ -107,11 +121,19 @@ export default class LayerConfig {
|
|||
}
|
||||
|
||||
|
||||
this.title = tr("title", "");
|
||||
this.title = tr("title", undefined);
|
||||
this.icon = tr("icon", Img.AsData(Svg.bug));
|
||||
const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt;
|
||||
if (iconPath.startsWith(Utils.assets_path)) {
|
||||
const iconKey = iconPath.substr(Utils.assets_path.length);
|
||||
if (Svg.All[iconKey] === undefined) {
|
||||
throw "Builtin SVG asset not found: " + iconPath
|
||||
}
|
||||
}
|
||||
this.iconSize = tr("iconSize", "40,40,center");
|
||||
this.color = tr("color", "#0000ff");
|
||||
this.width = tr("width", "7");
|
||||
this.rotation = tr("rotation", "0");
|
||||
this.dashArray = tr("dashArray", "");
|
||||
|
||||
|
||||
|
@ -121,13 +143,16 @@ export default class LayerConfig {
|
|||
public GenerateLeafletStyle(tags: any):
|
||||
{
|
||||
color: string;
|
||||
icon: { popupAnchor: [number, number]; iconAnchor: [number, number]; iconSize: [number, number]; iconUrl: string }; weight: number; dashArray: number[]
|
||||
icon: {
|
||||
iconUrl: string,
|
||||
popupAnchor: [number, number];
|
||||
iconAnchor: [number, number];
|
||||
iconSize: [number, number];
|
||||
html: string;
|
||||
rotation: number;
|
||||
};
|
||||
weight: number; dashArray: number[]
|
||||
} {
|
||||
const iconUrl = this.icon?.GetRenderValue(tags)?.txt;
|
||||
const iconSize = (this.iconSize?.GetRenderValue(tags)?.txt ?? "40,40,center").split(",");
|
||||
|
||||
|
||||
const dashArray = this.dashArray.GetRenderValue(tags)?.txt.split(" ").map(Number);
|
||||
|
||||
function num(str, deflt = 40) {
|
||||
const n = Number(str);
|
||||
|
@ -137,6 +162,33 @@ export default class LayerConfig {
|
|||
return n;
|
||||
}
|
||||
|
||||
function rendernum(tr: TagRenderingConfig, deflt: number) {
|
||||
const str = Number(render(tr, "" + deflt));
|
||||
const n = Number(str);
|
||||
if (isNaN(n)) {
|
||||
return deflt;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function render(tr: TagRenderingConfig, deflt?: string) {
|
||||
const str = (tr?.GetRenderValue(tags)?.txt ?? deflt);
|
||||
return SubstitutedTranslation.SubstituteKeys(str, tags);
|
||||
}
|
||||
|
||||
const iconUrl = render(this.icon);
|
||||
const iconSize = render(this.iconSize, "40,40,center").split(",");
|
||||
const dashArray = render(this.dashArray).split(" ").map(Number);
|
||||
let color = render(this.color, "#00f");
|
||||
|
||||
if (color.startsWith("--")) {
|
||||
color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color")
|
||||
}
|
||||
|
||||
const weight = rendernum(this.width, 5);
|
||||
const rotation = rendernum(this.rotation, 0);
|
||||
|
||||
|
||||
const iconW = num(iconSize[0]);
|
||||
const iconH = num(iconSize[1]);
|
||||
const mode = iconSize[2] ?? "center"
|
||||
|
@ -157,16 +209,22 @@ export default class LayerConfig {
|
|||
anchorH = iconH;
|
||||
}
|
||||
|
||||
|
||||
const color = this.color?.GetRenderValue(tags)?.txt ?? "#00f";
|
||||
let weight = num(this.width?.GetRenderValue(tags)?.txt, 5);
|
||||
let html = `<img src="${iconUrl}" style="width:100%;height:100%;rotate:${rotation}deg;display:block;" />`;
|
||||
if (iconUrl.startsWith(Utils.assets_path)) {
|
||||
const key = iconUrl.substr(Utils.assets_path.length);
|
||||
html = new Combine([
|
||||
(Svg.All[key] as string).replace(/stop-color:#000000/g, 'stop-color:' + color)
|
||||
]).SetStyle(`width:100%;height:100%;rotate:${rotation}deg;display:block;`).Render();
|
||||
}
|
||||
return {
|
||||
icon:
|
||||
{
|
||||
iconUrl: iconUrl,
|
||||
html: html,
|
||||
iconSize: [iconW, iconH],
|
||||
iconAnchor: [anchorW, anchorH],
|
||||
popupAnchor: [0, 3 - anchorH]
|
||||
popupAnchor: [0, 3 - anchorH],
|
||||
rotation: rotation,
|
||||
iconUrl: iconUrl
|
||||
},
|
||||
color: color,
|
||||
weight: weight,
|
||||
|
|
|
@ -29,6 +29,12 @@ export interface LayerConfigJson {
|
|||
*/
|
||||
overpassTags: AndOrTagConfigJson | string;
|
||||
|
||||
/**
|
||||
* If set, this layer will not query overpass; but it'll still match the tags above which are by chance returned by other layers.
|
||||
* Works well together with 'passAllFeatures', to add decoration
|
||||
*/
|
||||
doNotDownload?: boolean;
|
||||
|
||||
/**
|
||||
* The zoomlevel at which point the data is shown and loaded.
|
||||
*/
|
||||
|
@ -39,8 +45,13 @@ export interface LayerConfigJson {
|
|||
/**
|
||||
* The title shown in a popup for elements of this layer.
|
||||
*/
|
||||
title: string | TagRenderingConfigJson;
|
||||
title?: string | TagRenderingConfigJson;
|
||||
|
||||
/**
|
||||
* Small icons shown next to the title.
|
||||
* If not specified, the OsmLink and wikipedia links will be used by default.
|
||||
* Use an empty array to hide them
|
||||
*/
|
||||
titleIcons?: (string | TagRenderingConfigJson)[];
|
||||
|
||||
/**
|
||||
|
@ -54,9 +65,14 @@ export interface LayerConfigJson {
|
|||
* Default is '40,40,center'
|
||||
*/
|
||||
iconSize?: string | TagRenderingConfigJson;
|
||||
/**
|
||||
* The rotation of an icon, useful for e.g. directions
|
||||
*/
|
||||
rotation?: string | TagRenderingConfigJson;
|
||||
|
||||
/**
|
||||
* The color for way-elements
|
||||
* The color for way-elements and SVG-elements.
|
||||
* If the value starts with "--", the style of the body element will be queried for the corresponding variable instead
|
||||
*/
|
||||
color?: string | TagRenderingConfigJson;
|
||||
/**
|
||||
|
@ -87,6 +103,11 @@ export interface LayerConfigJson {
|
|||
*/
|
||||
hideUnderlayingFeaturesMinPercentage?:number;
|
||||
|
||||
/**
|
||||
* If set, this layer will pass all the features it receives onto the next layer
|
||||
*/
|
||||
passAllFeatures?:boolean
|
||||
|
||||
/**
|
||||
* Presets for this layer
|
||||
*/
|
||||
|
@ -98,6 +119,7 @@ export interface LayerConfigJson {
|
|||
|
||||
/**
|
||||
* All the tag renderings.
|
||||
* A tag rendering is a block that either shows the known value or asks a question.
|
||||
*/
|
||||
tagRenderings?: (string | TagRenderingConfigJson) []
|
||||
}
|
|
@ -102,7 +102,19 @@ export interface LayoutConfigJson {
|
|||
|
||||
|
||||
/**
|
||||
* The layers to display
|
||||
* The layers to display.
|
||||
*
|
||||
* Every layer contains a description of which feature to display - the overpassTags which are queried.
|
||||
* Instead of running one query for every layer, the query is fused.
|
||||
*
|
||||
* Afterwards, every layer is given the list of features.
|
||||
* Every layer takes away the features that match with them*, and give the leftovers to the next layers.
|
||||
*
|
||||
* This implies that the _order_ of the layers is important in the case of features with the same tags;
|
||||
* as the later layers might never receive their feature.
|
||||
*
|
||||
* *layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself
|
||||
*
|
||||
*/
|
||||
layers: (LayerConfigJson | string)[],
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import * as bike_shops from "../assets/layers/bike_shop/bike_shop.json"
|
|||
import * as bike_cleaning from "../assets/layers/bike_cleaning/bike_cleaning.json"
|
||||
import * as maps from "../assets/layers/maps/maps.json"
|
||||
import * as information_boards from "../assets/layers/information_board/information_board.json"
|
||||
import * as direction from "../assets/layers/direction/direction.json"
|
||||
import LayerConfig from "./JSON/LayerConfig";
|
||||
|
||||
export default class SharedLayers {
|
||||
|
@ -37,6 +38,7 @@ export default class SharedLayers {
|
|||
new LayerConfig(bike_shops, "shared_layers"),
|
||||
new LayerConfig(bike_cleaning, "shared_layers"),
|
||||
new LayerConfig(maps, "shared_layers"),
|
||||
new LayerConfig(direction, "shared_layers"),
|
||||
new LayerConfig(information_boards, "shared_layers")
|
||||
];
|
||||
|
||||
|
|
|
@ -35,6 +35,9 @@ import Svg from "./Svg";
|
|||
import Link from "./UI/Base/Link";
|
||||
import * as personal from "./assets/themes/personalLayout/personalLayout.json"
|
||||
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
|
||||
import * as L from "leaflet";
|
||||
import {Img} from "./UI/Img";
|
||||
import {UserDetails} from "./Logic/Osm/OsmConnection";
|
||||
|
||||
export class InitUiElements {
|
||||
|
||||
|
@ -142,6 +145,7 @@ export class InitUiElements {
|
|||
|
||||
}
|
||||
|
||||
|
||||
if (layoutToUse.id === personal.id) {
|
||||
State.state.favouriteLayers.addCallback(updateFavs);
|
||||
State.state.installedThemes.addCallback(updateFavs);
|
||||
|
@ -153,6 +157,10 @@ export class InitUiElements {
|
|||
* This is given to the div which renders fullscreen on mobile devices
|
||||
*/
|
||||
State.state.selectedElement.addCallback((feature) => {
|
||||
|
||||
if (feature === undefined) {
|
||||
State.state.fullScreenMessage.setData(undefined);
|
||||
}
|
||||
if (feature?.properties === undefined) {
|
||||
return;
|
||||
}
|
||||
|
@ -163,9 +171,15 @@ export class InitUiElements {
|
|||
continue;
|
||||
}
|
||||
const applicable = layer.overpassTags.matches(TagUtils.proprtiesToKV(data));
|
||||
if (applicable) {
|
||||
// This layer is the layer that gives the questions
|
||||
if (!applicable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(layer.title === null && layer.tagRenderings.length === 0){
|
||||
continue;
|
||||
}
|
||||
|
||||
// This layer is the layer that gives the questions
|
||||
const featureBox = new FeatureInfoBox(
|
||||
State.state.allElements.getElement(data.id),
|
||||
layer
|
||||
|
@ -175,7 +189,6 @@ export class InitUiElements {
|
|||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => {
|
||||
|
@ -204,6 +217,21 @@ export class InitUiElements {
|
|||
content.AttachTo("messagesbox");
|
||||
}
|
||||
|
||||
State.state.osmConnection.userDetails.map((userDetails: UserDetails) => userDetails?.home)
|
||||
.addCallbackAndRun(home => {
|
||||
if (home === undefined) {
|
||||
return;
|
||||
}
|
||||
const color = getComputedStyle(document.body).getPropertyValue("--subtle-detail-color")
|
||||
const icon = L.icon({
|
||||
iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)),
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 15]
|
||||
});
|
||||
const marker = L.marker([home.lat, home.lon], {icon: icon})
|
||||
marker.addTo(State.state.bm.map)
|
||||
console.log(marker)
|
||||
});
|
||||
|
||||
new GeoLocationHandler()
|
||||
.SetStyle(`position:relative;display:block;border: solid 2px #0005;cursor: pointer; z-index: 999; /*Just below leaflets zoom*/background-color: white;border-radius: 5px;width: 43px;height: 43px;`)
|
||||
|
@ -327,6 +355,10 @@ export class InitUiElements {
|
|||
checkbox.isEnabled.setData(false);
|
||||
})
|
||||
|
||||
State.state.selectedElement.addCallback(() => {
|
||||
checkbox.isEnabled.setData(false);
|
||||
})
|
||||
|
||||
|
||||
const fullOptions2 = this.CreateWelcomePane();
|
||||
State.state.fullScreenMessage.setData(fullOptions2)
|
||||
|
@ -435,13 +467,15 @@ export class InitUiElements {
|
|||
return new Combine([mapComplete, reportBug, " | ", stats, " | ", editHere, editWithJosm]).Render();
|
||||
|
||||
}, [State.state.osmConnection.userDetails])
|
||||
|
||||
).SetClass("map-attribution")
|
||||
}
|
||||
|
||||
static InitBaseMap() {
|
||||
const bm = new Basemap("leafletDiv", State.state.locationControl, this.CreateAttribution());
|
||||
State.state.bm = bm;
|
||||
bm.map.on("popupclose", () => {
|
||||
State.state.selectedElement.setData(undefined)
|
||||
})
|
||||
State.state.layerUpdater = new UpdateFromOverpass(State.state);
|
||||
|
||||
State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state).availableEditorLayers;
|
||||
|
@ -475,13 +509,12 @@ export class InitUiElements {
|
|||
throw "Layer " + layer + " was not substituted";
|
||||
}
|
||||
|
||||
const flayer: FilteredLayer = new FilteredLayer(layer,
|
||||
(tagsES) => {
|
||||
return new FeatureInfoBox(
|
||||
tagsES,
|
||||
layer,
|
||||
)
|
||||
});
|
||||
let generateContents = (tags: UIEventSource<any>) => new FeatureInfoBox(tags, layer);
|
||||
if (layer.title === undefined && (layer.tagRenderings ?? []).length === 0) {
|
||||
generateContents = undefined;
|
||||
}
|
||||
|
||||
const flayer: FilteredLayer = new FilteredLayer(layer, generateContents);
|
||||
flayers.push(flayer);
|
||||
|
||||
QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wehter or not layer " + layer.id + " is shown")
|
||||
|
|
|
@ -6,6 +6,7 @@ import {GeoOperations} from "./GeoOperations";
|
|||
import {UIElement} from "../UI/UIElement";
|
||||
import State from "../State";
|
||||
import LayerConfig from "../Customizations/JSON/LayerConfig";
|
||||
import Hash from "./Web/Hash";
|
||||
|
||||
/***
|
||||
* A filtered layer is a layer which offers a 'set-data' function
|
||||
|
@ -75,12 +76,14 @@ export class FilteredLayer {
|
|||
const selfFeatures = [];
|
||||
for (let feature of geojson.features) {
|
||||
const tags = TagUtils.proprtiesToKV(feature.properties);
|
||||
if (!this.filters.matches(tags)) {
|
||||
leftoverFeatures.push(feature);
|
||||
continue;
|
||||
}
|
||||
const matches = this.filters.matches(tags);
|
||||
if (matches) {
|
||||
selfFeatures.push(feature);
|
||||
}
|
||||
if (!matches || this.layerDef.passAllFeatures) {
|
||||
leftoverFeatures.push(feature);
|
||||
}
|
||||
}
|
||||
|
||||
this.RenderLayer(selfFeatures)
|
||||
|
||||
|
@ -117,7 +120,6 @@ export class FilteredLayer {
|
|||
|
||||
// We fetch all the data we have to show:
|
||||
let fusedFeatures = this.ApplyWayHandling(this.FuseData(features));
|
||||
console.log("Fused:",fusedFeatures)
|
||||
|
||||
// And we copy some features as points - if needed
|
||||
const data = {
|
||||
|
@ -126,7 +128,6 @@ export class FilteredLayer {
|
|||
}
|
||||
|
||||
let self = this;
|
||||
console.log(data);
|
||||
this._geolayer = L.geoJSON(data, {
|
||||
style: feature =>
|
||||
self.layerDef.GenerateLeafletStyle(feature.properties),
|
||||
|
@ -147,19 +148,21 @@ export class FilteredLayer {
|
|||
color: style.color
|
||||
});
|
||||
} else {
|
||||
if (style.icon.iconSize === undefined) {
|
||||
style.icon.iconSize = [50, 50]
|
||||
}
|
||||
|
||||
marker = L.marker(latLng, {
|
||||
icon: L.icon(style.icon)
|
||||
icon: L.divIcon(style.icon)
|
||||
});
|
||||
}
|
||||
return marker;
|
||||
},
|
||||
onEachFeature: function (feature, layer: Layer) {
|
||||
|
||||
layer.on("click", (e) => {
|
||||
if (self._showOnPopup === undefined) {
|
||||
// No popup contents defined -> don't do anything
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
function openPopup(latlng: any) {
|
||||
if (layer.getPopup() === undefined
|
||||
&& (window.screen.availHeight > 600 || window.screen.availWidth > 600) // We DON'T trigger this code on small screens! No need to create a popup
|
||||
) {
|
||||
|
@ -168,8 +171,7 @@ export class FilteredLayer {
|
|||
closeOnEscapeKey: true,
|
||||
}, layer);
|
||||
|
||||
// @ts-ignore
|
||||
popup.setLatLng(e.latlng)
|
||||
popup.setLatLng(latlng)
|
||||
|
||||
layer.bindPopup(popup);
|
||||
const eventSource = State.state.allElements.addOrGetElement(feature);
|
||||
|
@ -181,17 +183,35 @@ export class FilteredLayer {
|
|||
// popup.openOn(State.state.bm.map);
|
||||
// ANd we perform the pending update
|
||||
uiElement.Update();
|
||||
// @ts-ignore
|
||||
popup.Update = () => {
|
||||
uiElement.Update();
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore
|
||||
layer.getPopup().Update();
|
||||
}
|
||||
|
||||
|
||||
// We set the element as selected...
|
||||
State.state.selectedElement.setData(feature);
|
||||
|
||||
}
|
||||
|
||||
layer.on("click", (e) => {
|
||||
// @ts-ignore
|
||||
openPopup(e.latlng);
|
||||
// We mark the event as consumed
|
||||
L.DomEvent.stop(e);
|
||||
});
|
||||
|
||||
if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) {
|
||||
const center = GeoOperations.centerpoint(feature).geometry.coordinates;
|
||||
openPopup({lat: center[1], lng: center[0]})
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
;
|
||||
});
|
||||
|
||||
if (this.combinedIsDisplayed.data) {
|
||||
this._geolayer.addTo(State.state.bm.map);
|
||||
|
|
|
@ -4,7 +4,6 @@ import {UIElement} from "../../UI/UIElement";
|
|||
import State from "../../State";
|
||||
import {Utils} from "../../Utils";
|
||||
import {Basemap} from "./Basemap";
|
||||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||
import Svg from "../../Svg";
|
||||
import {Img} from "../../UI/Img";
|
||||
|
||||
|
@ -48,15 +47,18 @@ export class GeoLocationHandler extends UIElement {
|
|||
map.on('accuratepositionfound', onAccuratePositionFound);
|
||||
map.on('accuratepositionerror', onAccuratePositionError);
|
||||
|
||||
FixedUiElement
|
||||
|
||||
|
||||
State.state.currentGPSLocation.addCallback((location) => {
|
||||
|
||||
const color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color")
|
||||
const icon = L.icon(
|
||||
{
|
||||
iconUrl: Img.AsData(Svg.crosshair_blue),
|
||||
iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)),
|
||||
iconSize: [40, 40], // size of the icon
|
||||
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
|
||||
})
|
||||
|
||||
State.state.currentGPSLocation.addCallback((location) => {
|
||||
const newMarker = L.marker(location.latlng, {icon: icon});
|
||||
newMarker.addTo(map);
|
||||
|
||||
|
|
|
@ -62,6 +62,11 @@ export class UpdateFromOverpass {
|
|||
if (state.locationControl.data.zoom < layer.minzoom) {
|
||||
continue;
|
||||
}
|
||||
if(layer.doNotDownload){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Check if data for this layer has already been loaded
|
||||
let previouslyLoaded = false;
|
||||
for (let z = layer.minzoom; z < 25 && !previouslyLoaded; z++) {
|
||||
|
|
18
Logic/Web/Hash.ts
Normal file
18
Logic/Web/Hash.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
|
||||
export default class Hash {
|
||||
|
||||
public static Get() : UIEventSource<string>{
|
||||
const hash = new UIEventSource<string>(window.location.hash.substr(1));
|
||||
hash.addCallback(h => {
|
||||
h = h.replace(/\//g, "_");
|
||||
return window.location.hash = "#" + h;
|
||||
});
|
||||
window.onhashchange = () => {
|
||||
hash.setData(window.location.hash.substr(1))
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
* Wraps the query parameters into UIEventSources
|
||||
*/
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import Hash from "./Hash";
|
||||
|
||||
export class QueryParameters {
|
||||
|
||||
|
@ -57,7 +58,7 @@ export class QueryParameters {
|
|||
|
||||
parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data))
|
||||
}
|
||||
history.replaceState(null, "", "?" + parts.join("&"));
|
||||
history.replaceState(null, "", "?" + parts.join("&") + "#" + Hash.Get().data);
|
||||
|
||||
}
|
||||
|
||||
|
|
19
State.ts
19
State.ts
|
@ -12,6 +12,7 @@ import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
|
|||
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
||||
import {BaseLayer} from "./Logic/BaseLayer";
|
||||
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
|
||||
import Hash from "./Logic/Web/Hash";
|
||||
|
||||
/**
|
||||
* Contains the global state: a bunch of UI-event sources
|
||||
|
@ -22,7 +23,7 @@ export default class State {
|
|||
// The singleton of the global state
|
||||
public static state: State;
|
||||
|
||||
public static vNumber = "0.1.3-rc2+g";
|
||||
public static vNumber = "0.1.3-rc4";
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
|
@ -209,6 +210,22 @@ export default class State {
|
|||
);
|
||||
|
||||
|
||||
const h = Hash.Get();
|
||||
this.selectedElement.addCallback(selected => {
|
||||
if (selected === undefined) {
|
||||
h.setData("");
|
||||
} else {
|
||||
h.setData(selected.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
h.addCallbackAndRun(hash => {
|
||||
if(hash === undefined || hash === ""){
|
||||
self.selectedElement.setData(undefined);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
this.installedThemes = this.osmConnection.preferencesHandler.preferences.map<{ layout: LayoutConfig, definition: string }[]>(allPreferences => {
|
||||
const installedThemes: { layout: LayoutConfig, definition: string }[] = [];
|
||||
if (allPreferences === undefined) {
|
||||
|
|
7
Svg.ts
7
Svg.ts
|
@ -79,6 +79,11 @@ export default class Svg {
|
|||
public static direction_svg() { return new FixedUiElement(Svg.direction);}
|
||||
public static direction_ui() { return new FixedUiElement(Svg.direction_img);}
|
||||
|
||||
public static direction_gradient = " <svg xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"100\" height=\"100\" viewBox=\"0 0 100 100\" version=\"1.1\" id=\"svg8\"> <metadata id=\"metadata8\"> <rdf:RDF> <cc:Work rdf:about=\"\"> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" /> <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <defs id=\"defs6\"> <linearGradient id=\"linearGradient820\"> <stop id=\"stop816\" offset=\"0\" style=\"stop-color:#000000;stop-opacity:1;\" /> <stop id=\"stop818\" offset=\"1\" style=\"stop-color:#000000;stop-opacity:0\" /> </linearGradient> <radialGradient gradientUnits=\"userSpaceOnUse\" gradientTransform=\"matrix(1.5439431,-0.01852438,0.02075364,1.7297431,-28.198837,-42.329969)\" r=\"28.883806\" fy=\"53.828533\" fx=\"49.787739\" cy=\"53.828533\" cx=\"49.787739\" id=\"radialGradient828\" xlink:href=\"#linearGradient820\" /> </defs> <path style=\"fill:url(#radialGradient828);fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"M 49.787737,49.857275 20.830626,9.2566092 C 35.979158,-2.144159 60.514289,-3.8195259 78.598237,9.0063685 Z\" id=\"path821\" /> </svg> "
|
||||
public static direction_gradient_img = Img.AsImageElement(Svg.direction_gradient)
|
||||
public static direction_gradient_svg() { return new FixedUiElement(Svg.direction_gradient);}
|
||||
public static direction_gradient_ui() { return new FixedUiElement(Svg.direction_gradient_img);}
|
||||
|
||||
public static down = " <svg xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" version=\"1.0\" width=\"700\" height=\"700\" id=\"svg6\" sodipodi:docname=\"down.svg\" inkscape:version=\"0.92.4 (5da689c313, 2019-01-14)\"> <metadata id=\"metadata12\"> <rdf:RDF> <cc:Work rdf:about=\"\"> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" /> </cc:Work> </rdf:RDF> </metadata> <defs id=\"defs10\" /> <sodipodi:namedview pagecolor=\"#ffffff\" bordercolor=\"#666666\" borderopacity=\"1\" objecttolerance=\"10\" gridtolerance=\"10\" guidetolerance=\"10\" inkscape:pageopacity=\"0\" inkscape:pageshadow=\"2\" inkscape:window-width=\"1920\" inkscape:window-height=\"1001\" id=\"namedview8\" showgrid=\"false\" inkscape:zoom=\"0.33714286\" inkscape:cx=\"477.91309\" inkscape:cy=\"350\" inkscape:window-x=\"0\" inkscape:window-y=\"0\" inkscape:window-maximized=\"1\" inkscape:current-layer=\"svg6\" /> <g transform=\"rotate(-180,342.1439,335.17672)\" id=\"g4\"> <path d=\"m -20,670.71582 c 0,-1.85843 349.99229,-699.98853 350.57213,-699.28671 1.94549,2.35478 350.06752,699.46087 349.427,699.71927 -0.41837,0.16878 -79.29725,-33.69092 -175.2864,-75.24377 l -174.52574,-75.55065 -174.2421,75.53732 c -95.83317,41.54551 -174.625237,75.5373 -175.093498,75.5373 -0.46826,0 -0.851382,-0.32075 -0.851382,-0.71276 z\" style=\"fill:#00ff00;stroke:none\" id=\"path2\" inkscape:connector-curvature=\"0\" /> </g> </svg> "
|
||||
public static down_img = Img.AsImageElement(Svg.down)
|
||||
public static down_svg() { return new FixedUiElement(Svg.down);}
|
||||
|
@ -214,4 +219,4 @@ export default class Svg {
|
|||
public static wikipedia_svg() { return new FixedUiElement(Svg.wikipedia);}
|
||||
public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);}
|
||||
|
||||
}
|
||||
public static All = {"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"close.svg": Svg.close,"compass.svg": Svg.compass,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapillary.svg": Svg.mapillary,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"search.svg": Svg.search,"share.svg": Svg.share,"star.svg": Svg.star,"statistics.svg": Svg.statistics,"up.svg": Svg.up,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};}
|
||||
|
|
|
@ -14,7 +14,6 @@ export class FullScreenMessageBox extends UIElement {
|
|||
constructor(onClear: (() => void)) {
|
||||
super(State.state.fullScreenMessage);
|
||||
this.HideOnEmpty(true);
|
||||
const self = this;
|
||||
|
||||
this.returnToTheMap =
|
||||
new Combine([Translations.t.general.returnToTheMap.Clone().SetStyle("font-size:xx-large")])
|
||||
|
|
|
@ -2,6 +2,10 @@ import {InputElement} from "./InputElement";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import Svg from "../../Svg";
|
||||
import * as L from "leaflet"
|
||||
import * as X from "leaflet-providers"
|
||||
import {Basemap} from "../../Logic/Leaflet/Basemap";
|
||||
import State from "../../State";
|
||||
|
||||
/**
|
||||
* Selects a direction in degrees
|
||||
|
@ -34,8 +38,8 @@ export default class DirectionInput extends InputElement<string> {
|
|||
}
|
||||
|
||||
InnerRender(): string {
|
||||
console.log("Inner render direction")
|
||||
return new Combine([
|
||||
`<div id="direction-leaflet-div-${this.id}" style="width:100%;height: 100%;position: absolute;top:0;left:0;border-radius:100%;"></div>`,
|
||||
Svg.direction_svg().SetStyle(
|
||||
`position: absolute;top: 0;left: 0;width: 100%;height: 100%;rotate:${this.value.data}deg;`)
|
||||
.SetClass("direction-svg"),
|
||||
|
@ -47,7 +51,6 @@ export default class DirectionInput extends InputElement<string> {
|
|||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
console.log("Inner update direction")
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self = this;
|
||||
|
||||
|
|
|
@ -16,7 +16,9 @@ interface TextFieldDef {
|
|||
explanation: string,
|
||||
isValid: ((s: string, country?: string) => boolean),
|
||||
reformat?: ((s: string, country?: string) => string),
|
||||
inputHelper?: (value: UIEventSource<string>) => InputElement<string>,
|
||||
inputHelper?: (value: UIEventSource<string>, options?: {
|
||||
location: [number, number]
|
||||
}) => InputElement<string>,
|
||||
}
|
||||
|
||||
export default class ValidatedTextField {
|
||||
|
@ -26,7 +28,9 @@ export default class ValidatedTextField {
|
|||
explanation: string,
|
||||
isValid?: ((s: string, country?: string) => boolean),
|
||||
reformat?: ((s: string, country?: string) => string),
|
||||
inputHelper?: (value: UIEventSource<string>) => InputElement<string>): TextFieldDef {
|
||||
inputHelper?: (value: UIEventSource<string>, options?:{
|
||||
location: [number, number]
|
||||
}) => InputElement<string>): TextFieldDef {
|
||||
|
||||
if (isValid === undefined) {
|
||||
isValid = () => true;
|
||||
|
@ -197,7 +201,8 @@ export default class ValidatedTextField {
|
|||
textArea?: boolean,
|
||||
textAreaRows?: number,
|
||||
isValid?: ((s: string, country: string) => boolean),
|
||||
country?: string
|
||||
country?: string,
|
||||
location?: [number /*lat*/, number /*lon*/]
|
||||
}): InputElement<string> {
|
||||
options = options ?? {};
|
||||
options.placeholder = options.placeholder ?? type;
|
||||
|
@ -230,7 +235,9 @@ export default class ValidatedTextField {
|
|||
}
|
||||
|
||||
if (tp.inputHelper) {
|
||||
input = new CombinedInputElement(input, tp.inputHelper(input.GetValue()));
|
||||
input = new CombinedInputElement(input, tp.inputHelper(input.GetValue(),{
|
||||
location: options.location
|
||||
}));
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,8 @@ export class FeatureInfoBox extends UIElement {
|
|||
this._layerConfig = layerConfig;
|
||||
|
||||
|
||||
this._title = new TagRenderingAnswer(tags, layerConfig.title)
|
||||
this._title = layerConfig.title === undefined ? undefined :
|
||||
new TagRenderingAnswer(tags, layerConfig.title)
|
||||
.SetClass("featureinfobox-title");
|
||||
this._titleIcons = new Combine(
|
||||
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon)))
|
||||
|
|
|
@ -15,6 +15,9 @@ export default class TagRenderingAnswer extends UIElement {
|
|||
super(tags);
|
||||
this._tags = tags;
|
||||
this._configuration = configuration;
|
||||
if(configuration === undefined){
|
||||
throw "Trying to generate a tagRenderingAnswer without configuration..."
|
||||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
|
|
@ -251,7 +251,8 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
|
||||
const textField = ValidatedTextField.InputForType(this._configuration.freeform.type, {
|
||||
isValid: (str) => (str.length <= 255),
|
||||
country: this._tags.data._country
|
||||
country: this._tags.data._country,
|
||||
location: [this._tags.data._lat, this._tags.data._lon]
|
||||
});
|
||||
|
||||
textField.GetValue().setData(this._tags.data[this._configuration.freeform.key]);
|
||||
|
|
|
@ -39,12 +39,16 @@ export class SubstitutedTranslation extends UIElement {
|
|||
return []
|
||||
}
|
||||
const tags = this.tags.data;
|
||||
txt = SubstitutedTranslation.SubstituteKeys(txt, tags);
|
||||
return this.EvaluateSpecialComponents(txt);
|
||||
}
|
||||
|
||||
public static SubstituteKeys(txt: string, tags: any) {
|
||||
for (const key in tags) {
|
||||
// Poor mans replace all
|
||||
txt = txt.split("{" + key + "}").join(tags[key]);
|
||||
}
|
||||
|
||||
return this.EvaluateSpecialComponents(txt);
|
||||
return txt;
|
||||
}
|
||||
|
||||
private EvaluateSpecialComponents(template: string): UIElement[] {
|
||||
|
|
|
@ -94,15 +94,6 @@ export class UserBadge extends UIElement {
|
|||
dryrun = new FixedUiElement("TESTING").SetClass("alert");
|
||||
}
|
||||
|
||||
if (user.home !== undefined) {
|
||||
const icon = L.icon({
|
||||
iconUrl: Img.AsData(Svg.home_white_bg),
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 15]
|
||||
});
|
||||
L.marker([user.home.lat, user.home.lon], {icon: icon}).addTo(State.state.bm.map)
|
||||
}
|
||||
|
||||
const settings =
|
||||
new Link(Svg.gear_svg(),
|
||||
`https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`,
|
||||
|
|
3
Utils.ts
3
Utils.ts
|
@ -3,6 +3,7 @@ import * as $ from "jquery"
|
|||
|
||||
export class Utils {
|
||||
|
||||
public static readonly assets_path = "./assets/svg/";
|
||||
|
||||
static EncodeXmlValue(str) {
|
||||
return str.replace(/&/g, '&')
|
||||
|
@ -167,4 +168,6 @@ export class Utils {
|
|||
console.log("Added custom layout ",location)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
32
assets/layers/direction/direction.json
Normal file
32
assets/layers/direction/direction.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"id": "direction",
|
||||
"name": {
|
||||
"en": "Direction visualization"
|
||||
},
|
||||
"minzoom": 16,
|
||||
"overpassTags": {
|
||||
"or": ["camera:direction~*","direction~*"]
|
||||
},
|
||||
"doNotDownload": true,
|
||||
"passAllFeatures": true,
|
||||
"title": null,
|
||||
"description": {
|
||||
"en": "This layer visualizes directions"
|
||||
},
|
||||
"tagRenderings": [],
|
||||
"icon": "./assets/svg/direction_gradient.svg",
|
||||
"rotation": {
|
||||
"render": "{camera:direction}",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "direction~*",
|
||||
"then": "{direction}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"iconSize": "200,200,center",
|
||||
"color": "--catch-detail-color",
|
||||
"stroke": "0",
|
||||
"presets": [],
|
||||
"wayHandling": 2
|
||||
}
|
54
assets/svg/direction_gradient.svg
Normal file
54
assets/svg/direction_gradient.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0 0 100 100"
|
||||
version="1.1"
|
||||
id="svg8">
|
||||
<metadata
|
||||
id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs6">
|
||||
<linearGradient
|
||||
id="linearGradient820">
|
||||
<stop
|
||||
id="innercolor"
|
||||
offset="0"
|
||||
style="stop-color:#000000;stop-opacity:1;" />
|
||||
<stop
|
||||
id="outercolor"
|
||||
offset="1"
|
||||
style="stop-color:#000000;stop-opacity:0" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.5439431,-0.01852438,0.02075364,1.7297431,-28.198837,-42.329969)"
|
||||
r="28.883806"
|
||||
fy="53.828533"
|
||||
fx="49.787739"
|
||||
cy="53.828533"
|
||||
cx="49.787739"
|
||||
id="radialGradient828"
|
||||
xlink:href="#linearGradient820" />
|
||||
</defs>
|
||||
<path
|
||||
style="fill:url(#radialGradient828);fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 49.787737,49.857275 20.830626,9.2566092 C 35.979158,-2.144159 60.514289,-3.8195259 78.598237,9.0063685 Z"
|
||||
id="path821" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -10,3 +10,17 @@ html {
|
|||
--shadow-color: #0f0 !important;
|
||||
}
|
||||
|
||||
#innercolor {
|
||||
stop-color:#ff0000
|
||||
}
|
||||
.leaflet-div-icon svg {
|
||||
width: calc(100% - 3px);
|
||||
height: calc(100% + 3px);
|
||||
}
|
||||
/*
|
||||
.leaflet-div-icon svg path {
|
||||
fill: none !important;
|
||||
stroke-width: 1px !important;
|
||||
stroke: #0f0 !important;
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"customCss": "./assets/themes/surveillance_cameras/custom_theme.css",
|
||||
"defaultBackgroundId": "Stadia.AlidadeSmoothDark",
|
||||
"layers": [
|
||||
"direction",
|
||||
{
|
||||
"id": "cameras",
|
||||
"name": {
|
||||
|
@ -56,6 +57,7 @@
|
|||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"#": "Camera type: fixed; panning; dome",
|
||||
"question": {
|
||||
"en": "What kind of camera is this?",
|
||||
"nl": "Wat voor soort camera is dit?"
|
||||
|
@ -97,18 +99,32 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"#": "direction. We don't ask this for a dome on a pole or ceiling as it has a 360° view",
|
||||
"question": {
|
||||
"en": "In which geographical direction does this camera film?",
|
||||
"nl": "Naar welke geografische richting filmt deze camera?"
|
||||
},
|
||||
"render": "Films to {camera:direction}",
|
||||
"condition": "camera:type!=dome",
|
||||
"condition": {
|
||||
"not": {
|
||||
"and": [
|
||||
"camera:type=dome",
|
||||
{
|
||||
"or": [
|
||||
"camera:mount=ceiling",
|
||||
"camera:mount=pole"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"freeform": {
|
||||
"key": "camera:direction",
|
||||
"type": "direction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"#": "Operator",
|
||||
"freeform": {
|
||||
"key": "operator"
|
||||
},
|
||||
|
@ -122,6 +138,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"#": "Surveillance type: public, outdoor, indoor",
|
||||
"question": {
|
||||
"en": "What kind of surveillance is this camera",
|
||||
"nl": "Wat soort bewaking wordt hier uitgevoerd?"
|
||||
|
@ -134,8 +151,8 @@
|
|||
]
|
||||
},
|
||||
"then": {
|
||||
"en": "A public area is surveilled, such as a street, a bridge, a square, a park, a train station...",
|
||||
"nl": "Bewaking van de publieke ruilmte, dus een straat, een brug, een park, een plein, een stationsgebouw..."
|
||||
"en": "A public area is surveilled, such as a street, a bridge, a square, a park, a train station, a public corridor or tunnel,...",
|
||||
"nl": "Bewaking van de publieke ruilmte, dus een straat, een brug, een park, een plein, een stationsgebouw, een publiek toegankelijke gang of tunnel..."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -156,13 +173,67 @@
|
|||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Een private binnenruimte wordt bewaakt, bv. een wiinkel, een parkeergarage, ...",
|
||||
"nl": "Een private binnenruimte wordt bewaakt, bv. een winkel, een parkeergarage, ...",
|
||||
"en": "A private indoor area is surveilled, e.g. a shop, a private underground parking, ..."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"#": "Indoor camera? This isn't clear for 'public'-cameras",
|
||||
"question": {
|
||||
"en": "Is the public space surveilled by this camera an indoor or outdoor space?",
|
||||
"nl": "Bevindt de bewaakte publieke ruimte camera zich binnen of buiten?"
|
||||
},
|
||||
"condition": {
|
||||
"and": [
|
||||
"surveillance:type=public"
|
||||
]
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "indoor=yes",
|
||||
"then": {
|
||||
"en": "This camera is located indoors",
|
||||
"nl": "Deze camera bevindt zich binnen"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "indoor=no",
|
||||
"then": {
|
||||
"en": "This camera is located outdoors",
|
||||
"nl": "Deze camera bevindt zich buiten"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "indoor=",
|
||||
"then": {
|
||||
"en": "This camera is probably located outdoors",
|
||||
"nl": "Deze camera bevindt zich waarschijnlijk buiten"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"#": "Level",
|
||||
"question": {
|
||||
"en": "On which level is this camera located?",
|
||||
"nl": "Op welke verdieping bevindt deze camera zich?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "level",
|
||||
"type": "nat"
|
||||
},
|
||||
"condition": {
|
||||
"or": [
|
||||
"indoor=yes",
|
||||
"surveillance:type=ye"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"#": "Surveillance:zone",
|
||||
"question": {
|
||||
"en": "What exactly is surveilled here?",
|
||||
"nl": "Wat wordt hier precies bewaakt?"
|
||||
|
@ -244,6 +315,7 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"#": "camera:mount",
|
||||
"question": {
|
||||
"en": "How is this camera placed?",
|
||||
"nl": "Hoe is deze camera geplaatst?"
|
||||
|
@ -267,10 +339,10 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": "camera:mount=pole",
|
||||
"if": "camera:mount=ceiling",
|
||||
"then": {
|
||||
"en": "This camera is placed one a pole",
|
||||
"nl": "Deze camera staat op een paal"
|
||||
"en": "This camera is placed on the ceiling",
|
||||
"nl": "Deze camera hangt aan het plafond"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -293,7 +365,7 @@
|
|||
"render": "50,50,center"
|
||||
},
|
||||
"color": {
|
||||
"render": "#00f"
|
||||
"render": "#f00"
|
||||
},
|
||||
"presets": [
|
||||
{
|
||||
|
|
|
@ -45,10 +45,6 @@ Contains tweaks for small screens
|
|||
}
|
||||
|
||||
.leaflet-popup {
|
||||
transform: unset !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
/* On mobile, the popups are shown as a full-screen element */
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
|
|
|
@ -38,6 +38,11 @@
|
|||
|
||||
}
|
||||
|
||||
.question svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
font-size: larger;
|
||||
font-weight: bold;
|
||||
|
|
|
@ -255,7 +255,6 @@ a {
|
|||
position: absolute;
|
||||
z-index: 5000;
|
||||
transition: all 500ms linear;
|
||||
overflow-x: hidden;
|
||||
pointer-events: none;
|
||||
/* Shadow offset */
|
||||
padding: 0.5em 10px 0 0.5em;
|
||||
|
@ -410,6 +409,12 @@ a {
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.leaflet-div-icon {
|
||||
background-color: unset !important;
|
||||
border: unset !important;
|
||||
}
|
||||
|
||||
/****** ShareScreen *****/
|
||||
|
||||
.literal-code {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
npm install
|
|
@ -16,10 +16,11 @@
|
|||
"start": "parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*",
|
||||
"test": "ts-node test/*",
|
||||
"generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json",
|
||||
"generate:images": "ts-node generateIncludedImages.ts",
|
||||
"generate:layouts": "ts-node createLayouts.ts",
|
||||
"generate:images": "ts-node scripts/generateIncludedImages.ts",
|
||||
"generate:translations": "ts-node scripts/generateTranslations.ts",
|
||||
"generate:layouts": "ts-node scripts/createLayouts.ts",
|
||||
"optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'",
|
||||
"generate": "npm run generate:images && npm run generate:layouts && npm run generate:editor-layer-index",
|
||||
"generate": "npm run generate:images && npm run generate:translations && npm run generate:layouts && npm run generate:editor-layer-index",
|
||||
"build": "rm -rf dist/ npm run generate && parcel build --public-url ./ *.html assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*",
|
||||
"prepare-deploy": "npm run generate && npm run build && rm -rf .cache",
|
||||
"deploy:staging": "npm run prepare-deploy && rm -rf /home/pietervdvn/git/pietervdvn.github.io/Staging/* && cp -r dist/* /home/pietervdvn/git/pietervdvn.github.io/Staging/ && cd /home/pietervdvn/git/pietervdvn.github.io/ && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean",
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import {Img} from "./UI/Img"
|
||||
import {UIElement} from "./UI/UIElement";
|
||||
import {Img} from "../UI/Img"
|
||||
import {UIElement} from "../UI/UIElement";
|
||||
Img.runningFromConsole = true;
|
||||
// We HAVE to mark this while importing
|
||||
UIElement.runningFromConsole = true;
|
||||
|
||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
|
||||
import {existsSync, mkdirSync, readFileSync, writeFile, writeFileSync} from "fs";
|
||||
import Locale from "./UI/i18n/Locale";
|
||||
import Locale from "../UI/i18n/Locale";
|
||||
import svg2img from 'promise-svg2img';
|
||||
import Translations from "./UI/i18n/Translations";
|
||||
import {Translation} from "./UI/i18n/Translation";
|
||||
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
|
||||
import Translations from "../UI/i18n/Translations";
|
||||
import {Translation} from "../UI/i18n/Translation";
|
||||
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
|
||||
|
||||
|
||||
function enc(str: string): string {
|
||||
|
@ -100,7 +100,7 @@ const alreadyWritten = []
|
|||
|
||||
function createIcon(iconPath: string, size: number, layout: LayoutConfig) {
|
||||
let name = iconPath.split(".").slice(0, -1).join(".");
|
||||
if (name.startsWith("./")) {
|
||||
if (name.startsWith("../")) {
|
||||
name = name.substr(2)
|
||||
}
|
||||
const newname = `${name}${size}.png`
|
||||
|
@ -151,7 +151,7 @@ function createManifest(layout: LayoutConfig, relativePath: string) {
|
|||
let path = layout.icon;
|
||||
if (layout.icon.startsWith("<")) {
|
||||
// THis is already the svg
|
||||
path = "./assets/generated/" + layout.id + "_logo.svg"
|
||||
path = "../assets/generated/" + layout.id + "_logo.svg"
|
||||
writeFileSync(path, layout.icon)
|
||||
}
|
||||
|
||||
|
@ -212,19 +212,19 @@ function createLandingPage(layout: LayoutConfig) {
|
|||
}
|
||||
|
||||
const og = `
|
||||
<meta property="og:image" content="${ogImage ?? './assets/svg/add.svg'}">
|
||||
<meta property="og:image" content="${ogImage ?? '../assets/svg/add.svg'}">
|
||||
<meta property="og:title" content="${ogTitle}">
|
||||
<meta property="og:description" content="${ogDescr}">`
|
||||
|
||||
let icon = layout.icon;
|
||||
if (icon.startsWith("<?xml") || icon.startsWith("<svg")) {
|
||||
// This already is an svg
|
||||
icon = `./assets/generated/${layout.id}_icon.svg`
|
||||
icon = `../assets/generated/${layout.id}_icon.svg`
|
||||
writeFileSync(icon, layout.icon);
|
||||
}
|
||||
|
||||
let output = template
|
||||
.replace(`./manifest.manifest`, `./${enc(layout.id)}.webmanifest`)
|
||||
.replace(`../manifest.manifest`, `../${enc(layout.id)}.webmanifest`)
|
||||
.replace("<!-- $$$OG-META -->", og)
|
||||
.replace(/<title>.+?<\/title>/, `<title>${ogTitle}</title>`)
|
||||
.replace("Loading MapComplete, hang on...", `Loading MapComplete theme <i>${ogTitle}</i>...`)
|
||||
|
@ -251,7 +251,7 @@ let wikiPage = "{|class=\"wikitable sortable\"\n" +
|
|||
"|-";
|
||||
|
||||
|
||||
const generatedDir = "./assets/generated";
|
||||
const generatedDir = "../assets/generated";
|
||||
if (! existsSync(generatedDir)) {
|
||||
mkdirSync(generatedDir)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import * as fs from "fs";
|
||||
import {Utils} from "./Utils";
|
||||
import {Utils} from "../Utils";
|
||||
|
||||
function genImages() {
|
||||
|
||||
|
@ -7,6 +7,7 @@ function genImages() {
|
|||
const dir = fs.readdirSync("./assets/svg")
|
||||
|
||||
let module = "import {Img} from \"./UI/Img\";\nimport {FixedUiElement} from \"./UI/Base/FixedUiElement\";\n\nexport default class Svg {\n\n\n";
|
||||
const allNames: string[] = [];
|
||||
for (const path of dir) {
|
||||
|
||||
if (!path.endsWith(".svg")) {
|
||||
|
@ -26,47 +27,11 @@ function genImages() {
|
|||
module += ` public static ${name}_img = Img.AsImageElement(Svg.${name})\n`
|
||||
module += ` public static ${name}_svg() { return new FixedUiElement(Svg.${name});}\n`
|
||||
module += ` public static ${name}_ui() { return new FixedUiElement(Svg.${name}_img);}\n\n`
|
||||
allNames.push(`"${path}": Svg.${name}`)
|
||||
}
|
||||
module += `public static All = {${allNames.join(",")}};`
|
||||
module += "}\n";
|
||||
fs.writeFileSync("Svg.ts", module);
|
||||
console.log("Done")
|
||||
}
|
||||
|
||||
function isTranslation(tr: any): boolean {
|
||||
for (const key in tr) {
|
||||
if (typeof tr[key] !== "string") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function transformTranslation(obj: any, depth = 1) {
|
||||
|
||||
if (isTranslation(obj)) {
|
||||
return `new Translation( ${JSON.stringify(obj)} )`
|
||||
}
|
||||
|
||||
let values = ""
|
||||
for (const key in obj) {
|
||||
values += (Utils.Times((_) => " ", depth)) + key + ": " + transformTranslation(obj[key], depth + 1) + ",\n"
|
||||
}
|
||||
return `{${values}}`;
|
||||
|
||||
}
|
||||
|
||||
function genTranslations() {
|
||||
const translations = JSON.parse(fs.readFileSync("./assets/translations.json", "utf-8"))
|
||||
const transformed = transformTranslation(translations);
|
||||
|
||||
let module = `import {Translation} from "./UI/i18n/Translation"\n\nexport default class AllTranslationAssets {\n\n`;
|
||||
module += " public static t = " + transformed;
|
||||
module += "}"
|
||||
|
||||
fs.writeFileSync("AllTranslationAssets.ts", module);
|
||||
|
||||
|
||||
}
|
||||
|
||||
genTranslations()
|
||||
genImages()
|
40
scripts/generateTranslations.ts
Normal file
40
scripts/generateTranslations.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import * as fs from "fs";
|
||||
import {Utils} from "../Utils";
|
||||
|
||||
function isTranslation(tr: any): boolean {
|
||||
for (const key in tr) {
|
||||
if (typeof tr[key] !== "string") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function transformTranslation(obj: any, depth = 1) {
|
||||
|
||||
if (isTranslation(obj)) {
|
||||
return `new Translation( ${JSON.stringify(obj)} )`
|
||||
}
|
||||
|
||||
let values = ""
|
||||
for (const key in obj) {
|
||||
values += (Utils.Times((_) => " ", depth)) + key + ": " + transformTranslation(obj[key], depth + 1) + ",\n"
|
||||
}
|
||||
return `{${values}}`;
|
||||
|
||||
}
|
||||
|
||||
function genTranslations() {
|
||||
const translations = JSON.parse(fs.readFileSync("./assets/translations.json", "utf-8"))
|
||||
const transformed = transformTranslation(translations);
|
||||
|
||||
let module = `import {Translation} from "./UI/i18n/Translation"\n\nexport default class AllTranslationAssets {\n\n`;
|
||||
module += " public static t = " + transformed;
|
||||
module += "}"
|
||||
|
||||
fs.writeFileSync("AllTranslationAssets.ts", module);
|
||||
|
||||
|
||||
}
|
||||
|
||||
genTranslations()
|
|
@ -25,7 +25,10 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="question">
|
||||
|
||||
<div id="maindiv">'maindiv' not attached</div>
|
||||
</div>
|
||||
<div id="extradiv">'extradiv' not attached</div>
|
||||
<script src="./test.ts"></script>
|
||||
</body>
|
||||
|
|
2
test.ts
2
test.ts
|
@ -6,7 +6,7 @@ import {UIEventSource} from "./Logic/UIEventSource";
|
|||
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
||||
|
||||
const d = new UIEventSource("90");
|
||||
new Direction(d).AttachTo("maindiv")
|
||||
new Direction(d, [51.21576,3.22001]).AttachTo("maindiv")
|
||||
new VariableUiElement(d.map(d => "" + d + "°")).AttachTo("extradiv")
|
||||
|
||||
UIEventSource.Chronic(25, () => {
|
||||
|
|
Loading…
Reference in a new issue