diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts
index 348d543..2772468 100644
--- a/Customizations/AllKnownLayouts.ts
+++ b/Customizations/AllKnownLayouts.ts
@@ -16,10 +16,11 @@ import * as charging_stations from "../assets/themes/charging_stations/charging_
import * as widths from "../assets/themes/widths/width.json"
import * as drinking_water from "../assets/themes/drinking_water/drinking_water.json"
import * as climbing from "../assets/themes/climbing/climbing.json"
-import LayerConfig from "./JSON/LayerConfig";
-import SharedLayers from "./SharedLayers";
+import * as surveillance_cameras from "../assets/themes/surveillance_cameras/surveillance_cameras.json"
import * as personal from "../assets/themes/personalLayout/personalLayout.json"
+import LayerConfig from "./JSON/LayerConfig";
import LayoutConfig from "./JSON/LayoutConfig";
+import SharedLayers from "./SharedLayers";
export class AllKnownLayouts {
@@ -62,6 +63,7 @@ export class AllKnownLayouts {
new LayoutConfig(buurtnatuur),
new LayoutConfig(bike_monitoring_stations),
new LayoutConfig(climbing),
+ new LayoutConfig(surveillance_cameras)
];
diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts
index 7b610b1..6e14be6 100644
--- a/Customizations/JSON/LayerConfig.ts
+++ b/Customizations/JSON/LayerConfig.ts
@@ -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 {VariableUiElement} from "../../UI/Base/VariableUIElement";
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;
@@ -46,17 +56,19 @@ export default class LayerConfig {
tagRenderings: TagRenderingConfig [];
- constructor(json: LayerConfigJson, context?: string) {
+ constructor(json: LayerConfigJson, roamingRenderings: TagRenderingConfig[],
+ context?: string) {
context = context + "." + json.id;
this.id = json.id;
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),
@@ -87,13 +99,16 @@ export default class LayerConfig {
});
}
- this.tagRenderings = trs(json.tagRenderings);
+ this.tagRenderings = trs(json.tagRenderings).concat(roamingRenderings);
this.titleIcons = trs(json.titleIcons ?? ["wikipedialink","osmlink"]);
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,13 +122,120 @@ 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", "");
}
+
+
+ public GenerateLeafletStyle(tags: any, clickable: boolean):
+ {
+ color: string;
+ icon: {
+ iconUrl: string,
+ popupAnchor: [number, number];
+ iconAnchor: [number, number];
+ iconSize: [number, number];
+ html: string;
+ rotation: string;
+ className?: string;
+ };
+ weight: number; dashArray: number[]
+ } {
+
+ function num(str, deflt = 40) {
+ const n = Number(str);
+ if (isNaN(n)) {
+ return deflt;
+ }
+ 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 = render(this.rotation, "0deg");
+
+
+ const iconW = num(iconSize[0]);
+ const iconH = num(iconSize[1]);
+ const mode = iconSize[2] ?? "center"
+
+ let anchorW = iconW / 2;
+ let anchorH = iconH / 2;
+ if (mode === "left") {
+ anchorW = 0;
+ }
+ if (mode === "right") {
+ anchorW = iconW;
+ }
+
+ if (mode === "top") {
+ anchorH = 0;
+ }
+ if (mode === "bottom") {
+ anchorH = iconH;
+ }
+
+
+ let html = `
`;
+
+ 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};display:block;`).Render();
+ }
+ return {
+ icon:
+ {
+ html: html,
+ iconSize: [iconW, iconH],
+ iconAnchor: [anchorW, anchorH],
+ popupAnchor: [0, 3 - anchorH],
+ rotation: rotation,
+ iconUrl: iconUrl,
+ className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable"
+ },
+ color: color,
+ weight: weight,
+ dashArray: dashArray
+ };
+ }
+
+
}
\ No newline at end of file
diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts
index be70638..98df035 100644
--- a/Customizations/JSON/LayerConfigJson.ts
+++ b/Customizations/JSON/LayerConfigJson.ts
@@ -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,15 @@ export interface LayerConfigJson {
* Default is '40,40,center'
*/
iconSize?: string | TagRenderingConfigJson;
-
/**
- * The color for way-elements
+ * The rotation of an icon, useful for e.g. directions.
+ * Usage: as if it were a css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)``
+ */
+ rotation?: string | TagRenderingConfigJson;
+
+ /**
+ * 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 +104,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 +120,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) []
}
\ No newline at end of file
diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts
index 2a22ccd..b914a79 100644
--- a/Customizations/JSON/LayoutConfig.ts
+++ b/Customizations/JSON/LayoutConfig.ts
@@ -80,7 +80,7 @@ export default class LayoutConfig {
} else {
throw "Unkown fixed layer " + layer;
}
- return new LayerConfig(layer, `${this.id}.layers[${i}]`);
+ return new LayerConfig(layer, this.roamingRenderings,`${this.id}.layers[${i}]`);
});
this.hideFromOverview = json.hideFromOverview ?? false;
diff --git a/Customizations/JSON/LayoutConfigJson.ts b/Customizations/JSON/LayoutConfigJson.ts
index 95c14c4..fe5451f 100644
--- a/Customizations/JSON/LayoutConfigJson.ts
+++ b/Customizations/JSON/LayoutConfigJson.ts
@@ -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)[],
diff --git a/Customizations/SharedLayers.ts b/Customizations/SharedLayers.ts
index 34dd9da..4beaa3e 100644
--- a/Customizations/SharedLayers.ts
+++ b/Customizations/SharedLayers.ts
@@ -13,6 +13,8 @@ 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 * as surveillance_camera from "../assets/layers/surveillance_cameras/surveillance_cameras.json"
import LayerConfig from "./JSON/LayerConfig";
export default class SharedLayers {
@@ -24,20 +26,22 @@ export default class SharedLayers {
private static getSharedLayers(){
const sharedLayersList = [
- new LayerConfig(drinkingWater, "shared_layers"),
- new LayerConfig(ghostbikes, "shared_layers"),
- new LayerConfig(viewpoint, "shared_layers"),
- new LayerConfig(bike_parking, "shared_layers"),
- new LayerConfig(bike_repair_station, "shared_layers"),
- new LayerConfig(bike_monitoring_station, "shared_layers"),
- new LayerConfig(birdhides, "shared_layers"),
- new LayerConfig(nature_reserve, "shared_layers"),
- new LayerConfig(bike_cafes, "shared_layers"),
- new LayerConfig(cycling_themed_objects, "shared_layers"),
- new LayerConfig(bike_shops, "shared_layers"),
- new LayerConfig(bike_cleaning, "shared_layers"),
- new LayerConfig(maps, "shared_layers"),
- new LayerConfig(information_boards, "shared_layers")
+ new LayerConfig(drinkingWater,[], "shared_layers"),
+ new LayerConfig(ghostbikes,[], "shared_layers"),
+ new LayerConfig(viewpoint,[], "shared_layers"),
+ new LayerConfig(bike_parking,[], "shared_layers"),
+ new LayerConfig(bike_repair_station,[], "shared_layers"),
+ new LayerConfig(bike_monitoring_station,[], "shared_layers"),
+ new LayerConfig(birdhides,[], "shared_layers"),
+ new LayerConfig(nature_reserve,[], "shared_layers"),
+ new LayerConfig(bike_cafes,[], "shared_layers"),
+ new LayerConfig(cycling_themed_objects,[], "shared_layers"),
+ 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"),
+ new LayerConfig(surveillance_camera,[], "shared_layers")
];
const sharedLayers = new Map();
diff --git a/InitUiElements.ts b/InitUiElements.ts
index 4b9a855..e4ac702 100644
--- a/InitUiElements.ts
+++ b/InitUiElements.ts
@@ -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 {
@@ -110,6 +113,9 @@ export class InitUiElements {
InitUiElements.setupAllLayerElements();
+ if (layoutToUse.customCss !== undefined) {
+ Utils.LoadCustomCss(layoutToUse.customCss);
+ }
function updateFavs() {
const favs = State.state.favouriteLayers.data ?? [];
@@ -139,6 +145,7 @@ export class InitUiElements {
}
+
if (layoutToUse.id === personal.id) {
State.state.favouriteLayers.addCallback(updateFavs);
State.state.installedThemes.addCallback(updateFavs);
@@ -150,27 +157,36 @@ export class InitUiElements {
* This is given to the div which renders fullscreen on mobile devices
*/
State.state.selectedElement.addCallback((feature) => {
- if (feature?.feature?.properties === undefined) {
+
+ if (feature === undefined) {
+ State.state.fullScreenMessage.setData(undefined);
+ }
+ if (feature?.properties === undefined) {
return;
}
- const data = feature.feature.properties;
+ const data = feature.properties;
// Which is the applicable set?
for (const layer of layoutToUse.layers) {
if (typeof layer === "string") {
continue;
}
const applicable = layer.overpassTags.matches(TagUtils.proprtiesToKV(data));
- if (applicable) {
- // This layer is the layer that gives the questions
-
- const featureBox = new FeatureInfoBox(
- State.state.allElements.getElement(data.id),
- layer
- );
-
- State.state.fullScreenMessage.setData(featureBox);
- break;
+ 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
+ );
+
+ State.state.fullScreenMessage.setData(featureBox);
+ break;
}
}
);
@@ -201,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;`)
@@ -269,7 +300,7 @@ export class InitUiElements {
]
if (State.state.featureSwitchShareScreen.data) {
- tabs.push({header: Svg.share_img, content: new ShareScreen()});
+ tabs.push({header: Svg.share, content: new ShareScreen()});
}
if (State.state.featureSwitchMoreQuests.data) {
@@ -282,12 +313,12 @@ export class InitUiElements {
tabs.push({
- header: Svg.help_img,
+ header: Svg.help ,
content: new VariableUiElement(State.state.osmConnection.userDetails.map(userdetails => {
if (userdetails.csCount < State.userJourney.mapCompleteHelpUnlock) {
return ""
}
- return Translations.t.general.aboutMapcomplete.Render();
+ return new Combine([Translations.t.general.aboutMapcomplete, "
Version "+State.vNumber]).Render();
}, [Locale.language]))
}
);
@@ -303,8 +334,8 @@ export class InitUiElements {
const fullOptions = this.CreateWelcomePane();
- const help = Svg.help_ui().SetClass("open-welcome-button");
- const close = Svg.close_ui().SetClass("close-welcome-button");
+ const help = Svg.help_svg().SetClass("open-welcome-button");
+ const close = Svg.close_svg().SetClass("close-welcome-button");
const checkbox = new CheckBox(
new Combine([
close,
@@ -324,11 +355,15 @@ export class InitUiElements {
checkbox.isEnabled.setData(false);
})
+ State.state.selectedElement.addCallback(() => {
+ checkbox.isEnabled.setData(false);
+ })
+
const fullOptions2 = this.CreateWelcomePane();
State.state.fullScreenMessage.setData(fullOptions2)
- Svg.help_ui()
+ Svg.help_svg()
.SetClass("open-welcome-button")
.SetClass("shadow")
.onClick(() => {
@@ -365,15 +400,15 @@ export class InitUiElements {
return;
}
- layerControlPanel.SetStyle("display:block;padding:1em;border-radius:1em;");
- const closeButton = Svg.close_ui().SetClass("layer-selection-toggle").SetStyle(" background: #e5f5ff;")
+ layerControlPanel.SetStyle("display:block;padding:0.75em;border-radius:1em;");
+ const closeButton = Svg.close_svg().SetClass("layer-selection-toggle").SetStyle(" background: var(--subtle-detail-color);")
const checkbox = new CheckBox(
new Combine([
closeButton,
layerControlPanel]).SetStyle("display:flex;flex-direction:row;")
.SetClass("hidden-on-mobile")
,
- Svg.layers_ui().SetClass("layer-selection-toggle"),
+ Svg.layers_svg().SetClass("layer-selection-toggle"),
State.state.layerControlIsOpened
);
@@ -432,17 +467,19 @@ 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;
- const queryParam = QueryParameters.GetQueryParameter("background", State.state.layoutToUse.data.defaultBackgroundId);
+ const queryParam = QueryParameters.GetQueryParameter("background", State.state.layoutToUse.data.defaultBackgroundId, "The id of the background layer to start with");
queryParam.addCallbackAndRun((selectedId: string) => {
const available = State.state.availableBackgroundLayers.data;
@@ -472,18 +509,15 @@ export class InitUiElements {
throw "Layer " + layer + " was not substituted";
}
- const generateInfo = (tagsES, feature) => {
+ let generateContents = (tags: UIEventSource) => new FeatureInfoBox(tags, layer);
+ if (layer.title === undefined && (layer.tagRenderings ?? []).length === 0) {
+ generateContents = undefined;
+ }
- return new FeatureInfoBox(
- tagsES,
- layer,
- )
- };
-
- const flayer: FilteredLayer = FilteredLayer.fromDefinition(layer, generateInfo);
+ const flayer: FilteredLayer = new FilteredLayer(layer, generateContents);
flayers.push(flayer);
- QueryParameters.GetQueryParameter("layer-" + layer.id, "true")
+ QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wehter or not layer " + layer.id + " is shown")
.map((str) => str !== "false", [], (b) => b.toString())
.syncWith(
flayer.isDisplayed
diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts
index bcf71c4..c978185 100644
--- a/Logic/FilteredLayer.ts
+++ b/Logic/FilteredLayer.ts
@@ -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
@@ -25,13 +26,9 @@ export class FilteredLayer {
public readonly layerDef: LayerConfig;
private readonly _maxAllowedOverlap: number;
- private readonly _style: (properties) => { color: string, weight?: number, icon: { iconUrl: string, iconSize?: [number, number], popupAnchor?: [number, number], iconAnchor?: [number, number] } };
-
-
/** The featurecollection from overpass
*/
private _dataFromOverpass: any[];
- private readonly _wayHandling: number;
/** List of new elements, geojson features
*/
private _newElements = [];
@@ -49,60 +46,7 @@ export class FilteredLayer {
) {
this.layerDef = layerDef;
- this._wayHandling = layerDef.wayHandling;
this._showOnPopup = showOnPopup;
- this._style = (tags) => {
-
- const iconUrl = layerDef.icon?.GetRenderValue(tags)?.txt;
- const iconSize = (layerDef.iconSize?.GetRenderValue(tags)?.txt ?? "40,40,center").split(",");
-
-
- const dashArray = layerDef.dashArray.GetRenderValue(tags)?.txt.split(" ").map(Number);
-
- function num(str, deflt = 40) {
- const n = Number(str);
- if (isNaN(n)) {
- return deflt;
- }
- return n;
- }
-
- const iconW = num(iconSize[0]);
- const iconH = num(iconSize[1]);
- const mode = iconSize[2] ?? "center"
-
- let anchorW = iconW / 2;
- let anchorH = iconH / 2;
- if (mode === "left") {
- anchorW = 0;
- }
- if (mode === "right") {
- anchorW = iconW;
- }
-
- if (mode === "top") {
- anchorH = 0;
- }
- if (mode === "bottom") {
- anchorH = iconH;
- }
-
-
- const color = layerDef.color?.GetRenderValue(tags)?.txt ?? "#00f";
- let weight = num(layerDef.width?.GetRenderValue(tags)?.txt, 5);
- return {
- icon:
- {
- iconUrl: iconUrl,
- iconSize: [iconW, iconH],
- iconAnchor: [anchorW, anchorH],
- popupAnchor: [0, 3 - anchorH]
- },
- color: color,
- weight: weight,
- dashArray: dashArray
- };
- };
this.name = name;
this.filters = layerDef.overpassTags;
this._maxAllowedOverlap = layerDef.hideUnderlayingFeaturesMinPercentage;
@@ -123,17 +67,6 @@ export class FilteredLayer {
}
})
}
-
- static fromDefinition(
- definition,
- showOnPopup: (tags: UIEventSource, feature: any) => UIElement):
- FilteredLayer {
- return new FilteredLayer(
- definition, showOnPopup);
-
- }
-
-
/**
* The main function to load data into this layer.
* The data that is NOT used by this layer, is returned as a geojson object; the other data is rendered
@@ -142,32 +75,17 @@ export class FilteredLayer {
const leftoverFeatures = [];
const selfFeatures = [];
for (let feature of geojson.features) {
- // feature.properties contains all the properties
-
const tags = TagUtils.proprtiesToKV(feature.properties);
-
- if (!this.filters.matches(tags)) {
+ const matches = this.filters.matches(tags);
+ if (matches) {
+ selfFeatures.push(feature);
+ }
+ if (!matches || this.layerDef.passAllFeatures) {
leftoverFeatures.push(feature);
- continue;
}
-
- if (feature.geometry.type !== "Point") {
- const centerPoint = GeoOperations.centerpoint(feature);
- if (this._wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) {
- selfFeatures.push(centerPoint);
- } else if (this._wayHandling === LayerConfig.WAYHANDLING_CENTER_ONLY) {
- feature = centerPoint;
- }
- }
- selfFeatures.push(feature);
-
}
-
- this.RenderLayer({
- type: "FeatureCollection",
- features: selfFeatures
- })
+ this.RenderLayer(selfFeatures)
const notShadowed = [];
for (const feature of leftoverFeatures) {
@@ -190,18 +108,126 @@ export class FilteredLayer {
public AddNewElement(element) {
this._newElements.push(element);
- this.RenderLayer({features: this._dataFromOverpass}, element); // Update the layer
-
+ this.RenderLayer( this._dataFromOverpass); // Update the layer
}
- private RenderLayer(data, openPopupOf = undefined) {
- let self = this;
+ private RenderLayer(features) {
if (this._geolayer !== undefined && this._geolayer !== null) {
// Remove the old geojson layer from the map - we'll reshow all the elements later on anyway
State.state.bm.map.removeLayer(this._geolayer);
}
+ // We fetch all the data we have to show:
+ let fusedFeatures = this.ApplyWayHandling(this.FuseData(features));
+
+ // And we copy some features as points - if needed
+ const data = {
+ type: "FeatureCollection",
+ features: fusedFeatures
+ }
+
+ let self = this;
+ this._geolayer = L.geoJSON(data, {
+ style: feature =>
+ self.layerDef.GenerateLeafletStyle(feature.properties, self._showOnPopup !== undefined),
+ pointToLayer: function (feature, latLng) {
+ // Point to layer converts the 'point' to a layer object - as the geojson layer natively cannot handle points
+ // Click handling is done in the next step
+
+ const style = self.layerDef.GenerateLeafletStyle(feature.properties, self._showOnPopup !== undefined);
+ let marker;
+ if (style.icon === undefined) {
+ marker = L.circle(latLng, {
+ radius: 25,
+ color: style.color
+ });
+ } else if (style.icon.iconUrl.startsWith("$circle")) {
+ marker = L.circle(latLng, {
+ radius: 25,
+ color: style.color
+ });
+ } else {
+ marker = L.marker(latLng, {
+ icon: L.divIcon(style.icon)
+ });
+ }
+ return marker;
+ },
+ onEachFeature: function (feature, layer: Layer) {
+
+ if (self._showOnPopup === undefined) {
+ // No popup contents defined -> don't do anything
+ return;
+ }
+ const popup = L.popup({
+ autoPan: true,
+ closeOnEscapeKey: true,
+ }, layer);
+
+ let uiElement: UIElement;
+
+ const eventSource = State.state.allElements.addOrGetElement(feature);
+ uiElement = self._showOnPopup(eventSource, feature);
+ popup.setContent(uiElement.Render());
+ layer.bindPopup(popup);
+ // We first render the UIelement (which'll still need an update later on...)
+ // But at least it'll be visible already
+
+
+ layer.on("click", (e) => {
+ // We set the element as selected...
+ State.state.selectedElement.setData(feature);
+ uiElement.Update();
+ });
+
+ if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) {
+ const center = GeoOperations.centerpoint(feature).geometry.coordinates;
+ popup.setLatLng({lat: center[1], lng: center[0]});
+ popup.openOn(State.state.bm.map)
+ }
+
+ }
+ });
+
+ if (this.combinedIsDisplayed.data) {
+ this._geolayer.addTo(State.state.bm.map);
+ }
+
+ }
+
+ private ApplyWayHandling(fusedFeatures: any[]) {
+ if (this.layerDef.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) {
+ // We don't have to do anything special
+ return fusedFeatures;
+ }
+
+
+ // We have to convert all the ways into centerpoints
+ const existingPoints = [];
+ const newPoints = [];
+ const existingWays = [];
+
+ for (const feature of fusedFeatures) {
+ if (feature.geometry.type === "Point") {
+ existingPoints.push(feature);
+ continue;
+ }
+
+ existingWays.push(feature);
+ const centerPoint = GeoOperations.centerpoint(feature);
+ newPoints.push(centerPoint);
+ }
+
+ fusedFeatures = existingPoints.concat(newPoints);
+ if (this.layerDef.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) {
+ fusedFeatures = fusedFeatures.concat(existingWays)
+ }
+ return fusedFeatures;
+ }
+
+ //*Fuses the old and the new datasets*/
+ private FuseData(data: any[]) {
const oldData = this._dataFromOverpass ?? [];
// We keep track of all the ids that are freshly loaded in order to avoid adding duplicates
@@ -209,7 +235,7 @@ export class FilteredLayer {
// A list of all the features to show
const fusedFeatures = [];
// First, we add all the fresh data:
- for (const feature of data.features) {
+ for (const feature of data) {
idsFromOverpass.add(feature.properties.id);
fusedFeatures.push(feature);
}
@@ -230,132 +256,6 @@ export class FilteredLayer {
fusedFeatures.push(feature);
}
}
-
-
- // We use a new, fused dataset
- data = {
- type: "FeatureCollection",
- features: fusedFeatures
- }
-
-
- // The data is split in two parts: the poinst and the rest
- // The points get a special treatment in order to render them properly
- // Note that some features might get a point representation as well
-
- const runWhenAdded: (() => void)[] = []
-
- this._geolayer = L.geoJSON(data, {
- style: function (feature) {
- return self._style(feature.properties);
- },
- pointToLayer: function (feature, latLng) {
- const style = self._style(feature.properties);
- let marker;
- if (style.icon === undefined) {
- marker = L.circle(latLng, {
- radius: 25,
- color: style.color
- });
-
- } else if (style.icon.iconUrl.startsWith("$circle")) {
- marker = L.circle(latLng, {
- radius: 25,
- color: style.color
- });
- } else {
- if (style.icon.iconSize === undefined) {
- style.icon.iconSize = [50, 50]
- }
-
- // @ts-ignore
- marker = L.marker(latLng, {
- icon: L.icon(style.icon),
- });
- }
- let eventSource = State.state.allElements.addOrGetElement(feature);
- const popup = L.popup({}, marker);
- let uiElement: UIElement;
- let content = undefined;
- let p = marker.bindPopup(popup)
- .on("popupopen", () => {
- if (content === undefined) {
- uiElement = self._showOnPopup(eventSource, feature);
- // Lazily create the content
- content = uiElement.Render();
- }
- popup.setContent(content);
- uiElement.Update();
- });
-
- if (feature === openPopupOf) {
- runWhenAdded.push(() => {
- p.openPopup();
- })
- }
-
- return marker;
- },
-
- onEachFeature: function (feature, layer:Layer) {
-
- // We monky-patch the feature element with an update-style
- function updateStyle () {
- // @ts-ignore
- if (layer.setIcon) {
- const style = self._style(feature.properties);
- const icon = style.icon;
- if (icon.iconUrl) {
- if (icon.iconUrl.startsWith("$circle")) {
- // pass
- } else {
- // @ts-ignore
- layer.setIcon(L.icon(icon))
- }
- }
- } else {
- self._geolayer.setStyle(function (featureX) {
- return self._style(featureX.properties);
- });
- }
- }
-
- let eventSource = State.state.allElements.addOrGetElement(feature);
-
-
- eventSource.addCallback(updateStyle);
-
- function openPopup(e) {
- State.state.selectedElement.setData({feature: feature});
- updateStyle()
- if (feature.geometry.type === "Point") {
- return; // Points bind there own popups
- }
-
- const uiElement = self._showOnPopup(eventSource, feature);
-
- L.popup({
- autoPan: true,
- }).setContent(uiElement.Render())
- .setLatLng(e.latlng)
- .openOn(State.state.bm.map);
- uiElement.Update();
- if (e) {
- L.DomEvent.stop(e); // Marks the event as consumed
- }
- }
-
- layer.on("click", openPopup);
- }
- });
-
- if (this.combinedIsDisplayed.data) {
- this._geolayer.addTo(State.state.bm.map);
- for (const f of runWhenAdded) {
- f();
- }
- }
+ return fusedFeatures;
}
-
-
}
\ No newline at end of file
diff --git a/Logic/Leaflet/GeoLocationHandler.ts b/Logic/Leaflet/GeoLocationHandler.ts
index 5975529..e94b23e 100644
--- a/Logic/Leaflet/GeoLocationHandler.ts
+++ b/Logic/Leaflet/GeoLocationHandler.ts
@@ -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
- const icon = L.icon(
- {
- iconUrl: Img.AsData(Svg.crosshair_blue),
- 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 color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color")
+ const icon = L.icon(
+ {
+ 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
+ })
+
const newMarker = L.marker(location.latlng, {icon: icon});
newMarker.addTo(map);
diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts
index e4acd42..d2991ea 100644
--- a/Logic/MetaTagging.ts
+++ b/Logic/MetaTagging.ts
@@ -82,7 +82,7 @@ export default class MetaTagging {
})
)
private static isOpen = new SimpleMetaTagger(
- ["_isOpen", "_isOpen:description"], "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no",
+ ["_isOpen", "_isOpen:description"], "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')",
(feature => {
const tagsSource = State.state.allElements.addOrGetElement(feature);
tagsSource.addCallback(tags => {
@@ -123,16 +123,40 @@ export default class MetaTagging {
})
)
- public static carriageWayWidth = new SimpleMetaTagger(
- ["_width:needed","_width:needed:no_pedestrians", "_width:difference"],
+ private static directionSimplified = new SimpleMetaTagger(
+ ["_direction:simplified", "_direction:leftright"], "_direction:simplified turns 'camera:direction' and 'direction' into either 0, 45, 90, 135, 180, 225, 270 or 315, whichever is closest. _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map",
+ (feature => {
+ const tags = feature.properties;
+ const direction = tags["camera:direction"] ?? tags["direction"];
+ if (direction === undefined) {
+ return;
+ }
+ let n = Number(direction);
+ if (isNaN(n)) {
+ return;
+ }
+
+ // [22.5 -> 67.5] is sector 1
+ // [67.5 -> ] is sector 1
+ n = (n + 22.5) % 360;
+ n = Math.floor(n / 45);
+ tags["_direction:simplified"] = n;
+ tags["_direction:leftright"] = n <= 3 ? "right" : "left";
+
+
+ })
+ )
+
+ private static carriageWayWidth = new SimpleMetaTagger(
+ ["_width:needed", "_width:needed:no_pedestrians", "_width:difference"],
"Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:carriageway' is present",
(feature: any, index: number) => {
const properties = feature.properties;
- if(properties["width:carriageway"] === undefined){
+ if (properties["width:carriageway"] === undefined) {
return;
}
-
+
const carWidth = 2;
const cyclistWidth = 1.5;
const pedestrianWidth = 0.75;
@@ -239,7 +263,8 @@ export default class MetaTagging {
MetaTagging.surfaceArea,
MetaTagging.country,
MetaTagging.isOpen,
- MetaTagging.carriageWayWidth
+ MetaTagging.carriageWayWidth,
+ MetaTagging.directionSimplified
];
diff --git a/Logic/UpdateFromOverpass.ts b/Logic/UpdateFromOverpass.ts
index 8cd6bbe..d5fe1a2 100644
--- a/Logic/UpdateFromOverpass.ts
+++ b/Logic/UpdateFromOverpass.ts
@@ -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++) {
@@ -128,6 +133,7 @@ export class UpdateFromOverpass {
this.ForceRefresh();
console.log(`QUERY FAILED (retrying in ${5 * this.retries.data} sec)`, reason);
this.retries.ping();
+ this.runningQuery.setData(false)
const self = this;
window?.setTimeout(
function () {
diff --git a/Logic/Web/Hash.ts b/Logic/Web/Hash.ts
new file mode 100644
index 0000000..6ca2d99
--- /dev/null
+++ b/Logic/Web/Hash.ts
@@ -0,0 +1,18 @@
+import {UIEventSource} from "../UIEventSource";
+
+export default class Hash {
+
+ public static Get() : UIEventSource{
+ const hash = new UIEventSource(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;
+ }
+
+}
\ No newline at end of file
diff --git a/Logic/Web/Imgur.ts b/Logic/Web/Imgur.ts
index ec47fc3..6fe620c 100644
--- a/Logic/Web/Imgur.ts
+++ b/Logic/Web/Imgur.ts
@@ -56,8 +56,8 @@ export class Imgur {
},
};
$.ajax(settings).done(function (response) {
- const descr : string= response.data.description;
- const data : any = {};
+ const descr: string = response.data.description ?? "";
+ const data: any = {};
for (const tag of descr.split("\n")) {
const kv = tag.split(":");
const k = kv[0];
diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts
index 21a57cc..dd8456e 100644
--- a/Logic/Web/QueryParameters.ts
+++ b/Logic/Web/QueryParameters.ts
@@ -2,23 +2,26 @@
* Wraps the query parameters into UIEventSources
*/
import {UIEventSource} from "../UIEventSource";
+import Hash from "./Hash";
export class QueryParameters {
- private static order: string [] = ["layout","test","z","lat","lon"];
+ private static order: string [] = ["layout", "test", "z", "lat", "lon"];
private static knownSources = {};
private static initialized = false;
private static defaults = {}
-
- private static addOrder(key){
- if(this.order.indexOf(key) < 0){
+
+ private static documentation = {}
+
+ private static addOrder(key) {
+ if (this.order.indexOf(key) < 0) {
this.order.push(key)
}
}
private static init() {
-
- if(this.initialized){
+
+ if (this.initialized) {
return;
}
this.initialized = true;
@@ -55,14 +58,15 @@ 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);
}
- public static GetQueryParameter(key: string, deflt: string): UIEventSource {
+ public static GetQueryParameter(key: string, deflt: string, documentation?: string): UIEventSource {
if(!this.initialized){
this.init();
}
+ QueryParameters.documentation[key] = documentation;
if (deflt !== undefined) {
QueryParameters.defaults[key] = deflt;
}
@@ -76,4 +80,12 @@ export class QueryParameters {
return source;
}
+ public static GenerateQueryParameterDocs(): string {
+ const docs = [];
+ for (const key in QueryParameters.documentation) {
+ docs.push("**" + key + "**: " + QueryParameters.documentation[key] + " (default value: _" + QueryParameters.defaults[key] + "_)")
+ }
+ return docs.join("\n\n");
+ }
+
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 797ba4a..f5393a6 100644
--- a/README.md
+++ b/README.md
@@ -103,8 +103,8 @@ A theme has translations into the preset.json (`assets/themes/themename/themenam
0. Fork this repository
1. Modify `"language"` to contain the new language, e.g. `"language": "nl"` becomes `"language": ["nl", "en"]`
-2. Add extra strings to the texts. If it used to be a single-language theme, one can replace the strings, e.g.: `"description": "Welcome to Open Bookcase Map"` to `"description": {"en": "Welcome to Open Bookcase Map", "nl": "Welkom bij de OpenBoekenruilkastenKaart", "fr": "Bienvenue sûr la carte des microbibliotheques"}`. If the correct language is not found, it'll fallback to another supported language.
-3. If you notice missing translations in the core of MapComplete, fork this project, open [the file containing all translations](https://github.com/pietervdvn/MapComplete/blob/master/UI/i18n/Translations.ts), add add a language string there
+2. Add extra strings to the texts. If it used to be a single-language theme, one can replace the strings, e.g.: `"description": "Welcome to Open Bookcase Map"` to `"description": {"en": "Welcome to Open Bookcase Map", "nl": "Welkom bij de OpenBoekenruilkastenKaart", "fr": "Bienvenue sûr la carte des petites bibliotheques"}`. If the correct language is not found, it'll fallback to another supported language.
+3. If you notice missing translations in the core of MapComplete, fork this project, open [the file containing all translations](https://github.com/pietervdvn/MapComplete/blob/master/assets/translations.json), add add a language string there
4. Send a pull request to update the languages, I'll gladly add it! It doesn't have to be a complete translation from the start ;)
### Adding your theme to the repository
@@ -166,6 +166,50 @@ Whenever a change is made -even adding a single tag- the change is uploaded into
Note that changesets are closed automatically after one hour of inactivity, so we don't have to worry about closing them.
+### Query parameters
+
+By adding extra query parameters, more options are available to influence:
+
+**test**: If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org (default value: _false_)
+
+**layout**: The layout to load into MapComplete (default value: _bookcases_)
+
+**userlayout**: undefined (default value: _false_)
+
+**layer-control-toggle**: Wether or not the layer control is shown (default value: _false_)
+
+**tab**: The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >200 changesets) (default value: _0_)
+
+**z**: The initial/current zoom level (default value: _1_)
+
+**lat**: The initial/current latitude (default value: _0_)
+
+**lon**: The initial/current longitude of the app (default value: _0_)
+
+**fs-userbadge**: Disables/Enables the userbadge (and thus disables login capabilities) (default value: _true_)
+
+**fs-search**: Disables/Enables the search bar (default value: _true_)
+
+**fs-layers**: Disables/Enables the layer control (default value: _true_)
+
+**fs-add-new**: Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) (default value: _true_)
+
+**fs-welcome-message**: undefined (default value: _true_)
+
+**fs-iframe**: Disables/Enables the iframe-popup (default value: _false_)
+
+**fs-more-quests**: Disables/Enables the 'More Quests'-tab in the welcome message (default value: _true_)
+
+**fs-share-screen**: Disables/Enables the 'Share-screen'-tab in the welcome message (default value: _true_)
+
+**fs-geolocation**: Disables/Enables the geolocation button (default value: _true_)
+
+**oauth_token**: Used to complete the login (default value: _undefined_)
+
+**background**: The id of the background layer to start with (default value: _undefined_)
+
+**layer-bookcases**: Wehter or not layer bookcases is shown (default value: _true_) index.ts:104:8
+
# Privacy
Privacy is important, we try to leak as little information as possible.
diff --git a/State.ts b/State.ts
index d739d42..ec61047 100644
--- a/State.ts
+++ b/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
@@ -21,9 +22,9 @@ export default class State {
// The singleton of the global state
public static state: State;
-
- public static vNumber = "0.1.2g";
-
+
+ public static vNumber = "0.2.0";
+
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
addNewPointsUnlock: 0,
@@ -82,7 +83,7 @@ export default class State {
/**
The latest element that was selected - used to generate the right UI at the right place
*/
- public readonly selectedElement = new UIEventSource<{ feature: any }>(undefined);
+ public readonly selectedElement = new UIEventSource(undefined);
public readonly zoom: UIEventSource;
public readonly lat: UIEventSource;
@@ -115,10 +116,10 @@ export default class State {
public layoutDefinition: string;
public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>;
- public layerControlIsOpened: UIEventSource = QueryParameters.GetQueryParameter("layer-control-toggle", "false")
+ public layerControlIsOpened: UIEventSource = QueryParameters.GetQueryParameter("layer-control-toggle", "false", "Wether or not the layer control is shown")
.map((str) => str !== "false", [], b => "" + b)
- public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0").map(
+ public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${State.userJourney.mapCompleteHelpUnlock} changesets)`).map(
str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n
);
@@ -138,11 +139,11 @@ export default class State {
})
}
this.zoom = asFloat(
- QueryParameters.GetQueryParameter("z", "" + layoutToUse.startZoom)
+ QueryParameters.GetQueryParameter("z", "" + layoutToUse.startZoom, "The initial/current zoom level")
.syncWith(LocalStorageSource.Get("zoom")));
- this.lat = asFloat(QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat)
+ this.lat = asFloat(QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat, "The initial/current latitude")
.syncWith(LocalStorageSource.Get("lat")));
- this.lon = asFloat(QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon)
+ this.lon = asFloat(QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon, "The initial/current longitude of the app")
.syncWith(LocalStorageSource.Get("lon")));
@@ -165,39 +166,66 @@ export default class State {
});
- function featSw(key: string, deflt: (layout: LayoutConfig) => boolean): UIEventSource {
- const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined);
+ function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource {
+ const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined, documentation);
// I'm so sorry about someone trying to decipher this
// It takes the current layout, extracts the default value for this query paramter. A query parameter event source is then retreived and flattened
return UIEventSource.flatten(
self.layoutToUse.map((layout) => {
const defaultValue = deflt(layout);
- const queryParam = QueryParameters.GetQueryParameter(key, "" + defaultValue)
+ const queryParam = QueryParameters.GetQueryParameter(key, "" + defaultValue, documentation)
return queryParam.map((str) => str === undefined ? defaultValue : (str !== "false"));
}), [queryParameterSource]);
}
- this.featureSwitchUserbadge = featSw("fs-userbadge", (layoutToUse) => layoutToUse?.enableUserBadge ?? true);
- this.featureSwitchSearch = featSw("fs-search", (layoutToUse) => layoutToUse?.enableSearch ?? true);
- this.featureSwitchLayers = featSw("fs-layers", (layoutToUse) => layoutToUse?.enableLayers ?? true);
- this.featureSwitchAddNew = featSw("fs-add-new", (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true);
- this.featureSwitchWelcomeMessage = featSw("fs-welcome-message", () => true);
- this.featureSwitchIframe = featSw("fs-iframe", () => false);
- this.featureSwitchMoreQuests = featSw("fs-more-quests", (layoutToUse) => layoutToUse?.enableMoreQuests ?? true);
- this.featureSwitchShareScreen = featSw("fs-share-screen", (layoutToUse) => layoutToUse?.enableShareScreen ?? true);
- this.featureSwitchGeolocation = featSw("fs-geolocation", (layoutToUse) => layoutToUse?.enableGeolocation ?? true);
+ this.featureSwitchUserbadge = featSw("fs-userbadge", (layoutToUse) => layoutToUse?.enableUserBadge ?? true,
+ "Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode.");
+ this.featureSwitchSearch = featSw("fs-search", (layoutToUse) => layoutToUse?.enableSearch ?? true,
+ "Disables/Enables the search bar");
+ this.featureSwitchLayers = featSw("fs-layers", (layoutToUse) => layoutToUse?.enableLayers ?? true,
+ "Disables/Enables the layer control");
+ this.featureSwitchAddNew = featSw("fs-add-new", (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true,
+ "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)");
+ this.featureSwitchWelcomeMessage = featSw("fs-welcome-message", () => true,
+ "Disables/enables the help menu or welcome message");
+ this.featureSwitchIframe = featSw("fs-iframe", () => false,
+ "Disables/Enables the iframe-popup");
+ this.featureSwitchMoreQuests = featSw("fs-more-quests", (layoutToUse) => layoutToUse?.enableMoreQuests ?? true,
+ "Disables/Enables the 'More Quests'-tab in the welcome message");
+ this.featureSwitchShareScreen = featSw("fs-share-screen", (layoutToUse) => layoutToUse?.enableShareScreen ?? true,
+ "Disables/Enables the 'Share-screen'-tab in the welcome message");
+ this.featureSwitchGeolocation = featSw("fs-geolocation", (layoutToUse) => layoutToUse?.enableGeolocation ?? true,
+ "Disables/Enables the geolocation button");
- const testParam = QueryParameters.GetQueryParameter("test", "false").data;
+ const testParam = QueryParameters.GetQueryParameter("test", "false",
+ "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org").data;
this.osmConnection = new OsmConnection(
testParam === "true",
- QueryParameters.GetQueryParameter("oauth_token", undefined),
+ QueryParameters.GetQueryParameter("oauth_token", undefined,
+ "Used to complete the login"),
layoutToUse.id,
true
);
+ 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) {
@@ -213,8 +241,10 @@ export default class State {
continue;
}
try {
+ const json = btoa(customLayout.data);
+ console.log(json);
const layout = new LayoutConfig(
- JSON.parse(btoa(customLayout.data)));
+ JSON.parse(json));
installedThemes.push({
layout: layout,
definition: customLayout.data
diff --git a/Svg.ts b/Svg.ts
index 7a11237..fbcbcdd 100644
--- a/Svg.ts
+++ b/Svg.ts
@@ -4,160 +4,219 @@ import {FixedUiElement} from "./UI/Base/FixedUiElement";
export default class Svg {
- public static add = " "
+ public static add = " "
public static add_img = Img.AsImageElement(Svg.add)
+ public static add_svg() { return new FixedUiElement(Svg.add);}
public static add_ui() { return new FixedUiElement(Svg.add_img);}
- public static addSmall = " "
+ public static addSmall = " "
public static addSmall_img = Img.AsImageElement(Svg.addSmall)
+ public static addSmall_svg() { return new FixedUiElement(Svg.addSmall);}
public static addSmall_ui() { return new FixedUiElement(Svg.addSmall_img);}
- public static ampersand = " "
+ public static ampersand = "e "
public static ampersand_img = Img.AsImageElement(Svg.ampersand)
+ public static ampersand_svg() { return new FixedUiElement(Svg.ampersand);}
public static ampersand_ui() { return new FixedUiElement(Svg.ampersand_img);}
- public static arrow_left_smooth = " "
+ public static arrow_left_smooth = " "
public static arrow_left_smooth_img = Img.AsImageElement(Svg.arrow_left_smooth)
+ public static arrow_left_smooth_svg() { return new FixedUiElement(Svg.arrow_left_smooth);}
public static arrow_left_smooth_ui() { return new FixedUiElement(Svg.arrow_left_smooth_img);}
- public static arrow_right_smooth = " "
+ public static arrow_right_smooth = " "
public static arrow_right_smooth_img = Img.AsImageElement(Svg.arrow_right_smooth)
+ public static arrow_right_smooth_svg() { return new FixedUiElement(Svg.arrow_right_smooth);}
public static arrow_right_smooth_ui() { return new FixedUiElement(Svg.arrow_right_smooth_img);}
public static bug = ""
public static bug_img = Img.AsImageElement(Svg.bug)
+ public static bug_svg() { return new FixedUiElement(Svg.bug);}
public static bug_ui() { return new FixedUiElement(Svg.bug_img);}
- public static camera_plus = " "
+ public static camera_plus = " "
public static camera_plus_img = Img.AsImageElement(Svg.camera_plus)
+ public static camera_plus_svg() { return new FixedUiElement(Svg.camera_plus);}
public static camera_plus_ui() { return new FixedUiElement(Svg.camera_plus_img);}
- public static checkmark = ""
+ public static checkmark = ""
public static checkmark_img = Img.AsImageElement(Svg.checkmark)
+ public static checkmark_svg() { return new FixedUiElement(Svg.checkmark);}
public static checkmark_ui() { return new FixedUiElement(Svg.checkmark_img);}
- public static close = " "
+ public static close = " "
public static close_img = Img.AsImageElement(Svg.close)
+ public static close_svg() { return new FixedUiElement(Svg.close);}
public static close_ui() { return new FixedUiElement(Svg.close_img);}
- public static crosshair_blue_center = " "
+ public static compass = " "
+ public static compass_img = Img.AsImageElement(Svg.compass)
+ public static compass_svg() { return new FixedUiElement(Svg.compass);}
+ public static compass_ui() { return new FixedUiElement(Svg.compass_img);}
+
+ public static crosshair_blue_center = " "
public static crosshair_blue_center_img = Img.AsImageElement(Svg.crosshair_blue_center)
+ public static crosshair_blue_center_svg() { return new FixedUiElement(Svg.crosshair_blue_center);}
public static crosshair_blue_center_ui() { return new FixedUiElement(Svg.crosshair_blue_center_img);}
- public static crosshair_blue = " "
+ public static crosshair_blue = " "
public static crosshair_blue_img = Img.AsImageElement(Svg.crosshair_blue)
+ public static crosshair_blue_svg() { return new FixedUiElement(Svg.crosshair_blue);}
public static crosshair_blue_ui() { return new FixedUiElement(Svg.crosshair_blue_img);}
- public static crosshair = " "
+ public static crosshair = " "
public static crosshair_img = Img.AsImageElement(Svg.crosshair)
+ public static crosshair_svg() { return new FixedUiElement(Svg.crosshair);}
public static crosshair_ui() { return new FixedUiElement(Svg.crosshair_img);}
- public static delete_icon = " "
+ public static delete_icon = " "
public static delete_icon_img = Img.AsImageElement(Svg.delete_icon)
+ public static delete_icon_svg() { return new FixedUiElement(Svg.delete_icon);}
public static delete_icon_ui() { return new FixedUiElement(Svg.delete_icon_img);}
- public static down = " "
+ public static direction = " "
+ public static direction_img = Img.AsImageElement(Svg.direction)
+ public static direction_svg() { return new FixedUiElement(Svg.direction);}
+ public static direction_ui() { return new FixedUiElement(Svg.direction_img);}
+
+ public static direction_gradient = " "
+ 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 = " "
public static down_img = Img.AsImageElement(Svg.down)
+ public static down_svg() { return new FixedUiElement(Svg.down);}
public static down_ui() { return new FixedUiElement(Svg.down_img);}
- public static envelope = " "
+ public static envelope = " "
public static envelope_img = Img.AsImageElement(Svg.envelope)
+ public static envelope_svg() { return new FixedUiElement(Svg.envelope);}
public static envelope_ui() { return new FixedUiElement(Svg.envelope_img);}
- public static floppy = " "
+ public static floppy = " "
public static floppy_img = Img.AsImageElement(Svg.floppy)
+ public static floppy_svg() { return new FixedUiElement(Svg.floppy);}
public static floppy_ui() { return new FixedUiElement(Svg.floppy_img);}
- public static gear = ""
+ public static gear = ""
public static gear_img = Img.AsImageElement(Svg.gear)
+ public static gear_svg() { return new FixedUiElement(Svg.gear);}
public static gear_ui() { return new FixedUiElement(Svg.gear_img);}
- public static help = " "
+ public static help = " "
public static help_img = Img.AsImageElement(Svg.help)
+ public static help_svg() { return new FixedUiElement(Svg.help);}
public static help_ui() { return new FixedUiElement(Svg.help_img);}
- public static home = " "
+ public static home = " "
public static home_img = Img.AsImageElement(Svg.home)
+ public static home_svg() { return new FixedUiElement(Svg.home);}
public static home_ui() { return new FixedUiElement(Svg.home_img);}
- public static josm_logo = " "
+ public static home_white_bg = " "
+ public static home_white_bg_img = Img.AsImageElement(Svg.home_white_bg)
+ public static home_white_bg_svg() { return new FixedUiElement(Svg.home_white_bg);}
+ public static home_white_bg_ui() { return new FixedUiElement(Svg.home_white_bg_img);}
+
+ public static josm_logo = " "
public static josm_logo_img = Img.AsImageElement(Svg.josm_logo)
+ public static josm_logo_svg() { return new FixedUiElement(Svg.josm_logo);}
public static josm_logo_ui() { return new FixedUiElement(Svg.josm_logo_img);}
- public static layers = " "
+ public static layers = " "
public static layers_img = Img.AsImageElement(Svg.layers)
+ public static layers_svg() { return new FixedUiElement(Svg.layers);}
public static layers_ui() { return new FixedUiElement(Svg.layers_img);}
- public static layersAdd = " "
+ public static layersAdd = " "
public static layersAdd_img = Img.AsImageElement(Svg.layersAdd)
+ public static layersAdd_svg() { return new FixedUiElement(Svg.layersAdd);}
public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);}
- public static logo = " "
+ public static logo = " "
public static logo_img = Img.AsImageElement(Svg.logo)
+ public static logo_svg() { return new FixedUiElement(Svg.logo);}
public static logo_ui() { return new FixedUiElement(Svg.logo_img);}
- public static logout = " "
+ public static logout = " "
public static logout_img = Img.AsImageElement(Svg.logout)
+ public static logout_svg() { return new FixedUiElement(Svg.logout);}
public static logout_ui() { return new FixedUiElement(Svg.logout_img);}
public static mapillary = ""
public static mapillary_img = Img.AsImageElement(Svg.mapillary)
+ public static mapillary_svg() { return new FixedUiElement(Svg.mapillary);}
public static mapillary_ui() { return new FixedUiElement(Svg.mapillary_img);}
public static no_checkmark = ""
public static no_checkmark_img = Img.AsImageElement(Svg.no_checkmark)
+ public static no_checkmark_svg() { return new FixedUiElement(Svg.no_checkmark);}
public static no_checkmark_ui() { return new FixedUiElement(Svg.no_checkmark_img);}
- public static or = " "
+ public static or = " "
public static or_img = Img.AsImageElement(Svg.or)
+ public static or_svg() { return new FixedUiElement(Svg.or);}
public static or_ui() { return new FixedUiElement(Svg.or_img);}
public static osm_logo_us = ""
public static osm_logo_us_img = Img.AsImageElement(Svg.osm_logo_us)
+ public static osm_logo_us_svg() { return new FixedUiElement(Svg.osm_logo_us);}
public static osm_logo_us_ui() { return new FixedUiElement(Svg.osm_logo_us_img);}
- public static osm_logo = " "
+ public static osm_logo = " "
public static osm_logo_img = Img.AsImageElement(Svg.osm_logo)
+ public static osm_logo_svg() { return new FixedUiElement(Svg.osm_logo);}
public static osm_logo_ui() { return new FixedUiElement(Svg.osm_logo_img);}
- public static pencil = ""
+ public static pencil = ""
public static pencil_img = Img.AsImageElement(Svg.pencil)
+ public static pencil_svg() { return new FixedUiElement(Svg.pencil);}
public static pencil_ui() { return new FixedUiElement(Svg.pencil_img);}
- public static pop_out = " "
+ public static pop_out = " "
public static pop_out_img = Img.AsImageElement(Svg.pop_out)
+ public static pop_out_svg() { return new FixedUiElement(Svg.pop_out);}
public static pop_out_ui() { return new FixedUiElement(Svg.pop_out_img);}
- public static reload = " "
+ public static reload = " "
public static reload_img = Img.AsImageElement(Svg.reload)
+ public static reload_svg() { return new FixedUiElement(Svg.reload);}
public static reload_ui() { return new FixedUiElement(Svg.reload_img);}
- public static search = " "
+ public static search = " "
public static search_img = Img.AsImageElement(Svg.search)
+ public static search_svg() { return new FixedUiElement(Svg.search);}
public static search_ui() { return new FixedUiElement(Svg.search_img);}
- public static share = " "
+ public static share = " "
public static share_img = Img.AsImageElement(Svg.share)
+ public static share_svg() { return new FixedUiElement(Svg.share);}
public static share_ui() { return new FixedUiElement(Svg.share_img);}
- public static star = " "
+ public static star = " "
public static star_img = Img.AsImageElement(Svg.star)
+ public static star_svg() { return new FixedUiElement(Svg.star);}
public static star_ui() { return new FixedUiElement(Svg.star_img);}
- public static statistics = " "
+ public static statistics = " "
public static statistics_img = Img.AsImageElement(Svg.statistics)
+ public static statistics_svg() { return new FixedUiElement(Svg.statistics);}
public static statistics_ui() { return new FixedUiElement(Svg.statistics_img);}
- public static up = " "
+ public static up = " "
public static up_img = Img.AsImageElement(Svg.up)
+ public static up_svg() { return new FixedUiElement(Svg.up);}
public static up_ui() { return new FixedUiElement(Svg.up_img);}
- public static wikimedia_commons_white = " "
+ public static wikimedia_commons_white = " "
public static wikimedia_commons_white_img = Img.AsImageElement(Svg.wikimedia_commons_white)
+ public static wikimedia_commons_white_svg() { return new FixedUiElement(Svg.wikimedia_commons_white);}
public static wikimedia_commons_white_ui() { return new FixedUiElement(Svg.wikimedia_commons_white_img);}
- public static wikipedia = "