Huge refactoring (WIP)

This commit is contained in:
Pieter Vander Vennet 2020-10-27 01:01:34 +01:00
parent 62c4f0a928
commit 895aa132ec
55 changed files with 1177 additions and 2190 deletions

View file

@ -1,4 +1,3 @@
import {LayerDefinition} from "./LayerDefinition";
import {Layout} from "./Layout";
import {FromJSON} from "./JSON/FromJSON";
import * as bookcases from "../assets/themes/bookcases/Bookcases.json";
@ -18,14 +17,15 @@ import * as benches from "../assets/themes/benches/benches.json";
import * as charging_stations from "../assets/themes/charging_stations/charging_stations.json"
import {PersonalLayout} from "../Logic/PersonalLayout";
import {StreetWidth} from "./StreetWidth/StreetWidth";
import LayerConfig from "./JSON/LayerConfig";
import SharedLayers from "./SharedLayers";
export class AllKnownLayouts {
public static allLayers: Map<string, LayerDefinition> = undefined;
public static allLayers: Map<string, LayerConfig> = undefined;
private static GenerateCycloFix(): Layout {
const layout = FromJSON.LayoutFromJSON(cyclofix)
const layout = Layout.LayoutFromJSON(cyclofix, SharedLayers.sharedLayers)
const now = new Date();
const m = now.getMonth() + 1;
const day = new Date().getDay() + 1;
@ -33,7 +33,7 @@ export class AllKnownLayouts {
if (date === "31/10" || date === "1/11" || date === "2/11") {
// Around Halloween/Fiesta de muerte/Allerzielen, we remember the dead
layout.layers.push(
FromJSON.sharedLayers.get("ghost_bike")
SharedLayers.sharedLayers.get("ghost_bike")
);
}
@ -42,7 +42,7 @@ export class AllKnownLayouts {
}
private static GenerateBuurtNatuur(): Layout {
const layout = FromJSON.LayoutFromJSON(buurtnatuur);
const layout = Layout.LayoutFromJSON(buurtnatuur, SharedLayers.sharedLayers);
layout.enableMoreQuests = false;
layout.enableShareScreen = false;
layout.hideFromOverview = true;
@ -50,7 +50,7 @@ export class AllKnownLayouts {
}
private static GenerateBikeMonitoringStations(): Layout {
const layout = FromJSON.LayoutFromJSON(bike_monitoring_stations);
const layout = Layout.LayoutFromJSON(bike_monitoring_stations, SharedLayers.sharedLayers);
layout.hideFromOverview = true;
return layout;
}
@ -60,37 +60,36 @@ export class AllKnownLayouts {
public static layoutsList: Layout[] = [
new PersonalLayout(),
FromJSON.LayoutFromJSON(shops),
FromJSON.LayoutFromJSON(bookcases),
FromJSON.LayoutFromJSON(aed),
FromJSON.LayoutFromJSON(toilets),
FromJSON.LayoutFromJSON(artworks),
Layout.LayoutFromJSON(shops, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(bookcases, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(aed, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(toilets, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(artworks, SharedLayers.sharedLayers),
AllKnownLayouts.GenerateCycloFix(),
FromJSON.LayoutFromJSON(ghostbikes),
FromJSON.LayoutFromJSON(nature),
FromJSON.LayoutFromJSON(cyclestreets),
FromJSON.LayoutFromJSON(maps),
FromJSON.LayoutFromJSON(fritures),
FromJSON.LayoutFromJSON(benches),
FromJSON.LayoutFromJSON(charging_stations),
Layout.LayoutFromJSON(ghostbikes, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(nature, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(cyclestreets, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(maps, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(fritures, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(benches, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(charging_stations, SharedLayers.sharedLayers),
AllKnownLayouts.GenerateBuurtNatuur(),
AllKnownLayouts.GenerateBikeMonitoringStations(),
new StreetWidth(), // The ugly duckling
];
public static allSets: Map<string, Layout> = AllKnownLayouts.AllLayouts();
private static AllLayouts(): Map<string, Layout> {
this.allLayers = new Map<string, LayerDefinition>();
this.allLayers = new Map<string, LayerConfig>();
for (const layout of this.layoutsList) {
for (let i = 0; i < layout.layers.length; i++) {
let layer = layout.layers[i];
if (typeof (layer) === "string") {
layer = layout.layers[i] = FromJSON.sharedLayers.get(layer);
layer = layout.layers[i] = SharedLayers.sharedLayers.get(layer);
if(layer === undefined){
console.log("Defined layers are ", FromJSON.sharedLayers.keys())
console.log("Defined layers are ", SharedLayers.sharedLayers.keys())
throw `Layer ${layer} was not found or defined - probably a type was made`
}
}

View file

@ -1,102 +1,11 @@
import {Layout} from "../Layout";
import {LayoutConfigJson} from "./LayoutConfigJson";
import {AndOrTagConfigJson} from "./TagConfigJson";
import {And, Or, RegexTag, Tag, TagsFilter} from "../../Logic/Tags";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {TagRenderingOptions} from "../TagRenderingOptions";
import Translation from "../../UI/i18n/Translation";
import {LayerConfigJson} from "./LayerConfigJson";
import {LayerDefinition, Preset} from "../LayerDefinition";
import {TagDependantUIElementConstructor} from "../UIElementConstructor";
import Combine from "../../UI/Base/Combine";
import * as drinkingWater from "../../assets/layers/drinking_water/drinking_water.json";
import * as ghostbikes from "../../assets/layers/ghost_bike/ghost_bike.json"
import * as viewpoint from "../../assets/layers/viewpoint/viewpoint.json"
import * as bike_parking from "../../assets/layers/bike_parking/bike_parking.json"
import * as bike_repair_station from "../../assets/layers/bike_repair_station/bike_repair_station.json"
import * as birdhides from "../../assets/layers/bird_hide/birdhides.json"
import * as nature_reserve from "../../assets/layers/nature_reserve/nature_reserve.json"
import * as bike_cafes from "../../assets/layers/bike_cafe/bike_cafes.json"
import * as bike_monitoring_station from "../../assets/layers/bike_monitoring_station/bike_monitoring_station.json"
import * as cycling_themed_objects from "../../assets/layers/cycling_themed_object/cycling_themed_objects.json"
import * as bike_shops from "../../assets/layers/bike_shop/bike_shop.json"
import * as maps from "../../assets/layers/maps/maps.json"
import * as information_boards from "../../assets/layers/information_board/information_board.json"
import {Utils} from "../../Utils";
import State from "../../State";
export class FromJSON {
public static sharedLayers: Map<string, LayerDefinition> = FromJSON.getSharedLayers();
private static getSharedLayers() {
// We inject a function into state while we are busy
State.FromBase64 = FromJSON.FromBase64;
const sharedLayers = new Map<string, LayerDefinition>();
const sharedLayersList = [
FromJSON.Layer(drinkingWater),
FromJSON.Layer(ghostbikes),
FromJSON.Layer(viewpoint),
FromJSON.Layer(bike_parking),
FromJSON.Layer(bike_repair_station),
FromJSON.Layer(bike_monitoring_station),
FromJSON.Layer(birdhides),
FromJSON.Layer(nature_reserve),
FromJSON.Layer(bike_cafes),
FromJSON.Layer(cycling_themed_objects),
FromJSON.Layer(bike_shops),
FromJSON.Layer(maps),
FromJSON.Layer(information_boards)
];
for (const layer of sharedLayersList) {
sharedLayers.set(layer.id, layer);
}
return sharedLayers;
}
public static FromBase64(layoutFromBase64: string): Layout {
return FromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64)));
}
public static LayoutFromJSON(json: LayoutConfigJson): Layout {
const tr = FromJSON.Translation;
const layers = json.layers.map(FromJSON.Layer);
const roaming: TagDependantUIElementConstructor[] = json.roamingRenderings?.map((tr, i) => FromJSON.TagRendering(tr, "Roaming rendering "+i)) ?? [];
for (const layer of layers) {
layer.elementsToShow.push(...roaming);
}
const layout = new Layout(
json.id,
typeof (json.language) === "string" ? [json.language] : json.language,
tr(json.title ?? "Title not defined"),
layers,
json.startZoom,
json.startLat,
json.startLon,
new Combine(["<h3>", tr(json.title), "</h3>", tr(json.description)]),
undefined,
undefined,
tr(json.descriptionTail)
);
layout.defaultBackground = json.defaultBackgroundId ?? "osm";
layout.widenFactor = json.widenFactor ?? 0.07;
layout.icon = json.icon;
layout.maintainer = json.maintainer;
layout.version = json.version;
layout.socialImage = json.socialImage;
layout.description = tr(json.shortDescription) ?? tr(json.description)?.FirstSentence();
layout.changesetMessage = json.changesetmessage;
return layout;
}
public static Translation(json: string | any): Translation {
if (json === undefined) {
@ -122,104 +31,6 @@ export class FromJSON {
return transl;
}
public static TagRendering(json: TagRenderingConfigJson | string, propertyeName: string): TagDependantUIElementConstructor {
return FromJSON.TagRenderingWithDefault(json, propertyeName, undefined);
}
public static TagRenderingWithDefault(json: TagRenderingConfigJson | string, propertyName, defaultValue: string): TagDependantUIElementConstructor {
if (json === undefined) {
if(defaultValue !== undefined){
return FromJSON.TagRendering(defaultValue, propertyName);
}
throw `Tagrendering ${propertyName} is undefined...`
}
if (typeof json === "string") {
switch (json) {
case "pictures": {
json = "{image_carousel()}{image_upload()}";
break;
}
case "images": {
json = "{image_carousel()}{image_upload()}";
}
}
return new TagRenderingOptions({
freeform: {
key: "id",
renderTemplate: json,
template: "$$$"
}
});
}
// It's the question that drives us, neo
const question = FromJSON.Translation(json.question);
let template = FromJSON.Translation(json.render);
let freeform = undefined;
if (json.freeform?.key && json.freeform.key !== "") {
// Setup the freeform
if (template === undefined) {
console.error("Freeform.key is defined, but render is not. This is not allowed.", json)
throw `Freeform is defined in tagrendering ${propertyName}, but render is not. This is not allowed.`
}
freeform = {
template: `$${json.freeform.type ?? "string"}$`,
renderTemplate: template,
key: json.freeform.key
};
if (json.freeform.addExtraTags) {
freeform.extraTags = new And(json.freeform.addExtraTags.map(FromJSON.SimpleTag))
}
} else if (json.render) {
// Template (aka rendering) is defined, but freeform.key is not. We allow an input as string
freeform = {
template: undefined, // Template to ask is undefined -> we block asking for this key
renderTemplate: template,
key: "id" // every object always has an id
}
}
const mappings = json.mappings?.map((mapping, i) => {
const k = FromJSON.Tag(mapping.if, `IN mapping #${i} of tagrendering ${propertyName}`)
if (question !== undefined && !mapping.hideInAnswer && !k.isUsableAsAnswer()) {
throw `Invalid mapping in ${propertyName}.${i}: this mapping uses a regex tag or an OR, but is also answerable. Either mark 'Not an answer option' or only use '=' to map key/values.`
}
return {
k: k,
txt: FromJSON.Translation(mapping.then),
hideInAnswer: mapping.hideInAnswer
};
}
);
if (template === undefined && (mappings === undefined || mappings.length === 0)) {
console.error(`Empty tagrendering detected in ${propertyName}: no mappings nor template given`, json)
throw `Empty tagrendering ${propertyName} detected: no mappings nor template given`
}
let rendering = new TagRenderingOptions({
question: question,
freeform: freeform,
mappings: mappings,
multiAnswer: json.multiAnswer
});
if (json.condition) {
const condition = FromJSON.Tag(json.condition, `In tagrendering ${propertyName}.condition`);
return rendering.OnlyShowIf(condition);
}
return rendering;
}
public static SimpleTag(json: string): Tag {
const tag = Utils.SplitFirst(json, "=");
return new Tag(tag[0], tag[1]);
@ -227,7 +38,7 @@ export class FromJSON {
public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter {
if(json === undefined){
throw "Error while parsing a tag: nothing defined. Make sure all the tags are defined and at least one tag is present in a complex expression"
throw `Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`
}
if (typeof (json) == "string") {
const tag = json as string;
@ -286,120 +97,4 @@ export class FromJSON {
return new Or(json.or.map(t => FromJSON.Tag(t, context)));
}
}
public static Layer(json: LayerConfigJson | string): LayerDefinition {
if (typeof (json) === "string") {
const cached = FromJSON.sharedLayers.get(json);
if (cached) {
return cached;
}
throw `Layer ${json} not yet loaded...`
}
try {
return FromJSON.LayerUncaught(json);
} catch (e) {
throw `While parsing layer ${json.id}: ${e}`
}
}
private static LayerUncaught(json: LayerConfigJson): LayerDefinition {
const tr = FromJSON.Translation;
const overpassTags = FromJSON.Tag(json.overpassTags, "overpasstags for layer "+json.id);
const icon = FromJSON.TagRenderingWithDefault(json.icon, "icon", "./assets/bug.svg");
const iconSize = FromJSON.TagRenderingWithDefault(json.iconSize, "iconSize", "40,40,center");
const color = FromJSON.TagRenderingWithDefault(json.color, "color", "#0000ff");
const width = FromJSON.TagRenderingWithDefault(json.width, "width", "10");
if (json.title === "Layer") {
json.title = {};
}
let title = FromJSON.TagRendering(json.title, "Popup title");
let tagRenderingDefs = json.tagRenderings ?? [];
let hasImageElement = false;
for (const tagRenderingDef of tagRenderingDefs) {
if (typeof tagRenderingDef !== "string") {
continue;
}
let str = tagRenderingDef as string;
if (tagRenderingDef.indexOf("images") >= 0 || str.indexOf("pictures") >= 0) {
hasImageElement = true;
break;
}
}
if (!hasImageElement) {
tagRenderingDefs = ["images", ...tagRenderingDefs];
}
let tagRenderings = tagRenderingDefs.map((tr, i) => FromJSON.TagRendering(tr, "Tagrendering #"+i));
const renderTags = {"id": "node/-1"}
const presets: Preset[] = json?.presets?.map(preset => {
return ({
title: tr(preset.title),
description: tr(preset.description),
tags: preset.tags.map(FromJSON.SimpleTag)
});
}) ?? [];
function style(tags) {
const iconSizeStr =
iconSize.GetContent(tags).txt.split(",");
const iconwidth = Number(iconSizeStr[0]);
const iconheight = Number(iconSizeStr[1]);
const iconmode = iconSizeStr[2];
const iconAnchor = [iconwidth / 2, iconheight / 2] // x, y
// If iconAnchor is set to [0,0], then the top-left of the icon will be placed at the geographical location
if (iconmode.indexOf("left") >= 0) {
iconAnchor[0] = 0;
}
if (iconmode.indexOf("right") >= 0) {
iconAnchor[0] = iconwidth;
}
if (iconmode.indexOf("top") >= 0) {
iconAnchor[1] = 0;
}
if (iconmode.indexOf("bottom") >= 0) {
iconAnchor[1] = iconheight;
}
// the anchor is always set from the center of the point
// x, y with x going right and y going down if the values are bigger
const popupAnchor = [0, 3 - iconAnchor[1]];
return {
color: color.GetContent(tags).txt,
weight: width.GetContent(tags).txt,
icon: {
iconUrl: icon.GetContent(tags).txt,
iconSize: [iconwidth, iconheight],
popupAnchor: popupAnchor,
iconAnchor: iconAnchor
},
}
}
const layer = new LayerDefinition(
json.id,
{
name: tr(json.name),
description: tr(json.description),
icon: icon.GetContent(renderTags).txt,
overpassFilter: overpassTags,
title: title,
minzoom: json.minzoom,
presets: presets,
elementsToShow: tagRenderings,
style: style,
wayHandling: json.wayHandling
}
);
layer.maxAllowedOverlapPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
return layer;
}
}

View file

@ -0,0 +1,114 @@
import Translation from "../../UI/i18n/Translation";
import TagRenderingConfig from "./TagRenderingConfig";
import {Tag, TagsFilter} from "../../Logic/Tags";
import {LayerConfigJson} from "./LayerConfigJson";
import Translations from "../../UI/i18n/Translations";
import {FromJSON} from "./FromJSON";
import SharedTagRenderings from "../SharedTagRenderings";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
export default class LayerConfig {
id: string;
name: Translation
description: Translation;
overpassTags: TagsFilter;
minzoom: number;
title: TagRenderingConfig;
titleIcons: TagRenderingConfig[];
icon?: TagRenderingConfig;
iconSize?: TagRenderingConfig;
color?: TagRenderingConfig;
width?: TagRenderingConfig;
wayHandling: number;
static WAYHANDLING_DEFAULT = 0;
static WAYHANDLING_CENTER_ONLY = 1;
static WAYHANDLING_CENTER_AND_WAY = 2;
hideUnderlayingFeaturesMinPercentage?: number;
presets: {
title: Translation,
tags: Tag[],
description?: Translation,
}[];
tagRenderings: TagRenderingConfig [];
constructor(json: LayerConfigJson, 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.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),
tags: pr.tags.map(t => FromJSON.SimpleTag(t)),
description: Translations.T(pr.description)
}))
/**
* Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig
* A string is interpreted as a name to call
* @param tagRenderings
*/
function trs(tagRenderings?: (string | TagRenderingConfigJson)[]) {
if (tagRenderings === undefined) {
return [];
}
return tagRenderings.map(
(renderingJson, i) => {
if (typeof renderingJson === "string") {
const shared = SharedTagRenderings.SharedTagRendering[renderingJson];
if (shared !== undefined) {
return shared;
}
throw `Predefined tagRendering ${renderingJson} not found in ${context}`;
}
return new TagRenderingConfig(renderingJson, `${context}.tagrendering[${i}]`);
});
}
this.tagRenderings = trs(json.tagRenderings);
this.titleIcons = trs(json.titleIcons ?? ["wikipedialink","osmlink"]);
function tr(key, deflt) {
const v = json[key];
if (v === undefined) {
return new TagRenderingConfig(deflt);
}
if (typeof v === "string") {
const shared = SharedTagRenderings.SharedTagRendering[v];
if (shared) {
console.log("Got shared TR:", v, "-->", shared)
return shared;
}
}
return new TagRenderingConfig(v, context + "." + key);
}
this.title = tr("title", "");
this.icon = tr("icon", "./assets/bug.svg");
this.iconSize = tr("iconSize", "40,40,center");
this.color = tr("color", "#0000ff");
this.width = tr("width", "7");
}
}

View file

@ -37,9 +37,11 @@ export interface LayerConfigJson {
/**
* The title shown in a popup for elements of this layer
* The title shown in a popup for elements of this layer.
*/
title: string | TagRenderingConfigJson;
titleIcons?: (string | TagRenderingConfigJson)[];
/**
* The icon for an element.

View file

@ -0,0 +1,112 @@
import Translation from "../../UI/i18n/Translation";
import {TagsFilter} from "../../Logic/Tags";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import Translations from "../../UI/i18n/Translations";
import {FromJSON} from "./FromJSON";
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
/***
* The parsed version of TagRenderingConfigJSON
* Identical data, but with some methods and validation
*/
export default class TagRenderingConfig {
render?: Translation;
question?: Translation;
condition?: TagsFilter;
freeform?: {
key: string,
type: string,
addExtraTags: TagsFilter[];
};
multiAnswer: boolean;
mappings?: {
if: TagsFilter,
then: Translation
hideInAnswer: boolean
}[]
constructor(json: string | TagRenderingConfigJson, context?: string) {
if(json === undefined){
throw "Initing a TagRenderingConfig with undefined in "+context;
}
if (typeof json === "string") {
this.render = Translations.T(json);
this.multiAnswer = false;
return;
}
this.render = Translations.T(json.render);
this.question = Translations.T(json.question);
this.condition = FromJSON.Tag(json.condition ?? {"and": []}, `${context}.condition`);
if (json.freeform) {
this.freeform = {
key: json.freeform.key,
type: json.freeform.type ?? "string",
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? []
}
if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) {
throw `Freeform.key ${this.freeform.key} is an invalid type`
}
}
this.multiAnswer = json.multiAnswer ?? false
if (json.mappings) {
this.mappings = json.mappings.map((mapping, i) => {
if (mapping.then === undefined) {
throw "Invalid mapping: if without body"
}
return {
if: FromJSON.Tag(mapping.if, `${context}.mapping[${i}]`),
then: Translations.T(mapping.then),
hideInAnswer: mapping.hideInAnswer ?? false
};
});
}
if (this.question && this.freeform?.key === undefined && this.mappings === undefined) {
throw `A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}`
}
if (json.multiAnswer) {
if ((this.mappings?.length ?? 0) === 0) {
throw "MultiAnswer is set, but no mappings are defined"
}
}
}
/**
* Gets the correct rendering value (or undefined if not known)
* @constructor
*/
public GetRenderValue(tags: any): Translation {
if (this.mappings !== undefined && !this.multiAnswer) {
for (const mapping of this.mappings) {
if (mapping.if === undefined) {
return mapping.then;
}
if (mapping.if.matchesProperties(tags)) {
return mapping.then;
}
}
}
if (this.freeform?.key === undefined){
return this.render;
}
if (tags[this.freeform.key] !== undefined) {
return this.render;
}
return undefined;
}
}

View file

@ -1,133 +0,0 @@
import {Tag, TagsFilter} from "../Logic/Tags";
import {UIElement} from "../UI/UIElement";
import {TagDependantUIElementConstructor} from "./UIElementConstructor";
import {TagRenderingOptions} from "./TagRenderingOptions";
import Translation from "../UI/i18n/Translation";
export interface Preset {
tags: Tag[],
title: string | UIElement,
description?: string | UIElement,
icon?: string | TagRenderingOptions
}
export class LayerDefinition {
/**
* This name is used in the 'hide or show this layer'-buttons
*/
name: string | Translation;
/***
* This is shown under the 'add new' button to indicate what kind of feature one is adding.
*/
description: string | Translation
/**
* These tags are added whenever a new point is added by the user on the map.
* This is the ideal place to add extra info, such as "fixme=added by MapComplete, geometry should be checked"
*/
presets: Preset[]
/**
* Not really used anymore
* This is meant to serve as icon in the buttons
*/
icon: string | TagRenderingOptions;
/**
* Only show this layer starting at this zoom level
*/
minzoom: number;
/**
* This tagfilter is used to query overpass.
* Examples are:
*
* new Tag("amenity","drinking_water")
*
* or a query for bicycle pumps which have two tagging schemes:
* new Or([
* new Tag("service:bicycle:pump","yes") ,
* new And([
* new Tag("amenity","compressed_air"),
* new Tag("bicycle","yes")])
* ])
*/
overpassFilter: TagsFilter;
public readonly id: string;
/**
* This UIElement is rendered as title element in the popup
*/
title: TagDependantUIElementConstructor | UIElement | string;
/**
* These are the questions/shown attributes in the popup
*/
elementsToShow: TagDependantUIElementConstructor[];
/**
* A simple styling for the geojson element
* color is the color for areas and ways
* icon is the Leaflet icon
* Note that this is passed entirely to leaflet, so other leaflet attributes work too
*/
style: (tags: any) => {
color: string,
weight?: number,
icon: {
iconUrl: string,
iconSize?: [number, number],
popupAnchor?: [number,number],
iconAnchor?: [number,number]
},
};
/**
* If an object of the next layer is contained for this many percent in this feature, it is eaten and not shown
*/
maxAllowedOverlapPercentage: number = undefined;
/**
* If true, then ways (and polygons) will be converted to a 'point' at the center instead before further processing
*/
wayHandling: number = 0;
static WAYHANDLING_DEFAULT = 0;
static WAYHANDLING_CENTER_ONLY = 1;
static WAYHANDLING_CENTER_AND_WAY = 2;
constructor(id: string, options: {
name: string | Translation,
description: string | Translation,
presets: Preset[],
icon: string,
minzoom: number,
overpassFilter: TagsFilter,
title?: TagDependantUIElementConstructor,
elementsToShow?: TagDependantUIElementConstructor[],
maxAllowedOverlapPercentage?: number,
wayHandling?: number,
widenFactor?: number,
style?: (tags: any) => {
color: string,
icon: any
}
} = undefined) {
this.id = id;
if (options === undefined) {
return;
}
this.name = options.name;
this.description = options.description;
this.maxAllowedOverlapPercentage = options.maxAllowedOverlapPercentage ?? 0;
this.presets = options.presets;
this.icon = options.icon;
this.minzoom = options.minzoom;
this.overpassFilter = options.overpassFilter;
this.title = options.title;
this.elementsToShow = options.elementsToShow;
this.style = options.style;
this.wayHandling = options.wayHandling ?? LayerDefinition.WAYHANDLING_DEFAULT;
}
}

View file

@ -1,9 +1,12 @@
import {LayerDefinition} from "./LayerDefinition";
import {UIElement} from "../UI/UIElement";
import Translations from "../UI/i18n/Translations";
import Combine from "../UI/Base/Combine";
import State from "../State";
import Translation from "../UI/i18n/Translation";
import LayerConfig from "./JSON/LayerConfig";
import {LayoutConfigJson} from "./JSON/LayoutConfigJson";
import TagRenderingConfig from "./JSON/TagRenderingConfig";
import {FromJSON} from "./JSON/FromJSON";
/**
* A layout is a collection of settings of the global view (thus: welcome text, title, selection of layers).
@ -23,7 +26,7 @@ export class Layout {
*/
public customCss: string = undefined;
public layers: (LayerDefinition | string)[];
public layers: LayerConfig[];
public welcomeMessage: UIElement;
public gettingStartedPlzLogin: UIElement;
public welcomeBackMessage: UIElement;
@ -52,11 +55,51 @@ export class Layout {
public widenFactor: number = 0.07;
public defaultBackground: string = "osm";
public static LayoutFromJSON(json: LayoutConfigJson, sharedLayers): Layout {
const tr = FromJSON.Translation;
const layers = json.layers.map(jsonLayer => {
if(typeof jsonLayer === "string"){
return sharedLayers[jsonLayer];
}
return new LayerConfig(jsonLayer, "theme."+json.id);
});
const roaming: TagRenderingConfig[] = json.roamingRenderings?.map((tr, i) =>
new TagRenderingConfig(tr, `theme.${json.id}.roamingRendering[${i}]`)) ?? [];
for (const layer of layers) {
layer.tagRenderings.push(...roaming);
}
const layout = new Layout(
json.id,
typeof (json.language) === "string" ? [json.language] : json.language,
tr(json.title ?? "Title not defined"),
layers,
json.startZoom,
json.startLat,
json.startLon,
new Combine(["<h3>", tr(json.title), "</h3>", tr(json.description)]),
undefined,
undefined,
tr(json.descriptionTail)
);
layout.defaultBackground = json.defaultBackgroundId ?? "osm";
layout.widenFactor = json.widenFactor ?? 0.07;
layout.icon = json.icon;
layout.maintainer = json.maintainer;
layout.version = json.version;
layout.socialImage = json.socialImage;
layout.description = tr(json.shortDescription) ?? tr(json.description)?.FirstSentence();
layout.changesetMessage = json.changesetmessage;
return layout;
}
constructor(
id: string,
supportedLanguages: string[],
title: Translation | string,
layers: (LayerDefinition | string)[],
layers: LayerConfig[],
startzoom: number,
startLat: number,
startLon: number,

View file

@ -1,98 +0,0 @@
import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor";
import {TagsFilter, TagUtils} from "../Logic/Tags";
import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
/**
* Wrapper around another TagDependandElement, which only shows if the filters match
*/
export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{
private readonly _tagsFilter: TagsFilter;
private readonly _embedded: TagDependantUIElementConstructor;
constructor(tagsFilter: TagsFilter, embedded: TagDependantUIElementConstructor) {
this._tagsFilter = tagsFilter;
this._embedded = embedded;
}
construct(tags: UIEventSource<any>): TagDependantUIElement {
return new OnlyShowIf(tags,
this._embedded.construct(tags),
this._tagsFilter);
}
IsKnown(properties: any): boolean {
if(!this.Matches(properties)){
return true;
}
return this._embedded.IsKnown(properties);
}
IsQuestioning(properties: any): boolean {
if(!this.Matches(properties)){
return false;
}
return this._embedded.IsQuestioning(properties);
}
GetContent(tags: any): Translation {
if(!this.IsKnown(tags)){
return undefined;
}
return this._embedded.GetContent(tags);
}
private Matches(properties: any) : boolean{
return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties));
}
}
class OnlyShowIf extends UIElement implements TagDependantUIElement {
private readonly _embedded: TagDependantUIElement;
private readonly _filter: TagsFilter;
constructor(
tags: UIEventSource<any>,
embedded: TagDependantUIElement,
filter: TagsFilter) {
super(tags);
this._filter = filter;
this._embedded = embedded;
}
private Matches() : boolean{
return this._filter.matches(TagUtils.proprtiesToKV(this._source.data));
}
InnerRender(): string {
if (this.Matches()) {
return this._embedded.Render();
} else {
return "";
}
}
IsKnown(): boolean {
if(!this.Matches()){
return false;
}
return this._embedded.IsKnown();
}
IsSkipped(): boolean {
if(!this.Matches()){
return false;
}
return this._embedded.IsSkipped();
}
IsQuestioning(): boolean {
if(!this.Matches()){
return false;
}
return this._embedded.IsQuestioning();
}
}

View file

@ -1,29 +0,0 @@
import {Img} from "../../UI/Img";
import {RegexTag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class OsmLink extends TagRenderingOptions {
static options = {
freeform: {
key: "id",
template: "$$$",
renderTemplate:
"<span class='osmlink'><a href='https://osm.org/{id}' target='_blank'>" +
Img.osmAbstractLogo +
"</a></span>",
placeholder: "",
},
mappings: [
{k: new RegexTag("id", /node\/-.+/), txt: ""}
]
}
constructor() {
super(OsmLink.options);
}
}

View file

@ -1,50 +0,0 @@
import {TagRenderingOptions} from "../TagRenderingOptions";
export class WikipediaLink extends TagRenderingOptions {
private static FixLink(value: string): string {
if (value === undefined) {
return undefined;
}
// @ts-ignore
if (value.startsWith("https")) {
return value;
} else {
const splitted = value.split(":");
const language = splitted[0];
splitted.shift();
const page = splitted.join(":");
return 'https://' + language + '.wikipedia.org/wiki/' + page;
}
}
static options = {
priority: 10,
// question: "Wat is het overeenstemmende wkipedia-artikel?",
tagsPreprocessor: (tags) => {
if (tags.wikipedia !== undefined) {
tags.wikipedia = WikipediaLink.FixLink(tags.wikipedia);
}
},
freeform: {
key: "wikipedia",
template: "$$$",
renderTemplate:
"<a href='{wikipedia}' target='_blank'>" +
"<img style='width: 24px;height: 24px;' src='./assets/wikipedia.svg' alt='wikipedia'>" +
"</a>",
placeholder: ""
},
}
constructor() {
super(WikipediaLink.options);
}
}

View file

@ -0,0 +1,50 @@
import * as drinkingWater from "../assets/layers/drinking_water/drinking_water.json";
import * as ghostbikes from "../assets/layers/ghost_bike/ghost_bike.json"
import * as viewpoint from "../assets/layers/viewpoint/viewpoint.json"
import * as bike_parking from "../assets/layers/bike_parking/bike_parking.json"
import * as bike_repair_station from "../assets/layers/bike_repair_station/bike_repair_station.json"
import * as birdhides from "../assets/layers/bird_hide/birdhides.json"
import * as nature_reserve from "../assets/layers/nature_reserve/nature_reserve.json"
import * as bike_cafes from "../assets/layers/bike_cafe/bike_cafes.json"
import * as bike_monitoring_station from "../assets/layers/bike_monitoring_station/bike_monitoring_station.json"
import * as cycling_themed_objects from "../assets/layers/cycling_themed_object/cycling_themed_objects.json"
import * as bike_shops from "../assets/layers/bike_shop/bike_shop.json"
import * as maps from "../assets/layers/maps/maps.json"
import * as information_boards from "../assets/layers/information_board/information_board.json"
import LayerConfig from "./JSON/LayerConfig";
export default class SharedLayers {
public static sharedLayers: Map<string, LayerConfig> = SharedLayers.getSharedLayers();
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(maps, "shared_layers"),
new LayerConfig(information_boards, "shared_layers")
];
const sharedLayers = new Map<string, LayerConfig>();
for (const layer of sharedLayersList) {
sharedLayers.set(layer.id, layer);
sharedLayers[layer.id] = layer;
}
return sharedLayers;
}
}

View file

@ -0,0 +1,20 @@
import * as questions from "../assets/questions/questions.json";
import TagRenderingConfig from "./JSON/TagRenderingConfig";
export default class SharedTagRenderings {
public static SharedTagRendering = SharedTagRenderings.generatedSharedFields();
private static generatedSharedFields() {
const dict = {}
for (const key in questions) {
try {
dict[key] = new TagRenderingConfig(questions[key])
} catch (e) {
console.error("COULD NOT PARSE", key, " FROM QUESTIONS:", e)
}
}
return dict;
}
}

View file

@ -1,107 +0,0 @@
import {Layout} from "../Layout";
import {Widths} from "./Widths";
export class StreetWidth extends Layout{
private static meetMethode = `
We meten de ruimte die gedeeld wordt door auto's, fietsers en -in sommige gevallen- voetgangers.
We meten dus van _verhoogde_ stoeprand tot stoeprand omdat dit de ruimte is die wordt gedeeld door auto's en fietsers.
Daarnaast zoeken we ook een smaller stuk van de weg waar dat smallere stuk toch minstens 2m zo smal blijft.
Een obstakel (zoals een trap, elektriciteitkast) negeren we omdat dit de meting te fel beinvloed.
In een aantal straten is er geen verhoogde stoep. In dit geval meten we van muur tot muur, omdat dit de gedeelde ruimte is.
We geven ook altijd een aanduiding of er al dan niet een voetpad aanwezig (en aan welke kant indien er maar één is), want indien er geen is heeft de voetganger ook ruimte nodig.
(In sommige straten zijn er wel 'voetpadsuggesties' door een meter in andere kasseien te leggen, bv. met een kleurtje. Dit rekenen we niet als voetpad.
Ook het parkeren van auto's wordt opgemeten.
Als er een parallele parkeerstrook is, dan duiden we dit aan en nemen we de parkeerstrook mee in de straatbreedte.
Als er een witte lijn is, dan negeren we dit. Deze witte lijnen duiden immers vaak een smalle parkeerplaats aan - bv. 1.6m.
Een auto is tegenwoordig al snel 1.8m tot zelfs 2.0m, dus dan springt die auto gemakkelijk 20 tot 30cm uit op de baan.
Staan de auto's schuin geparkeerd of dwarsgeparkeerd?
Ook hier kan men het argument maken dat auto's er soms overspringen, maar dat is hier te variabel om in kaart te brengen.
Daarnaast gebeurt het minder dat auto's overspringen én zijn deze gevallen relatief zeldzaam in de binnenstad.
Concreet:
- Sla de 'parkeren'-vraag over
- Maak een foto en stuur die door naar Pieter (+ vermelding straatnaam of dergelijke)
- Meet de breedte vanaf de afbakening van de parkeerstrook.
Ook bij andere lastige gevallen: maak een foto en vraag Pieter
Instellen van de lasermeter
===========================
1) Zet de lasermeter aan met de rode knop
2) Het icoontje linksboven indiceert vanaf waar de laser meet - de voorkant of de achterkant van het apparaatje.
Dit kan aangepast worden met het knopje links-onderaan.
Kies wat je het liefste hebt
3) Het icoontje bovenaan-midden indiceert de stand van de laser: directe afstand, of afstand over de grond.
Dit MOET een driehoekje tonen.
Indien niet: duw op het knopje links-bovenaan totdat dit een rechte driehoek toont
4) Duw op de rode knop. Het lasertje gaat branden
5) Hou het meetbakje boven de stoeprand (met de juiste rand), richt de laser op de andere stoep
6) Duw opnieuw op de rode knop om te meten (de laser flikkert en gaat uit)
7) Lees de afstand af op het scherm. Let op: in 'hoekstand' is dit niet de onderste waarde, maar die er net boven.
`
constructor() {
super( "width",
["nl"],
"Straatbreedtes in Brugge",
[new Widths(
2,
1.5,
0.75
)],
15,
51.20875,
3.22435,
"<h3>De straat is opgebruikt</h3>" +
"<p>Er is steeds meer druk op de openbare ruimte. Voetgangers, fietsers, steps, auto's, bussen, bestelwagens, buggies, cargobikes, ... willen allemaal hun deel van de openbare ruimte.</p>" +
"" +
"<p>In deze studie nemen we Brugge onder de loep en kijken we hoe breed elke straat is én hoe breed elke straat zou moeten zijn voor een veilig én vlot verkeer.</p>" +
"<h3>Legende</h3>" +
"<span style='background: red'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat te smal voor veilig verkeer<br/>"+
"<span style='background: #0f0'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat is breed genoeg veilig verkeer<br/>"+
"<span style='background: orange'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat zonder voetpad, te smal als ook voetgangers plaats krijgen<br/>"+
"<span style='background: lightgrey'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Woonerf, autoluw, autoloos of enkel plaatselijk verkeer<br/>" +
"<br/>" +
"<br/>" +
"Een gestippelde lijn is een straat waar ook voor fietsers éénrichtingsverkeer geldt.<br/>" +
"Klik op een straat om meer informatie te zien."+
"<h3>Hoe gaan we verder?</h3>" +
"Verschillende ingrepen kunnen de stad teruggeven aan de inwoners en de stad leefbaarder en levendiger maken.<br/>" +
"Denk aan:" +
"<ul>" +
"<li>De autovrije zone's uitbreiden</li>" +
"<li>De binnenstad fietszone maken</li>" +
"<li>Het aantal woonerven uitbreiden</li>" +
"<li>Grotere auto's meer belasten - ze nemen immers meer parkeerruimte in.</li>" +
"<li>Laat toeristen verplicht parkeren onder het zand; een (fiets)taxi kan hen naar hun hotel brengen</li>" +
"<li>Voorzie in elke straat enkele parkeerplaatsen voor kortparkeren. Zo kunnen leveringen, iemand afzetten,... gebeuren zonder op het voetpad en fietspad te parkeren</li>" +
"</ul>");
this.icon = "./assets/bug.svg";
this.enableSearch = false;
this.enableUserBadge = false;
this.enableAdd = false;
this.hideFromOverview = true;
this.enableMoreQuests = false;
this.enableShareScreen = false;
this.defaultBackground = "Stadia.AlidadeSmoothDark"
this.enableBackgroundLayers = false;
}
}

View file

@ -1,312 +0,0 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Or, RegexTag, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
import {FromJSON} from "../JSON/FromJSON";
export class Widths extends LayerDefinition {
private readonly cyclistWidth: number;
private readonly carWidth: number;
private readonly pedestrianWidth: number;
private readonly _bothSideParking = new Tag("parking:lane:both", "parallel");
private readonly _noSideParking = new Tag("parking:lane:both", "no_parking");
private readonly _otherParkingMode =
new Or([
new Tag("parking:lane:both", "perpendicular"),
new Tag("parking:lane:left", "perpendicular"),
new Tag("parking:lane:right", "perpendicular"),
new Tag("parking:lane:both", "diagonal"),
new Tag("parking:lane:left", "diagonal"),
new Tag("parking:lane:right", "diagonal"),
])
private readonly _leftSideParking =
new And([new Tag("parking:lane:left", "parallel"), new Tag("parking:lane:right", "no_parking")]);
private readonly _rightSideParking =
new And([new Tag("parking:lane:right", "parallel"), new Tag("parking:lane:left", "no_parking")]);
private _sidewalkBoth = new Tag("sidewalk", "both");
private _sidewalkLeft = new Tag("sidewalk", "left");
private _sidewalkRight = new Tag("sidewalk", "right");
private _sidewalkNone = new Tag("sidewalk", "none");
private readonly _oneSideParking = new Or([this._leftSideParking, this._rightSideParking]);
private readonly _notCarfree =
FromJSON.Tag({"and":[
"highway!~pedestrian|living_street",
"access!~destination",
"motor_vehicle!~destination|no"
]});
private calcProps(properties) {
let parkingStateKnown = true;
let parallelParkingCount = 0;
if (this._oneSideParking.matchesProperties(properties)) {
parallelParkingCount = 1;
} else if (this._bothSideParking.matchesProperties(properties)) {
parallelParkingCount = 2;
} else if (this._noSideParking.matchesProperties(properties)) {
parallelParkingCount = 0;
} else if (this._otherParkingMode.matchesProperties(properties)) {
parallelParkingCount = 0;
} else {
parkingStateKnown = false;
console.log("No parking data for ", properties.name, properties.id, properties)
}
let pedestrianFlowNeeded;
if (this._sidewalkBoth.matchesProperties(properties)) {
pedestrianFlowNeeded = 0;
} else if (this._sidewalkNone.matchesProperties(properties)) {
pedestrianFlowNeeded = 2;
} else if (this._sidewalkLeft.matchesProperties(properties) || this._sidewalkRight.matches(properties)) {
pedestrianFlowNeeded = 1;
} else {
pedestrianFlowNeeded = -1;
}
let onewayCar = properties.oneway === "yes";
let onewayBike = properties["oneway:bicycle"] === "yes" ||
(onewayCar && properties["oneway:bicycle"] === undefined)
let cyclingAllowed =
!(properties.bicycle === "use_sidepath"
|| properties.bicycle === "no");
let carWidth = (onewayCar ? 1 : 2) * this.carWidth;
let cyclistWidth = 0;
if (cyclingAllowed) {
cyclistWidth = (onewayBike ? 1 : 2) * this.cyclistWidth;
}
const width = parseFloat(properties["width:carriageway"]);
const targetWidthIgnoringPedestrians =
carWidth +
cyclistWidth +
parallelParkingCount * this.carWidth;
const targetWidth = targetWidthIgnoringPedestrians + Math.max(0, pedestrianFlowNeeded) * this.pedestrianWidth;
return {
parkingLanes: parallelParkingCount,
parkingStateKnown: parkingStateKnown,
width: width,
targetWidth: targetWidth,
targetWidthIgnoringPedestrians: targetWidthIgnoringPedestrians,
onewayBike: onewayBike,
pedestrianFlowNeeded: pedestrianFlowNeeded,
cyclingAllowed: cyclingAllowed
}
}
constructor(carWidth: number,
cyclistWidth: number,
pedestrianWidth: number) {
super("width");
this.carWidth = carWidth;
this.cyclistWidth = cyclistWidth;
this.pedestrianWidth = pedestrianWidth;
this.minzoom = 12;
function r(n: number) {
const pre = Math.floor(n);
const post = Math.floor((n * 10) % 10);
return "" + pre + "." + post;
}
this.name = "widths";
this.overpassFilter = new RegexTag("width:carriageway", /.*/);
this.title = new TagRenderingOptions({
freeform: {
renderTemplate: "{name}",
template: "$$$",
key: "name"
}
})
const self = this;
this.style = (properties) => {
let c = "#f00";
const props = self.calcProps(properties);
if (props.width >= props.targetWidthIgnoringPedestrians) {
c = "#fa0"
}
if (props.width >= props.targetWidth || !props.cyclingAllowed) {
c = "#0c0";
}
if (!props.parkingStateKnown && properties["note:width:carriageway"] === undefined) {
c = "#f0f"
}
if (!this._notCarfree.matchesProperties(properties)) {
c = "#aaa";
}
// Mark probably wrong data
if (props.width > 15) {
c = "#f0f"
}
let dashArray = undefined;
if (props.onewayBike) {
dashArray = [5, 6]
}
return {
icon: null,
color: c,
weight: 5,
dashArray: dashArray
}
}
this.elementsToShow = [
new TagRenderingOptions({
mappings: [
{
k: this._bothSideParking,
txt: "Auto's kunnen langs beide zijden parkeren.<br+>Dit gebruikt <b>" + r(this.carWidth * 2) + "m</b><br/>"
},
{
k: this._oneSideParking,
txt: "Auto's kunnen langs één kant parkeren.<br/>Dit gebruikt <b>" + r(this.carWidth) + "m</b><br/>"
},
{
k: this._otherParkingMode,
txt: "Deze straat heeft dwarsparkeren of diagonaalparkeren aan minstens één zijde. Deze parkeerruimte is niet opgenomen in de straatbreedte."
},
{k: this._noSideParking, txt: "Auto's mogen hier niet parkeren"},
],
freeform: {
key: "note:width:carriageway",
renderTemplate: "{note:width:carriageway}",
template: "$$$",
}
}).OnlyShowIf(this._notCarfree),
new TagRenderingOptions({
mappings: [
{
k: this._sidewalkNone,
txt: "Deze straat heeft geen voetpaden. Voetgangers hebben hier <b>" + r(this.pedestrianWidth * 2) + "m</b> nodig"
},
{
k: new Or([this._sidewalkLeft, this._sidewalkRight]),
txt: "Deze straat heeft een voetpad aan één kant. Voetgangers hebben hier <b>" + r(this.pedestrianWidth) + "m</b> nodig"
},
{k: this._sidewalkBoth, txt: "Deze straat heeft voetpad aan beide zijden."},
],
freeform: {
key: "note:width:carriageway",
renderTemplate: "{note:width:carriageway}",
template: "$$$",
}
}).OnlyShowIf(this._notCarfree),
new TagRenderingOptions({
mappings: [
{
k: new Tag("bicycle", "use_sidepath"),
txt: "Er is een afgescheiden, verplicht te gebruiken fietspad. Fietsen op dit wegsegment hoeft dus niet"
},
{
k: new Tag("bicycle", "no"),
txt: "Fietsen is hier niet toegestaan"
},
{
k: new Tag("oneway:bicycle", "yes"),
txt: "Eenrichtingsverkeer, óók voor fietsers. Dit gebruikt <b>" + r(this.carWidth + this.cyclistWidth) + "m</b>"
},
{
k: new And([new Tag("oneway", "yes"), new Tag("oneway:bicycle", "no")]),
txt: "Tweerichtingverkeer voor fietsers, eenrichting voor auto's Dit gebruikt <b>" + r(this.carWidth + 2 * this.cyclistWidth) + "m</b>"
},
{
k: new Tag("oneway", "yes"),
txt: "Eenrichtingsverkeer voor iedereen. Dit gebruikt <b>" + (this.carWidth + this.cyclistWidth) + "m</b>"
},
{
k: null,
txt: "Tweerichtingsverkeer voor iedereen. Dit gebruikt <b>" + r(2 * this.carWidth + 2 * this.cyclistWidth) + "m</b>"
}
]
}).OnlyShowIf(this._notCarfree),
new TagRenderingOptions(
{
tagsPreprocessor: (tags) => {
const props = self.calcProps(tags);
tags.targetWidth = r(props.targetWidth);
tags.short = "";
if (props.width < props.targetWidth) {
tags.short = `<span class='alert'>Dit is ${r(props.targetWidth - props.width)}m te weinig</span>`
}
console.log("SHORT", tags.short)
},
mappings: [
{
k: null,
txt: "De totale nodige ruimte voor vlot en veilig verkeer is dus <span class='thanks'>{targetWidth}m</span><br/>{short}"
}
]
}
).OnlyShowIf(this._notCarfree),
new TagRenderingOptions({
mappings: [
{k:new Tag("highway","living_street"),txt: "Dit is een woonerf"},
{k:new Tag("highway","pedestrian"),txt: "Deze weg is autovrij"}
]
}),
new TagRenderingOptions({
mappings: [
{
k: new Tag("sidewalk", "none"),
txt: "De afstand van huis tot huis is <b>{width:carriageway}m</b>"
},
{
k: new Tag("sidewalk", "left"),
txt: "De afstand van huis tot voetpad is <b>{width:carriageway}m</b>"
},
{
k: new Tag("sidewalk", "right"),
txt: "De afstand van huis tot voetpad is <b>{width:carriageway}m</b>"
},
{
k: new Tag("sidewalk", "both"),
txt: "De afstand van voetpad tot voetpad is <b>{width:carriageway}m</b>"
},
{
k: new Tag("sidewalk", ""),
txt: "De straatbreedte is <b>{width:carriageway}m</b>"
}
]
})
]
}
}

View file

@ -1,148 +0,0 @@
import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor";
import {TagsFilter, TagUtils} from "../Logic/Tags";
import {OnlyShowIfConstructor} from "./OnlyShowIf";
import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
import Translations from "../UI/i18n/Translations";
export class TagRenderingOptions implements TagDependantUIElementConstructor {
/**
* Notes: by not giving a 'question', one disables the question form alltogether
*/
public options: {
question?: string | Translation;
freeform?: {
key: string;
tagsPreprocessor?: (tags: any) => any;
template: string | Translation;
renderTemplate: string | Translation;
placeholder?: string | Translation;
extraTags?: TagsFilter
};
multiAnswer?: boolean,
mappings?: { k: TagsFilter; txt: string | Translation; substitute?: boolean, hideInAnwser?: boolean }[]
};
constructor(options: {
/**
* This is the string that is shown in the popup if this tag is missing.
*
* If 'question' is undefined, then the question is never asked at all
* If the question is "" (empty string) then the question is
*/
question?: Translation | string,
/**
* What is the priority of the question.
* By default, in the popup of a feature, only one question is shown at the same time. If multiple questions are unanswered, the question with the highest priority is asked first
*/
priority?: number,
/**
* Mappings convert a well-known tag combination into a user friendly text.
* It converts e.g. 'access=yes' into 'this area can be accessed'
*
* If there are multiple tags that should be matched, And can be used. All tags in AND will be added when the question is picked (and the corresponding text will only be shown if all tags are present).
* If AND is used, it is best practice to make sure every used tag is in every option (with empty string) to erase extra tags.
*
* If a 'k' is null, then this one is shown by default. It can be used to force a default value, e.g. to show that the name of a POI is not (yet) known .
* A mapping where 'k' is null will not be shown as option in the radio buttons.
*
*
*/
mappings?: { k: TagsFilter, txt: Translation | string, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[],
/**
* If true, use checkboxes to answer instead of radiobuttons
*/
multiAnswer?: boolean,
/**
* If one wants to render a freeform tag (thus no predefined key/values) or if there are a few well-known tags with a freeform object,
* use this.
* In the question, it'll offer a textfield
*/
freeform?: {
key: string,
template: string | Translation,
renderTemplate: string | Translation
placeholder?: string | Translation,
extraTags?: TagsFilter,
},
/**
* In some very rare cases, tags have to be rewritten before displaying
* This function can be used for that.
* This function is ran on a _copy_ of the original properties
*/
tagsPreprocessor?: ((tags: any) => void)
}) {
this.options = options;
}
OnlyShowIf(tagsFilter: TagsFilter): TagDependantUIElementConstructor {
return new OnlyShowIfConstructor(tagsFilter, this);
}
IsQuestioning(tags: any): boolean {
const tagsKV = TagUtils.proprtiesToKV(tags);
for (const oneOnOneElement of this.options.mappings ?? []) {
if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tagsKV)) {
return false;
}
}
if (this.options.freeform !== undefined && tags[this.options.freeform.key] !== undefined) {
return false;
}
return this.options.question !== undefined;
}
GetContent(tags: any): Translation {
const tagsKV = TagUtils.proprtiesToKV(tags);
for (const oneOnOneElement of this.options.mappings ?? []) {
if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tagsKV)) {
return Translations.WT(oneOnOneElement.txt);
}
}
if (this.options.freeform !== undefined) {
let template = Translations.WT(this.options.freeform.renderTemplate);
return template.Subs(tags);
}
console.warn("No content defined for", tags, " with mapping", this);
return undefined;
}
public static tagRendering: (tags: UIEventSource<any>,
options: {
priority?: number;
question?: string | Translation;
freeform?: {
key: string;
tagsPreprocessor?: (tags: any) => any;
template: string | Translation;
renderTemplate: string | Translation;
placeholder?: string | Translation; extraTags?: TagsFilter
},
multiAnswer?: boolean,
mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean, hideInAnswer?: boolean }[]
}) => TagDependantUIElement;
construct(tags: UIEventSource<any>): TagDependantUIElement {
return TagRenderingOptions.tagRendering(tags, this.options);
}
IsKnown(properties: any): boolean {
return !this.IsQuestioning(properties);
}
}

View file

@ -1,21 +0,0 @@
import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
export interface TagDependantUIElementConstructor {
construct(tags: UIEventSource<any>): TagDependantUIElement;
IsKnown(properties: any): boolean;
IsQuestioning(properties: any): boolean;
GetContent(tags: any): Translation;
}
export abstract class TagDependantUIElement extends UIElement {
abstract IsKnown(): boolean;
abstract IsQuestioning(): boolean;
abstract IsSkipped() : boolean;
}

View file

@ -11,9 +11,7 @@ import {Basemap} from "./Logic/Leaflet/Basemap";
import State from "./State";
import {WelcomeMessage} from "./UI/WelcomeMessage";
import {Img} from "./UI/Img";
import {DropDown} from "./UI/Input/DropDown";
import {LayerSelection} from "./UI/LayerSelection";
import {Preset} from "./Customizations/LayerDefinition";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import {UpdateFromOverpass} from "./Logic/UpdateFromOverpass";
import {UIEventSource} from "./Logic/UIEventSource";
@ -37,6 +35,7 @@ import {Utils} from "./Utils";
import BackgroundSelector from "./UI/BackgroundSelector";
import AvailableBaseLayers from "./Logic/AvailableBaseLayers";
import {FeatureInfoBox} from "./UI/Popup/FeatureInfoBox";
import SharedLayers from "./Customizations/SharedLayers";
export class InitUiElements {
@ -162,15 +161,14 @@ export class InitUiElements {
if (typeof layer === "string") {
continue;
}
const applicable = layer.overpassFilter.matches(TagUtils.proprtiesToKV(data));
const applicable = layer.overpassTags.matches(TagUtils.proprtiesToKV(data));
if (applicable) {
// This layer is the layer that gives the questions
const featureBox = new FeatureInfoBox(
feature.feature,
State.state.allElements.getElement(data.id),
layer.title,
layer.elementsToShow,
layer
);
State.state.fullScreenMessage.setData(featureBox);
@ -215,6 +213,10 @@ export class InitUiElements {
}
public static FromBase64(layoutFromBase64: string): Layout {
return Layout.LayoutFromJSON(JSON.parse(atob(layoutFromBase64)), SharedLayers.sharedLayers);
}
static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>) {
try {
@ -235,7 +237,7 @@ export class InitUiElements {
hashFromLocalStorage.setData(hash);
dedicatedHashFromLocalStorage.setData(hash);
}
const layoutToUse = FromJSON.FromBase64(hash);
const layoutToUse = InitUiElements.FromBase64(hash);
userLayoutParam.setData(layoutToUse.id);
return layoutToUse;
} catch (e) {
@ -338,18 +340,6 @@ export class InitUiElements {
}
static CreateLanguagePicker(label: string | UIElement = "") {
if (State.state.layoutToUse.data.supportedLanguages.length <= 1) {
return undefined;
}
return new DropDown(label, State.state.layoutToUse.data.supportedLanguages.map(lang => {
return {value: lang, shown: lang}
}
), Locale.language);
}
private static GenerateLayerControlPanel() {
@ -476,7 +466,6 @@ export class InitUiElements {
static InitLayers() {
const flayers: FilteredLayer[] = []
const presets: Preset[] = [];
const state = State.state;
@ -491,27 +480,10 @@ export class InitUiElements {
return new FeatureInfoBox(
feature,
tagsES,
layer.title,
layer.elementsToShow,
layer,
)
};
for (const preset of layer.presets ?? []) {
if (preset.icon === undefined) {
const tags = {};
for (const tag of preset.tags) {
const k = tag.key;
if (typeof (k) === "string") {
tags[k] = tag.value;
}
}
preset.icon = layer.style(tags)?.icon?.iconUrl;
}
presets.push(preset);
}
const flayer: FilteredLayer = FilteredLayer.fromDefinition(layer, generateInfo);
flayers.push(flayer);
@ -523,8 +495,6 @@ export class InitUiElements {
}
State.state.filteredLayers.setData(flayers);
State.state.presets.setData(presets);
}
}

View file

@ -4,10 +4,9 @@ import * as L from "leaflet"
import {Layer} from "leaflet"
import {GeoOperations} from "./GeoOperations";
import {UIElement} from "../UI/UIElement";
import {LayerDefinition} from "../Customizations/LayerDefinition";
import State from "../State";
import CodeGrid from "./Web/CodeGrid";
import LayerConfig from "../Customizations/JSON/LayerConfig";
/***
* A filtered layer is a layer which offers a 'set-data' function
@ -23,11 +22,11 @@ export class FilteredLayer {
public readonly name: string | UIElement;
public readonly filters: TagsFilter;
public readonly isDisplayed: UIEventSource<boolean> = new UIEventSource(true);
private readonly combinedIsDisplayed : UIEventSource<boolean>;
public readonly layerDef: LayerDefinition;
private readonly combinedIsDisplayed: UIEventSource<boolean>;
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] } };
private readonly _style: (properties) => { color: string, weight?: number, icon: { iconUrl: string, iconSize?: [number, number], popupAnchor?: [number, number], iconAnchor?: [number, number] } };
/** The featurecollection from overpass
@ -46,7 +45,7 @@ export class FilteredLayer {
constructor(
layerDef: LayerDefinition,
layerDef: LayerConfig,
showOnPopup: ((tags: UIEventSource<any>, feature: any) => UIElement)
) {
this.layerDef = layerDef;
@ -54,22 +53,56 @@ export class FilteredLayer {
this._wayHandling = layerDef.wayHandling;
this._showOnPopup = showOnPopup;
this._style = (tags) => {
if(layerDef.style === undefined){
return {icon: {iconUrl: "./assets/bug.svg"}, color: "#000"};
}
const obj = layerDef.style(tags);
if(obj.weight && typeof (obj.weight) === "string"){
obj.weight = Number(obj.weight);// Weight MUST be a number, otherwise leaflet does weird things. see https://github.com/Leaflet/Leaflet/issues/6075
if(isNaN(obj.weight)){
obj.weight = undefined;
const iconUrl = layerDef.icon?.GetRenderValue(tags)?.txt ?? "./assets/bug.svg";
const iconSize = (layerDef.iconSize?.GetRenderValue(tags)?.txt ?? "40,40,center").split(",");
function num(str, deflt = 40) {
const n = Number(str);
if (isNaN(n)) {
return deflt;
}
return n;
}
return obj;
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
};
};
this.name = name;
this.filters = layerDef.overpassFilter;
this._maxAllowedOverlap = layerDef.maxAllowedOverlapPercentage;
this.filters = layerDef.overpassTags;
this._maxAllowedOverlap = layerDef.hideUnderlayingFeaturesMinPercentage;
const self = this;
this.combinedIsDisplayed = this.isDisplayed.map<boolean>(isDisplayed => {
return isDisplayed && State.state.locationControl.data.zoom >= self.layerDef.minzoom
@ -111,9 +144,9 @@ export class FilteredLayer {
const tags = TagUtils.proprtiesToKV(feature.properties);
const centerPoint = GeoOperations.centerpoint(feature);
if (feature.geometry.type !== "Point") {
if (this._wayHandling === LayerDefinition.WAYHANDLING_CENTER_AND_WAY) {
if (this._wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) {
selfFeatures.push(centerPoint);
} else if (this._wayHandling === LayerDefinition.WAYHANDLING_CENTER_ONLY) {
} else if (this._wayHandling === LayerConfig.WAYHANDLING_CENTER_ONLY) {
feature = centerPoint;
}
}

View file

@ -103,7 +103,6 @@ export class ImageSearcher extends UIEventSource<{key: string, url: string}[]> {
}
private LoadImages(imagePrefix: string, loadAdditional: boolean): void {
console.log("Loading images from",this._tags)
const imageTag = this._tags.data[imagePrefix];
if (imageTag !== undefined) {
const bareImages = imageTag.split(";");

View file

@ -6,18 +6,19 @@ import {OsmNode, OsmObject} from "./OsmObject";
import {And, Tag, TagsFilter} from "../Tags";
import State from "../../State";
import {Utils} from "../../Utils";
import {UIEventSource} from "../UIEventSource";
export class Changes {
private static _nextId = -1; // New assined ID's are negative
addTag(elementId: string, tagsFilter: TagsFilter) {
addTag(elementId: string, tagsFilter: TagsFilter,
tags?: UIEventSource<any>) {
const changes = this.tagToChange(tagsFilter);
if (changes.length == 0) {
return;
}
const eventSource = State.state.allElements.getElement(elementId);
const eventSource = tags ?? State.state?.allElements.getElement(elementId);
const elementTags = eventSource.data;
const pending : {elementId:string, key: string, value: string}[] = [];
for (const change of changes) {

View file

@ -359,9 +359,10 @@ export class TagUtils {
return new And([]);
}
const keyValues = {} // Map string -> string[]
tagsFilters = [...tagsFilters]
tagsFilters = [...tagsFilters] // copy all
while (tagsFilters.length > 0) {
const tagsFilter = tagsFilters.pop();
// Queue
const tagsFilter = tagsFilters.shift();
if (tagsFilter === undefined) {
continue;
@ -388,7 +389,6 @@ export class TagUtils {
for (const key in keyValues) {
and.push(new Tag(key, Utils.Dedup(keyValues[key]).join(";")));
}
return new And(and);
}

View file

@ -4,7 +4,6 @@ import {FilteredLayer} from "./FilteredLayer";
import {Bounds} from "./Bounds";
import {Overpass} from "./Osm/Overpass";
import State from "../State";
import {LayerDefinition} from "../Customizations/LayerDefinition";
import MetaTagging from "./MetaTagging";
export class UpdateFromOverpass {
@ -34,7 +33,7 @@ export class UpdateFromOverpass {
if(location?.zoom === undefined){
return false;
}
let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => (layer as LayerDefinition).minzoom ?? 18));
let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18));
return location.zoom >= minzoom;
}, [state.layoutToUse]
);
@ -80,7 +79,7 @@ export class UpdateFromOverpass {
if (previouslyLoaded) {
continue;
}
filters.push(layer.overpassFilter);
filters.push(layer.overpassTags);
}
if (filters.length === 0) {
return undefined;

View file

@ -1,7 +1,6 @@
import {UIElement} from "./UI/UIElement";
import {Layout} from "./Customizations/Layout";
import {Utils} from "./Utils";
import {Preset} from "./Customizations/LayerDefinition";
import {ElementStorage} from "./Logic/ElementStorage";
import {Changes} from "./Logic/Osm/Changes";
import {OsmConnection} from "./Logic/Osm/OsmConnection";
@ -70,7 +69,6 @@ export default class State {
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([])
public presets: UIEventSource<Preset[]> = new UIEventSource<Preset[]>([])
/**
* The message that should be shown at the center of the screen

View file

@ -2,12 +2,11 @@ import {UIElement} from "../UIElement";
import {ImageSearcher} from "../../Logic/ImageSearcher";
import {SlideShow} from "./SlideShow";
import {UIEventSource} from "../../Logic/UIEventSource";
import {TagDependantUIElement} from "../../Customizations/UIElementConstructor";
import Combine from "../Base/Combine";
import DeleteImage from "./DeleteImage";
export class ImageCarousel extends TagDependantUIElement {
export class ImageCarousel extends UIElement{
public readonly slideshow: SlideShow;
@ -40,19 +39,4 @@ export class ImageCarousel extends TagDependantUIElement {
IsKnown(): boolean {
return true;
}
IsQuestioning(): boolean {
return false;
}
IsSkipped(): boolean {
return false;
}
Priority(): number {
return 0;
}
}

View file

@ -17,16 +17,6 @@ export class Img {
static readonly checkmark = `<svg width="26" height="18" viewBox="0 0 26 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 7.28571L10.8261 15L23 3" stroke="black" stroke-width="4" stroke-linejoin="round"/></svg>`;
static readonly no_checkmark = `<svg width="26" height="18" viewBox="0 0 26 18" fill="none" xmlns="http://www.w3.org/2000/svg"></svg>`;
static osmAbstractLogo: string =
"<svg class='osm-logo' xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" width=\"24px\" version=\"1.1\" viewBox=\"0 0 66 64\">" +
" <g transform=\"translate(-0.849, -61)\">\n" +
" <path d=\"M0.849,61 L6.414,75.609 L0.849,90.217 L6.414,104.826 L0.849,119.435 L4.266,120.739 L22.831,102.183 L26.162,102.696 L30.205,98.652 C27.819,95.888 26.033,92.59 25.057,88.948 L26.953,87.391 C26.627,85.879 26.449,84.313 26.449,82.704 C26.449,74.67 30.734,67.611 37.136,63.696 L30.066,61 L15.457,66.565 L0.849,61 z\"></path>" +
" <path d=\"M48.71,64.617 C48.406,64.617 48.105,64.629 47.805,64.643 C47.52,64.657 47.234,64.677 46.953,64.704 C46.726,64.726 46.499,64.753 46.275,64.783 C46.039,64.814 45.811,64.847 45.579,64.887 C45.506,64.9 45.434,64.917 45.362,64.93 C45.216,64.958 45.072,64.987 44.927,65.017 C44.812,65.042 44.694,65.06 44.579,65.087 C44.442,65.119 44.307,65.156 44.17,65.191 C43.943,65.25 43.716,65.315 43.492,65.383 C43.323,65.433 43.155,65.484 42.988,65.539 C42.819,65.595 42.65,65.652 42.483,65.713 C42.475,65.716 42.466,65.719 42.457,65.722 C35.819,68.158 31.022,74.369 30.649,81.774 C30.633,82.083 30.622,82.391 30.622,82.704 C30.622,83.014 30.631,83.321 30.649,83.626 C30.649,83.629 30.648,83.632 30.649,83.635 C30.662,83.862 30.681,84.088 30.701,84.313 C31.466,93.037 38.377,99.948 47.101,100.713 C47.326,100.733 47.552,100.754 47.779,100.765 C47.782,100.765 47.785,100.765 47.788,100.765 C48.093,100.783 48.399,100.791 48.709,100.791 C53.639,100.791 58.096,98.833 61.353,95.652 C61.532,95.477 61.712,95.304 61.883,95.122 C61.913,95.09 61.941,95.058 61.97,95.026 C61.98,95.015 61.987,95.002 61.996,94.991 C62.132,94.845 62.266,94.698 62.396,94.548 C62.449,94.487 62.501,94.426 62.553,94.365 C62.594,94.316 62.634,94.267 62.675,94.217 C62.821,94.04 62.961,93.861 63.101,93.678 C63.279,93.444 63.456,93.199 63.622,92.956 C63.956,92.471 64.267,91.97 64.553,91.452 C64.661,91.257 64.757,91.06 64.857,90.861 C64.89,90.796 64.93,90.735 64.962,90.67 C64.98,90.633 64.996,90.594 65.014,90.556 C65.125,90.324 65.234,90.09 65.336,89.852 C65.349,89.82 65.365,89.789 65.379,89.756 C65.48,89.517 65.575,89.271 65.666,89.026 C65.678,88.994 65.689,88.962 65.701,88.93 C65.792,88.679 65.881,88.43 65.962,88.174 C65.97,88.148 65.98,88.122 65.988,88.096 C66.069,87.832 66.144,87.564 66.214,87.296 C66.219,87.275 66.226,87.255 66.231,87.235 C66.301,86.962 66.365,86.686 66.423,86.409 C66.426,86.391 66.428,86.374 66.431,86.356 C66.445,86.291 66.453,86.223 66.466,86.156 C66.511,85.925 66.552,85.695 66.588,85.461 C66.632,85.169 66.671,84.878 66.701,84.583 C66.701,84.574 66.701,84.565 66.701,84.556 C66.731,84.258 66.755,83.955 66.77,83.652 C66.77,83.646 66.77,83.641 66.77,83.635 C66.786,83.326 66.797,83.017 66.797,82.704 C66.797,72.69 58.723,64.617 48.71,64.617 z\"></path>" +
" <path d=\"M62.936,99.809 C59.074,103.028 54.115,104.965 48.71,104.965 C47.101,104.965 45.535,104.787 44.023,104.461 L42.466,106.357 C39.007,105.43 35.855,103.781 33.179,101.574 L28.996,105.765 L29.51,108.861 L13.953,124.426 L15.457,125 L30.066,119.435 L44.675,125 L59.283,119.435 L64.849,104.826 L62.936,99.809 z\"></path>" +
" </g>" +
"</svg>";
static closedFilterButton: string = `<svg width="27" height="27" viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.5353 8.13481C26.4422 8.35428 26.2683 8.47598 26.0632 8.58537C21.9977 10.7452 17.935 12.9085 13.8758 15.0799C13.6475 15.2016 13.4831 15.1962 13.2568 15.0751C9.19822 12.903 5.13484 10.7404 1.07215 8.5758C0.490599 8.26608 0.448478 7.52562 0.991303 7.13796C1.0803 7.07438 1.17813 7.0231 1.2746 6.97045C5.15862 4.86462 9.04536 2.7629 12.9246 0.648187C13.3805 0.399316 13.7779 0.406837 14.2311 0.65434C18.0954 2.76153 21.9658 4.85779 25.8383 6.94926C26.1569 7.12155 26.411 7.32872 26.5353 7.67604C26.5353 7.82919 26.5353 7.98166 26.5353 8.13481Z" fill="#003B8B"/>
<path d="M13.318 26.535C12.1576 25.9046 10.9972 25.2736 9.83614 24.6439C6.96644 23.0877 4.09674 21.533 1.22704 19.9762C0.694401 19.6876 0.466129 19.2343 0.669943 18.7722C0.759621 18.5691 0.931505 18.3653 1.11969 18.2512C1.66659 17.9182 2.23727 17.6228 2.80863 17.3329C2.89423 17.2892 3.04981 17.3206 3.14493 17.3712C6.40799 19.1031 9.66969 20.837 12.9239 22.5845C13.3703 22.8238 13.7609 22.83 14.208 22.59C17.4554 20.8472 20.7117 19.1202 23.9605 17.3801C24.1493 17.2789 24.2838 17.283 24.4632 17.3876C24.8926 17.6386 25.3301 17.8772 25.7751 18.1001C26.11 18.2683 26.3838 18.4857 26.5346 18.8385C26.5346 18.9916 26.5346 19.1441 26.5346 19.2972C26.4049 19.6528 26.1399 19.8613 25.8152 20.0363C22.9964 21.5549 20.1831 23.0829 17.3684 24.609C16.1863 25.2496 15.0055 25.893 13.8248 26.535C13.6556 26.535 13.4865 26.535 13.318 26.535Z" fill="#003B8B"/>

View file

@ -49,6 +49,9 @@ export default class ValidatedTextField {
ValidatedTextField.tp(
"string",
"A basic string"),
ValidatedTextField.tp(
"text",
"A string, but allows input of longer strings more comfortably (a text area)"),
ValidatedTextField.tp(
"date",
"A date",
@ -171,6 +174,9 @@ export default class ValidatedTextField {
return new DropDown<string>("", values)
}
/**
* {string (typename) --> TextFieldDef}
*/
public static AllTypes = ValidatedTextField.allTypesDict();
public static InputForType(type: string, options?: {
@ -186,6 +192,7 @@ export default class ValidatedTextField {
const tp: TextFieldDef = ValidatedTextField.AllTypes[type]
const isValidTp = tp.isValid;
let isValid;
options.textArea = options.textArea ?? type === "text";
if (options.isValid) {
const optValid = options.isValid;
isValid = (str, country) => {

View file

@ -0,0 +1,83 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import {FixedUiElement} from "../Base/FixedUiElement";
import TagRenderingQuestion from "./TagRenderingQuestion";
import Translations from "../i18n/Translations";
import Combine from "../Base/Combine";
import TagRenderingAnswer from "./TagRenderingAnswer";
import State from "../../State";
export default class EditableTagRendering extends UIElement {
private _tags: UIEventSource<any>;
private _configuration: TagRenderingConfig;
private _editMode: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _editButton: UIElement;
private _question: UIElement;
private _answer: UIElement;
constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig) {
super(tags);
this._tags = tags;
this._configuration = configuration;
this.ListenTo(this._editMode);
this.ListenTo(State.state?.osmConnection?.userDetails)
const self = this;
this._answer = new TagRenderingAnswer(tags, configuration);
this._answer.SetStyle("width:100%;")
if (this._configuration.question !== undefined) {
// 2.3em total width
this._editButton = new FixedUiElement(
"<img style='width: 1.3em;height: 1.3em;padding: 0.5em;border-radius: 0.65em;border: solid black 1px;font-size: medium;float: right;' " +
"src='./assets/pencil.svg' alt='edit'>")
.onClick(() => {
self._editMode.setData(true);
});
// And at last, set up the skip button
const cancelbutton =
Translations.t.general.cancel.Clone()
.SetClass("cancel")
.onClick(() => {
self._editMode.setData(false)
});
this._question = new TagRenderingQuestion(tags, configuration,
() => {
self._editMode.setData(false)
},
cancelbutton)
}
}
InnerRender(): string {
if (this._editMode.data) {
return this._question.Render();
}
if(this._configuration.GetRenderValue(this._tags.data)=== undefined){
return "";
}
if(!this._configuration?.condition?.matchesProperties(this._tags.data)){
return "";
}
return new Combine([this._answer,
(State.state?.osmConnection?.userDetails?.data?.loggedIn ?? true) ? this._editButton : undefined
]).SetClass("answer")
.Render();
}
}

View file

@ -1,150 +1,47 @@
import {VerticalCombine} from "../Base/VerticalCombine";
import {UIElement} from "../UIElement";
import Combine from "../Base/Combine";
import {WikipediaLink} from "../../Customizations/Questions/WikipediaLink";
import {OsmLink} from "../../Customizations/Questions/OsmLink";
import {UIEventSource} from "../../Logic/UIEventSource";
import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions";
import State from "../../State";
import {And} from "../../Logic/Tags";
import {TagDependantUIElement, TagDependantUIElementConstructor} from "../../Customizations/UIElementConstructor";
import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations";
import LayerConfig from "../../Customizations/JSON/LayerConfig";
import EditableTagRendering from "./EditableTagRendering";
import QuestionBox from "./QuestionBox";
import Combine from "../Base/Combine";
import TagRenderingAnswer from "./TagRenderingAnswer";
export class FeatureInfoBox extends UIElement {
private _tags: UIEventSource<any>;
private _layerConfig: LayerConfig;
/**
* The actual GEOJSON-object, with geometry and stuff
*/
private _feature: any;
/**
* The tags, wrapped in a global event source
*/
private readonly _tagsES: UIEventSource<any>;
private readonly _title: UIElement;
private readonly _infoboxes: TagDependantUIElement[];
private readonly _oneSkipped = Translations.t.general.oneSkippedQuestion.Clone();
private readonly _someSkipped = Translations.t.general.skippedQuestions.Clone();
private _title : UIElement;
private _titleIcons: UIElement;
private _renderings: UIElement[];
private _questionBox : UIElement;
constructor(
feature: any,
tagsES: UIEventSource<any>,
title: TagDependantUIElementConstructor | UIElement | string,
elementsToShow: TagDependantUIElementConstructor[],
tags: UIEventSource<any>,
layerConfig: LayerConfig
) {
super(tagsES);
this._feature = feature;
this._tagsES = tagsES
if(tagsES === undefined){
throw "No Tags event source given"
}
this.ListenTo(State.state.osmConnection.userDetails);
this.SetClass("featureinfobox");
const tags = this._tagsES;
this._infoboxes = [];
elementsToShow = elementsToShow ?? []
const self = this;
for (const tagRenderingOption of elementsToShow) {
self._infoboxes.push(
tagRenderingOption.construct(tags));
}
function initTags() {
self._infoboxes.splice(0, self._infoboxes.length);
for (const tagRenderingOption of elementsToShow) {
self._infoboxes.push(
tagRenderingOption.construct(tags));
}
self.Update();
}
this._someSkipped.onClick(initTags)
this._oneSkipped.onClick(initTags)
super();
this._tags = tags;
this._layerConfig = layerConfig;
let renderedTitle: UIElement;
title = title ?? new TagRenderingOptions(
{
mappings: [{k: new And([]), txt: ""}]
}
)
if (typeof (title) == "string") {
renderedTitle = new FixedUiElement(title);
} else if (title instanceof UIElement) {
renderedTitle = title;
} else {
renderedTitle = title.construct(tags);
}
this._title = new TagRenderingAnswer(tags, layerConfig.title)
.SetClass("featureinfobox-title");
this._titleIcons = new Combine(
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon)))
.SetClass("featureinfobox-icons");
this._renderings = layerConfig.tagRenderings.map(tr => new EditableTagRendering(tags, tr));
this._questionBox = new QuestionBox(tags, layerConfig.tagRenderings);
renderedTitle
.SetStyle("width: calc(100% - 50px - 0.2em);")
.SetClass("title-font")
const osmLink = new OsmLink()
.construct(tags)
.SetStyle("width: 24px; display:block;")
const wikipedialink = new WikipediaLink()
.construct(tags)
.SetStyle("width: 24px; display:block;")
this._title = new Combine([
renderedTitle,
wikipedialink,
osmLink]).SetStyle("display:flex;");
}
InnerRender(): string {
const info = [];
const questions: TagDependantUIElement[] = [];
let skippedQuestions = 0;
for (const infobox of this._infoboxes) {
if (infobox.IsKnown()) {
info.push(infobox);
} else if (infobox.IsQuestioning()) {
questions.push(infobox);
} else if (infobox.IsSkipped()) {
// This question is neither known nor questioning -> it was skipped
skippedQuestions++;
}
}
let questionElement: UIElement;
if (questions.length > 0) {
// We select the most important question and render that one
let mostImportantQuestion;
for (const question of questions) {
if (mostImportantQuestion === undefined) {
mostImportantQuestion = question;
break;
}
}
questionElement = mostImportantQuestion;
} else if (skippedQuestions == 1) {
questionElement = this._oneSkipped;
} else if (skippedQuestions > 0) {
questionElement = this._someSkipped;
}
const infoboxcontents = new Combine(
[new VerticalCombine(info).SetClass("infobox-information")
, questionElement ?? ""]);
return new Combine([
this._title,
"<div class='infoboxcontents'>",
infoboxcontents,
"</div>"])
.Render();
new Combine([this._title, this._titleIcons])
.SetClass("featureinfobox-titlebar"),
...this._renderings,
this._questionBox
]).Render();
}
}

78
UI/Popup/QuestionBox.ts Normal file
View file

@ -0,0 +1,78 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import TagRenderingQuestion from "./TagRenderingQuestion";
import Translations from "../i18n/Translations";
/**
* Generates all the questions, one by one
*/
export default class QuestionBox extends UIElement {
private _tags: UIEventSource<any>;
private _tagRenderings: TagRenderingConfig[];
private _tagRenderingQuestions: UIElement[];
private _skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])
private _skippedQuestionsButton: UIElement;
constructor(tags: UIEventSource<any>, tagRenderings: TagRenderingConfig[]) {
super(tags);
this.ListenTo(this._skippedQuestions);
this._tags = tags;
const self = this;
this._tagRenderings = tagRenderings.filter(tr => tr.question !== undefined);
this._tagRenderingQuestions = this._tagRenderings
.map((tagRendering, i) => new TagRenderingQuestion(this._tags, tagRendering,
() => {
// We save
self._skippedQuestions.data.push(i)
self._skippedQuestions.ping();
},
Translations.t.general.skip.Clone()
.SetClass("cancel")
.onClick(() => {
self._skippedQuestions.data.push(i);
self._skippedQuestions.ping();
})
));
this._skippedQuestionsButton = Translations.t.general.skippedQuestions.Clone()
.onClick(() => {
self._skippedQuestions.setData([]);
})
}
InnerRender(): string {
for (let i = 0; i < this._tagRenderingQuestions.length; i++) {
let tagRendering = this._tagRenderings[i];
if(tagRendering.condition &&
!tagRendering.condition.matchesProperties(this._tags.data)){
// Filtered away by the condition
continue;
}
if (tagRendering.GetRenderValue(this._tags.data) !== undefined) {
// This value is known
continue;
}
if (this._skippedQuestions.data.indexOf(i) >= 0) {
continue;
}
// this value is NOT known
return this._tagRenderingQuestions[i].Render();
}
if (this._skippedQuestions.data.length > 0) {
return this._skippedQuestionsButton.Render();
}
return "";
}
}

View file

@ -1,9 +1,12 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement";
import Translations from "../i18n/Translations";
import State from "../../State";
export class SaveButton extends UIElement {
private _value: UIEventSource<any>;
private _friendlyLogin: UIElement;
constructor(value: UIEventSource<any>) {
super(value);
@ -11,16 +14,22 @@ export class SaveButton extends UIElement {
throw "No event source for savebutton, something is wrong"
}
this._value = value;
this._friendlyLogin = Translations.t.general.loginToStart.Clone()
.SetClass("login-button-friendly")
.onClick(() => State.state.osmConnection.AttemptLogin())
}
InnerRender(): string {
if (this._value.data === undefined ||
this._value.data === null
|| this._value.data === ""
) {
return "<span class='save-non-active'>"+Translations.t.general.save.Render()+"</span>"
let clss = "save";
if(State.state !== undefined && !State.state.osmConnection.userDetails.data.loggedIn){
return this._friendlyLogin.Render();
}
return "<span class='save'>"+Translations.t.general.save.Render()+"</span>";
if ((this._value.data ?? "") === "") {
clss = "save-non-active";
}
return Translations.t.general.save.Clone().SetClass(clss).Render();
}
}

View file

@ -1,544 +0,0 @@
import {UIElement} from "../UIElement";
import Translation from "../i18n/Translation";
import {VariableUiElement} from "../Base/VariableUIElement";
import InputElementMap from "../Input/InputElementMap";
import CheckBoxes from "../Input/Checkboxes";
import Combine from "../Base/Combine";
import {And, Tag, TagsFilter, TagUtils} from "../../Logic/Tags";
import {InputElement} from "../Input/InputElement";
import {SaveButton} from "./SaveButton";
import {RadioButton} from "../Input/RadioButton";
import {FixedInputElement} from "../Input/FixedInputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import ValidatedTextField from "../Input/ValidatedTextField";
import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions";
import State from "../../State";
import {SubstitutedTranslation} from "../SpecialVisualizations";
import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations";
import {TagDependantUIElement} from "../../Customizations/UIElementConstructor";
import Locale from "../i18n/Locale";
export class TagRendering extends UIElement implements TagDependantUIElement {
private readonly _question: string | Translation;
private readonly _mapping: { k: TagsFilter, txt: string | Translation }[];
private readonly currentTags: UIEventSource<any>;
private readonly _freeform: {
key: string,
template: string | UIElement,
renderTemplate: string | Translation,
placeholder?: string | UIElement,
extraTags?: TagsFilter
};
private readonly _questionElement: InputElement<TagsFilter>;
private readonly _saveButton: UIElement;
private readonly _friendlyLogin: UIElement;
private readonly _skipButton: UIElement;
private readonly _editButton: UIElement;
private readonly _appliedTags: UIElement;
private readonly _questionSkipped: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _editMode: UIEventSource<boolean> = new UIEventSource<boolean>(false);
static injectFunction() {
// This is a workaround as not to import tagrendering into TagREnderingOptions
TagRenderingOptions.tagRendering = (tags, options) => new TagRendering(tags, options);
return true;
}
constructor(tags: UIEventSource<any>, options: {
question?: string | Translation,
freeform?: {
key: string,
template: string | Translation,
renderTemplate: string | Translation,
placeholder?: string | Translation,
extraTags?: TagsFilter,
},
tagsPreprocessor?: ((tags: any) => any),
multiAnswer?: boolean,
mappings?: { k: TagsFilter, txt: string | Translation, substitute?: boolean, hideInAnswer?: boolean }[]
}) {
super(tags);
if (tags === undefined) {
throw "No tags given for a tagrendering..."
}
if (options.question !== undefined) {
if ((options.mappings?.length ?? 0) === 0 && options.freeform.key === undefined) {
throw "Error: question without mappings or key"
}
}
this.ListenTo(Locale.language);
this.ListenTo(this._editMode);
this.ListenTo(this._questionSkipped);
this.ListenTo(State.state?.osmConnection?.userDetails);
const self = this;
this.currentTags = tags.map(tags => {
if (options.tagsPreprocessor === undefined) {
return tags;
}
// we clone the tags...
let newTags = {};
for (const k in tags) {
newTags[k] = tags[k];
}
// ... in order to safely edit them here
options.tagsPreprocessor(newTags);
return newTags;
}
);
tags.addCallback(() => self.currentTags.ping());
if (options.question !== undefined) {
this._question = options.question;
}
this._mapping = [];
this._freeform = options.freeform;
for (const choice of options.mappings ?? []) {
let choiceSubbed = {
k: choice.k?.substituteValues(this.currentTags.data),
txt: choice.txt,
}
this._mapping.push({
k: choiceSubbed.k,
txt: choiceSubbed.txt
});
}
// Prepare the actual input element -> pick an appropriate implementation
this._questionElement = this.InputElementFor(options) ??
new FixedInputElement<TagsFilter>("<span class='alert'>No input possible</span>", new Tag("a", "b"));
const save = () => {
const selection = self._questionElement.GetValue().data;
console.log("Tagrendering: saving tags ", selection);
if (selection) {
State.state?.changes?.addTag(tags.data.id, selection);
}
self._editMode.setData(false);
}
this._appliedTags = new VariableUiElement(
self._questionElement.GetValue().map(
(tags: TagsFilter) => {
const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000;
if (csCount < State.userJourney.tagsVisibleAt) {
return "";
}
if (tags === undefined) {
return Translations.t.general.noTagsSelected.SetClass("subtle").Render();
}
if (csCount < State.userJourney.tagsVisibleAndWikiLinked) {
const tagsStr = tags.asHumanString(false, true);
return new FixedUiElement(tagsStr).SetClass("subtle").Render();
}
return tags.asHumanString(true, true);
}
)
).ListenTo(self._questionElement);
const cancel = () => {
self._questionSkipped.setData(true);
self._editMode.setData(false);
self._source.ping(); // Send a ping upstream to render the next question
}
// Setup the save button and it's action
this._saveButton = new SaveButton(this._questionElement.GetValue())
.onClick(save);
this._friendlyLogin = Translations.t.general.loginToStart.Clone()
.SetClass("login-button-friendly")
.onClick(() => State.state.osmConnection.AttemptLogin())
this._editButton = new FixedUiElement("");
if (this._question !== undefined) {
// 2.3em total width
this._editButton = new FixedUiElement(
"<img style='width: 1.3em;height: 1.3em;padding: 0.5em;border-radius: 0.65em;border: solid black 1px;font-size: medium;float: right;' " +
"src='./assets/pencil.svg' alt='edit'>")
.onClick(() => {
self._editMode.setData(true);
self._questionElement.GetValue().setData(self.CurrentValue());
});
}
const cancelContents = this._editMode.map((isEditing) => {
const tr = Translations.t.general;
const text = isEditing ? tr.cancel : tr.skip;
return text
.SetStyle("display: inline-block;border: solid black 0.5px;padding: 0.2em 0.3em;border-radius: 1.5em;")
.Render();
}, [Locale.language]);
// And at last, set up the skip button
this._skipButton = new VariableUiElement(cancelContents).onClick(cancel);
}
private InputElementFor(options: {
freeform?: {
key: string,
template: string | Translation,
renderTemplate: string | Translation,
placeholder?: string | Translation,
extraTags?: TagsFilter,
},
multiAnswer?: boolean,
mappings?: { k: TagsFilter, txt: string | Translation, substitute?: boolean, hideInAnswer?: boolean }[]
}):
InputElement<TagsFilter> {
let freeformElement: InputElement<TagsFilter> = undefined;
if (options.freeform !== undefined) {
freeformElement = this.InputForFreeForm(options.freeform);
}
if (options.mappings === undefined || options.mappings.length === 0) {
return freeformElement;
}
const elements: InputElement<TagsFilter>[] = [];
for (const mapping of options.mappings) {
if (mapping.k === null) {
continue;
}
if (mapping.hideInAnswer) {
continue;
}
elements.push(this.InputElementForMapping(mapping, mapping.substitute));
}
if (freeformElement !== undefined) {
elements.push(freeformElement);
}
if (!options.multiAnswer) {
return new RadioButton(elements, false);
} else {
const possibleTags = elements.map(el => el.GetValue().data);
const checkBoxes = new CheckBoxes(elements);
const inputEl = new InputElementMap<number[], TagsFilter>(checkBoxes,
(t0, t1) => {
return t0?.isEquivalent(t1) ?? false
},
(indices) => {
if (indices.length === 0) {
return undefined;
}
let tags: TagsFilter[] = indices.map(i => elements[i].GetValue().data);
return TagUtils.FlattenMultiAnswer(tags);
},
(tags: TagsFilter) => {
const splitUpValues = TagUtils.SplitMultiAnswer(tags, possibleTags, this._freeform?.key, this._freeform?.extraTags);
const indices: number[] = []
for (let i = 0; i < splitUpValues.length; i++) {
let splitUpValue = splitUpValues[i];
for (let j = 0; j < elements.length; j++) {
let inputElement = elements[j];
if (inputElement.IsValid(splitUpValue)) {
indices.push(j);
inputElement.GetValue().setData(splitUpValue);
break;
}
}
}
return indices;
},
[freeformElement?.GetValue()]
);
freeformElement?.GetValue()?.addCallbackAndRun(value => {
const es = checkBoxes.GetValue();
const i = elements.length - 1;
const index = es.data.indexOf(i);
if (value === undefined) {
if (index >= 0) {
es.data.splice(index, 1);
es.ping();
}
} else if (index < 0) {
es.data.push(i);
es.ping();
}
});
return inputEl;
}
}
private InputElementForMapping(mapping: { k: TagsFilter, txt: (string | Translation) }, substituteValues: boolean): FixedInputElement<TagsFilter> {
if (substituteValues) {
return new FixedInputElement(this.ApplyTemplate(mapping.txt),
mapping.k.substituteValues(this.currentTags.data),
(t0, t1) => t0.isEquivalent(t1)
);
}
let txt = this.ApplyTemplate(mapping.txt);
if (txt.Render().indexOf("<img") >= 0) {
txt.SetClass("question-option-with-border");
}
const inputEl = new FixedInputElement(txt, mapping.k,
(t0, t1) => t1.isEquivalent(t0));
return inputEl;
}
private InputForFreeForm(freeform: {
key: string,
template: string | Translation,
renderTemplate: string | Translation,
placeholder?: string | Translation,
extraTags?: TagsFilter,
}): InputElement<TagsFilter> {
if (freeform?.template === undefined) {
return undefined;
}
const prepost = Translations.W(freeform.template).InnerRender()
.replace("$$$", "$string$")
.split("$");
let type = prepost[1];
let isTextArea = false;
if (type === "text") {
isTextArea = true;
type = "string";
}
if (ValidatedTextField.AllTypes[type] === undefined) {
console.error("Type:", type, ValidatedTextField.AllTypes)
throw "Unkown type: " + type;
}
const pickString =
(string: any) => {
if (string === "" || string === undefined) {
return undefined;
}
const tag = new Tag(freeform.key, string);
if (freeform.extraTags === undefined) {
return tag;
}
return new And([
tag,
freeform.extraTags
]
);
};
const toString = (tag) => {
if (tag instanceof And) {
for (const subtag of tag.and) {
if (subtag instanceof Tag && subtag.key === freeform.key) {
return subtag.value;
}
}
return undefined;
} else if (tag instanceof Tag) {
return tag.value
}
return undefined;
}
return ValidatedTextField.Mapped(pickString, toString, {
placeholder: freeform.placeholder,
type: type,
isValid: (str) => (str.length <= 255),
textArea: isTextArea,
country: this._source.data._country
})
}
IsKnown(): boolean {
const tags = TagUtils.proprtiesToKV(this._source.data);
for (const oneOnOneElement of this._mapping) {
if (oneOnOneElement.k === null || oneOnOneElement.k === undefined || oneOnOneElement.k.matches(tags)) {
return true;
}
}
return this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined;
}
IsSkipped(): boolean {
return this._questionSkipped.data;
}
private CurrentValue(): TagsFilter {
const tags = TagUtils.proprtiesToKV(this._source.data);
for (const oneOnOneElement of this._mapping) {
if (oneOnOneElement.k !== null && oneOnOneElement.k.matches(tags)) {
return oneOnOneElement.k;
}
}
if (this._freeform === undefined) {
return undefined;
}
return new Tag(this._freeform.key, this._source.data[this._freeform.key]);
}
IsQuestioning(): boolean {
if (this.IsKnown()) {
return false;
}
if (this._question === undefined ||
this._question === "" ||
(this._freeform?.template === undefined && (this._mapping?.length ?? 0) == 0)) {
// We don't ask this question in the first place
return false;
}
if (this._questionSkipped.data) {
// We don't ask for this question anymore, skipped by user
return false;
}
return true;
}
private RenderAnswer(): UIElement {
const tags = TagUtils.proprtiesToKV(this._source.data);
for (const oneOnOneElement of this._mapping) {
if (oneOnOneElement.k === undefined || oneOnOneElement.k.matches(tags)) {
// We have found a matching key -> we use this template
return this.ApplyTemplate(oneOnOneElement.txt);
}
}
if (this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined) {
return this.ApplyTemplate(this._freeform.renderTemplate);
}
return new FixedUiElement("");
}
private CreateComponent(): UIElement {
if (this.IsQuestioning()
&& (State.state !== undefined) // If State.state is undefined, we are testing/custom theme building -> show regular save
&& !State.state.osmConnection.userDetails.data.loggedIn) {
const question =
this.ApplyTemplate(this._question).SetClass('question-text');
return new Combine(["<div class='question'>",
question,
"<br/>",
this._questionElement,
this._friendlyLogin, "</div>"
]);
}
if (this.IsQuestioning() || this._editMode.data) {
// Not yet known or questioning, we have to ask a question
return new Combine([
this.ApplyTemplate(this._question).SetStyle('question-text'),
"<br/>",
"<div>", this._questionElement, "</div>",
this._skipButton,
this._saveButton,
"<br/>",
this._appliedTags
]).SetClass('question');
}
if (this.IsKnown()) {
const answer = this.RenderAnswer();
if (answer.IsEmpty()) {
return new FixedUiElement("");
}
const answerStyle = " display: inline-block;" +
" margin: 0.1em;" +
" width: 100%;" +
" font-size: large;"
if (State.state === undefined || // state undefined -> we are custom testing
State.state?.osmConnection?.userDetails?.data?.loggedIn && this._question !== undefined) {
answer.SetStyle("display:inline-block;width:calc(100% - 2.3em);")
return new Combine([
answer,
this._editButton])
.SetStyle(answerStyle);
}
return answer.SetStyle(answerStyle);
}
console.error("Invalid tagrendering: fallthrough",this)
return new FixedUiElement("");
}
InnerRender(): string {
return this.CreateComponent().Render();
}
protected InnerUpdate(htmlElement: HTMLElement) {
this._editButton.Update();
}
private readonly answerCache = {}
// Makes sure that the elements receive updates
// noinspection JSMismatchedCollectionQueryUpdate
private readonly substitutedElements : UIElement[]= [];
private ApplyTemplate(template: string | Translation): UIElement {
const tr = Translations.WT(template);
if(tr === undefined){
return undefined;
}
if (this.answerCache[tr.id]) {
return this.answerCache[tr.id];
}
// We have to cache these elemnts, otherwise it is to slow
const el = new SubstitutedTranslation(tr, this.currentTags);
this.answerCache[tr.id] = el;
this.substitutedElements.push(el);
return el;
}
}

View file

@ -0,0 +1,28 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import {UIElement} from "../UIElement";
import {SubstitutedTranslation} from "../SpecialVisualizations";
/***
* Displays the correct value for a known tagrendering
*/
export default class TagRenderingAnswer extends UIElement {
private _tags: UIEventSource<any>;
private _configuration: TagRenderingConfig;
constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig) {
super(tags);
this._tags = tags;
this._configuration = configuration;
}
InnerRender(): string {
const tr = this._configuration.GetRenderValue(this._tags.data);
if(tr === undefined){
return "";
}
return new SubstitutedTranslation(tr, this._tags).Render();
}
}

View file

@ -0,0 +1,251 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import {InputElement} from "../Input/InputElement";
import {And, Tag, TagsFilter, TagUtils} from "../../Logic/Tags";
import ValidatedTextField from "../Input/ValidatedTextField";
import Translation from "../i18n/Translation";
import {FixedInputElement} from "../Input/FixedInputElement";
import {SubstitutedTranslation} from "../SpecialVisualizations";
import {RadioButton} from "../Input/RadioButton";
import {Utils} from "../../Utils";
import CheckBoxes from "../Input/Checkboxes";
import InputElementMap from "../Input/InputElementMap";
import {SaveButton} from "./SaveButton";
import State from "../../State";
import {Changes} from "../../Logic/Osm/Changes";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import {FixedUiElement} from "../Base/FixedUiElement";
/**
* Shows the question element.
* Note that the value _migh_ already be known, e.g. when selected or when changing the value
*/
export default class TagRenderingQuestion extends UIElement {
private _tags: UIEventSource<any>;
private _configuration: TagRenderingConfig;
private _saveButton: UIElement;
private _inputElement: InputElement<TagsFilter>;
private _cancelButton: UIElement;
private _appliedTags: UIElement;
private _question: UIElement;
constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig,
afterSave?: () => void,
cancelButton?: UIElement) {
super(tags);
this._tags = tags;
this._configuration = configuration;
this._cancelButton = cancelButton;
this._question = new SubstitutedTranslation(this._configuration.question, tags)
.SetClass("question-text");
if (configuration === undefined) {
throw "A question is needed for a question visualization"
}
this._inputElement = this.GenerateInputElement()
const self = this;
const save = () => {
const selection = self._inputElement.GetValue().data;
if (selection) {
(State.state?.changes ?? new Changes())
.addTag(tags.data.id, selection, tags);
}
if (afterSave) {
afterSave();
}
}
this._saveButton = new SaveButton(this._inputElement.GetValue())
.onClick(save)
this._appliedTags = new VariableUiElement(
self._inputElement.GetValue().map(
(tags: TagsFilter) => {
const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000;
if (csCount < State.userJourney.tagsVisibleAt) {
return "";
}
if (tags === undefined) {
return Translations.t.general.noTagsSelected.SetClass("subtle").Render();
}
if (csCount < State.userJourney.tagsVisibleAndWikiLinked) {
const tagsStr = tags.asHumanString(false, true);
return new FixedUiElement(tagsStr).SetClass("subtle").Render();
}
return tags.asHumanString(true, true);
}
)
)
}
private GenerateInputElement(): InputElement<TagsFilter> {
const ff = this.GenerateFreeform();
const self = this;
let mappings =
(this._configuration.mappings ?? []).map(mapping => self.GenerateMappingElement(mapping));
mappings = Utils.NoNull(mappings);
if (mappings.length == 0) {
return ff;
}
mappings = Utils.NoNull([...mappings, ff]);
mappings.forEach(el => el.SetClass("question-option-with-border"))
if (this._configuration.multiAnswer) {
return this.GenerateMultiAnswer(mappings, ff)
} else {
return new RadioButton(mappings, false)
}
}
private GenerateMultiAnswer(elements: InputElement<TagsFilter>[], freeformField: InputElement<TagsFilter>): InputElement<TagsFilter> {
const possibleTags = elements.map(el => el.GetValue().data);
const checkBoxes = new CheckBoxes(elements);
const inputEl = new InputElementMap<number[], TagsFilter>(
checkBoxes,
(t0, t1) => {
return t0?.isEquivalent(t1) ?? false
},
(indices) => {
if (indices.length === 0) {
return undefined;
}
const tags: TagsFilter[] = indices.map(i => elements[i].GetValue().data);
return TagUtils.FlattenMultiAnswer(tags);
},
(tags: TagsFilter) => {
const splitUpValues = TagUtils.SplitMultiAnswer(tags, possibleTags, this._configuration.freeform?.key, new And(this._configuration.freeform?.addExtraTags));
const indices: number[] = []
for (let i = 0; i < splitUpValues.length; i++) {
let splitUpValue = splitUpValues[i];
for (let j = 0; j < elements.length; j++) {
let inputElement = elements[j];
if (inputElement.IsValid(splitUpValue)) {
indices.push(j);
inputElement.GetValue().setData(splitUpValue);
break;
}
}
}
console.log(indices)
return indices;
},
elements.map(el => el.GetValue())
);
freeformField?.GetValue()?.addCallbackAndRun(value => {
const es = checkBoxes.GetValue();
const i = elements.length - 1;
const index = es.data.indexOf(i);
if (value === undefined) {
if (index >= 0) {
es.data.splice(index, 1);
es.ping();
}
} else if (index < 0) {
es.data.push(i);
es.ping();
}
});
return inputEl;
}
private GenerateMappingElement(mapping: {
if: TagsFilter,
then: Translation,
hideInAnswer: boolean
}): InputElement<TagsFilter> {
if (mapping.hideInAnswer) {
return undefined;
}
return new FixedInputElement(
new SubstitutedTranslation(mapping.then, this._tags),
mapping.if,
(t0, t1) => t1.isEquivalent(t0));
}
private GenerateFreeform(): InputElement<TagsFilter> {
const freeform = this._configuration.freeform;
if (freeform === undefined) {
return undefined;
}
const pickString =
(string: any) => {
if (string === "" || string === undefined) {
return undefined;
}
const tag = new Tag(freeform.key, string);
if (freeform.addExtraTags === undefined) {
return tag;
}
return new And([
tag,
...freeform.addExtraTags
]
);
};
const toString = (tag) => {
if (tag instanceof And) {
for (const subtag of tag.and) {
if (subtag instanceof Tag && subtag.key === freeform.key) {
return subtag.value;
}
}
return undefined;
} else if (tag instanceof Tag) {
return tag.value
}
return undefined;
}
const textField = ValidatedTextField.InputForType(this._configuration.freeform.type, {
isValid: (str) => (str.length <= 255),
country: this._tags.data._country
});
textField.GetValue().setData(this._tags.data[this._configuration.freeform.key]);
return new InputElementMap(
textField, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
pickString, toString
);
}
InnerRender(): string {
return new Combine([
this._question,
this._inputElement, "<br/>",
this._cancelButton,
this._saveButton, "<br/>",
this._appliedTags])
.SetClass("question")
.Render()
}
}

View file

@ -8,7 +8,6 @@ import Locale from "./i18n/Locale";
import State from "../State";
import {UIEventSource} from "../Logic/UIEventSource";
import {Utils} from "../Utils";
/**
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
@ -52,21 +51,9 @@ export class SimpleAddUI extends UIElement {
for (const preset of layer.layerDef.presets) {
let icon: string = "./assets/bug.svg";
if (preset.icon !== undefined) {
if (typeof (preset.icon) !== "string") {
const tags = Utils.MergeTags(TagUtils.KVtoProperties(preset.tags), {id:"node/-1"});
icon = preset.icon.GetContent(tags).txt;
if(icon.startsWith("$")){
icon = undefined;
}
} else {
icon = preset.icon;
}
} else {
console.warn("No icon defined for preset ", preset, "in layer ", layer.layerDef.id)
}
let icon: string = layer.layerDef.icon.GetRenderValue(
TagUtils.KVtoProperties(preset.tags ?? [])).txt ??
"./assets/bug.svg";
const csCount = State.state.osmConnection.userDetails.data.csCount;
let tagInfo = "";

View file

@ -6,8 +6,8 @@ import Translations from "./i18n/Translations";
import {UserDetails} from "../Logic/Osm/OsmConnection";
import State from "../State";
import {UIEventSource} from "../Logic/UIEventSource";
import {InitUiElements} from "../InitUiElements";
import Combine from "./Base/Combine";
import Locale from "./i18n/Locale";
/**
* Handles and updates the user badge
@ -23,7 +23,7 @@ export class UserBadge extends UIElement {
constructor() {
super(State.state.osmConnection.userDetails);
this._userDetails = State.state.osmConnection.userDetails;
this._languagePicker = (InitUiElements.CreateLanguagePicker() ?? new FixedUiElement(""))
this._languagePicker = (Locale.CreateLanguagePicker(State.state.layoutToUse.data.supportedLanguages) ?? new FixedUiElement(""))
.SetStyle("display:inline-block;width:min-content;");
this._loginButton = Translations.t.general.loginWithOpenStreetMap

View file

@ -3,7 +3,6 @@ import Locale from "../UI/i18n/Locale";
import State from "../State";
import Translations from "./i18n/Translations";
import Combine from "./Base/Combine";
import {InitUiElements} from "../InitUiElements";
export class WelcomeMessage extends UIElement {
@ -18,7 +17,7 @@ export class WelcomeMessage extends UIElement {
constructor() {
super(State.state.osmConnection.userDetails);
this.ListenTo(Locale.language);
this.languagePicker = InitUiElements.CreateLanguagePicker(Translations.t.general.pickLanguage);
this.languagePicker = Locale.CreateLanguagePicker(State.state.layoutToUse.data.supportedLanguages, Translations.t.general.pickLanguage);
const layout = State.state.layoutToUse.data;
this.description =Translations.W(layout.welcomeMessage);

View file

@ -1,11 +1,13 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement";
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
import {DropDown} from "../Input/DropDown";
export default class Locale {
public static language: UIEventSource<string> = Locale.setup();
private static setup() {
const source = LocalStorageSource.Get('language', "en");
if (!UIElement.runningFromConsole) {
@ -17,6 +19,20 @@ export default class Locale {
return source;
}
public static CreateLanguagePicker(
languages : string[] ,
label: string | UIElement = "") {
if (languages.length <= 1) {
return undefined;
}
return new DropDown(label, languages.map(lang => {
return {value: lang, shown: lang}
}
), Locale.language);
}
}

View file

@ -1056,6 +1056,22 @@ export default class Translations {
return s;
}
static T(t: string | any): Translation {
if(t === undefined){
return undefined;
}
if(typeof t === "string"){
return new Translation({"*":t});
}
if(t.render !== undefined){
const msg = "Creating a translation, but this object contains a 'render'-field. Use the translation directly"
console.error(msg, t);
throw msg
}
return new Translation(t);
}
private static wtcache = {}
public static WT(s: string | Translation): Translation {
if(s === undefined){

View file

@ -276,7 +276,8 @@
"en": "{curator} is the curator of this nature reserve"
},
"freeform": {
"key": "curator"
"key": "curator",
"type": "string"
}
},
{
@ -329,16 +330,17 @@
}
},
{"#": "Surface are",
"render": {
"en": "Surface area: {_surface:ha}Ha",
"mappings": {
"if": "_surface:ha=0",
"then": ""
"render": {
"en": "Surface area: {_surface:ha}Ha",
"mappings": {
"if": "_surface:ha=0",
"then": ""
}
}
}
}
],
"hideUnderlayingFeaturesMinPercentage": 10,
"wayHandling": 1,
"icon": {
"render": "./assets/themes/buurtnatuur/nature_reserve.svg"
},

7
assets/osm-logo-us.svg Normal file
View file

@ -0,0 +1,7 @@
<svg class='osm-logo' xmlns="http://www.w3.org/2000/svg" height="100px" width="100px" version="1.1" viewBox="0 0 66 64">
<g transform="translate(-0.849, -61)" fill="#7ebc6f">
<path d="M0.849,61 L6.414,75.609 L0.849,90.217 L6.414,104.826 L0.849,119.435 L4.266,120.739 L22.831,102.183 L26.162,102.696 L30.205,98.652 C27.819,95.888 26.033,92.59 25.057,88.948 L26.953,87.391 C26.627,85.879 26.449,84.313 26.449,82.704 C26.449,74.67 30.734,67.611 37.136,63.696 L30.066,61 L15.457,66.565 L0.849,61 z"/>
<path d="M48.71,64.617 C48.406,64.617 48.105,64.629 47.805,64.643 C47.52,64.657 47.234,64.677 46.953,64.704 C46.726,64.726 46.499,64.753 46.275,64.783 C46.039,64.814 45.811,64.847 45.579,64.887 C45.506,64.9 45.434,64.917 45.362,64.93 C45.216,64.958 45.072,64.987 44.927,65.017 C44.812,65.042 44.694,65.06 44.579,65.087 C44.442,65.119 44.307,65.156 44.17,65.191 C43.943,65.25 43.716,65.315 43.492,65.383 C43.323,65.433 43.155,65.484 42.988,65.539 C42.819,65.595 42.65,65.652 42.483,65.713 C42.475,65.716 42.466,65.719 42.457,65.722 C35.819,68.158 31.022,74.369 30.649,81.774 C30.633,82.083 30.622,82.391 30.622,82.704 C30.622,83.014 30.631,83.321 30.649,83.626 C30.649,83.629 30.648,83.632 30.649,83.635 C30.662,83.862 30.681,84.088 30.701,84.313 C31.466,93.037 38.377,99.948 47.101,100.713 C47.326,100.733 47.552,100.754 47.779,100.765 C47.782,100.765 47.785,100.765 47.788,100.765 C48.093,100.783 48.399,100.791 48.709,100.791 C53.639,100.791 58.096,98.833 61.353,95.652 C61.532,95.477 61.712,95.304 61.883,95.122 C61.913,95.09 61.941,95.058 61.97,95.026 C61.98,95.015 61.987,95.002 61.996,94.991 C62.132,94.845 62.266,94.698 62.396,94.548 C62.449,94.487 62.501,94.426 62.553,94.365 C62.594,94.316 62.634,94.267 62.675,94.217 C62.821,94.04 62.961,93.861 63.101,93.678 C63.279,93.444 63.456,93.199 63.622,92.956 C63.956,92.471 64.267,91.97 64.553,91.452 C64.661,91.257 64.757,91.06 64.857,90.861 C64.89,90.796 64.93,90.735 64.962,90.67 C64.98,90.633 64.996,90.594 65.014,90.556 C65.125,90.324 65.234,90.09 65.336,89.852 C65.349,89.82 65.365,89.789 65.379,89.756 C65.48,89.517 65.575,89.271 65.666,89.026 C65.678,88.994 65.689,88.962 65.701,88.93 C65.792,88.679 65.881,88.43 65.962,88.174 C65.97,88.148 65.98,88.122 65.988,88.096 C66.069,87.832 66.144,87.564 66.214,87.296 C66.219,87.275 66.226,87.255 66.231,87.235 C66.301,86.962 66.365,86.686 66.423,86.409 C66.426,86.391 66.428,86.374 66.431,86.356 C66.445,86.291 66.453,86.223 66.466,86.156 C66.511,85.925 66.552,85.695 66.588,85.461 C66.632,85.169 66.671,84.878 66.701,84.583 C66.701,84.574 66.701,84.565 66.701,84.556 C66.731,84.258 66.755,83.955 66.77,83.652 C66.77,83.646 66.77,83.641 66.77,83.635 C66.786,83.326 66.797,83.017 66.797,82.704 C66.797,72.69 58.723,64.617 48.71,64.617 z"/>
<path d="M62.936,99.809 C59.074,103.028 54.115,104.965 48.71,104.965 C47.101,104.965 45.535,104.787 44.023,104.461 L42.466,106.357 C39.007,105.43 35.855,103.781 33.179,101.574 L28.996,105.765 L29.51,108.861 L13.953,124.426 L15.457,125 L30.066,119.435 L44.675,125 L59.283,119.435 L64.849,104.826 L62.936,99.809 z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,32 @@
{
"images": {
"render": "{image_carousel()}{image_upload()}"
},
"osmlink": {
"render": "<a href='https://openstreetmap.org/{id}' target='_blank'><img src='./assets/osm-logo-us.svg' alt='OSM'/></a>",
"mappings":[{
"if": "id~=-",
"then": "<span class='alert'>Uploading...</alert>"
}]
},
"wikipedialink": {
"render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/wikipedia.svg' alt='WP'/></a>",
"condition": "wikipedia~*"
},
"website": {
"question": {
"en": "What is the website of {name}?",
"nl": "Wat is de website van {name}?",
"fr": "Quel est le site internet de {name}?",
"gl": "Cal é a páxina web de {name}?"
},
"render": "<a href='{website}' target='_blank'>{website}</a>",
"freeform": {
"key": "website",
"type": "url"
}
}
}

View file

@ -13,7 +13,7 @@
"description": {
"en": "On this map, one can find and mark nearby defibrillators",
"ca": "En aquest mapa , qualsevol pot trobar i marcar els desfibril·ladors externs automàtics més propers",
"en": "En este mapa , cualquiera puede encontrar y marcar los desfibriladores externos automáticos más cercanos",
"es": "En este mapa , cualquiera puede encontrar y marcar los desfibriladores externos automáticos más cercanos",
"fr": "Sur cette carte, vous pouvez trouver et améliorer les informations sur les défibrillateurs",
"nl": "Op deze kaart kan je informatie over AEDs vinden en verbeteren",
"de": "Auf dieser Karte kann man nahe gelegene Defibrillatoren finden und markieren"
@ -71,12 +71,12 @@
}
],
"tagRenderings": [
"pictures",
"images",
{
"question": {
"en": "Is this defibrillator located indoors?",
"ca": "Està el desfibril·lador a l'interior?",
"en": "¿Esté el desfibrilador en interior?",
"es": "¿Esté el desfibrilador en interior?",
"fr": "Ce défibrillateur est-il disposé en intérieur ?",
"nl": "Hangt deze defibrillator binnen of buiten?",
"de": "Befindet sich dieser Defibrillator im Gebäude?"

View file

@ -81,7 +81,7 @@
}
],
"tagRenderings": [
"pictures",
"images",
{
"render": {
"en": "This is a {artwork_type}",

View file

@ -63,7 +63,7 @@
"nl": "Wat is de naam van deze frituur?",
"fr": "Quel est le nom de cette friterie?"
},
"feeform": {
"freeform": {
"key": "name"
}
},

View file

@ -77,7 +77,7 @@
}
],
"tagRenderings": [
"pictures",
"images",
{
"question": {
"en": "Are these toilets publicly accessible?",

View file

@ -9,9 +9,8 @@ import Locale from "./UI/i18n/Locale";
import svg2img from 'promise-svg2img';
import Translation from "./UI/i18n/Translation";
import Translations from "./UI/i18n/Translations";
import {TagRendering} from "./UI/Popup/TagRendering";
TagRendering.injectFunction();
console.log("Building the layouts")
function enc(str: string): string {

View file

@ -3,6 +3,7 @@
width: 100%;
height: 100%;
text-align: center;
word-break: normal;
}
.oh-table th {
@ -169,7 +170,7 @@
/**** Opening hours visualization table ****/
.ohviz-table {
word-break: normal;
}
.ohviz-range {
@ -279,6 +280,7 @@
.ohviz-weekday {
padding-left: 0.5em;
word-break: normal;
}

109
css/tagrendering.css Normal file
View file

@ -0,0 +1,109 @@
.featureinfobox-title {
background-color: deeppink;
}
.featureinfobox-icons img{
max-height: 1.5em;
}
.featureinfobox-icons {
background-color: red;
}
.featureinfobox-titlebar{
font-size: large;
font-weight: bold;
display: flex;
justify-content: space-between;
}
.answer {
display: flex;
width: 100%;
font-size: large;
justify-content: space-between;
word-break: break-word;
}
.question .form-text-field > input {
width: 100%;
box-sizing: border-box;
}
.question {
display: block;
margin-top: 1em;
background-color: #e5f5ff;
padding: 1em;
border-radius: 1em;
font-size: larger;
}
.question-text {
font-size: larger;
font-weight: bold;
margin-bottom: 0.5em;
}
.question-text img {
max-width: 100%;
}
.question-subtext {
font-size: medium;
font-weight: normal;
}
.question-option-with-border {
border: 2px solid lightgray;
border-radius: 0.5em;
display: inline-block;
width: 90%;
box-sizing: border-box;
padding: 0.5em;
}
input:checked + label .question-option-with-border {
border: 2px solid black;
}
.save {
display: inline-block;
border: solid white 2px;
background-color: #3a3aeb;
color: white;
padding: 0.2em 0.6em;
font-size: x-large;
font-weight: bold;
border-radius: 1.5em;
}
.cancel {
display: inline-block;
border: solid black 0.5px;
padding: 0.2em 0.3em;
border-radius: 1.5em;
}
.login-button-friendly {
display: inline-block;
border: solid white 2px;
background-color: #3a3aeb;
color: white;
padding: 0.2em 0.6em;
font-size: large;
font-weight: bold;
border-radius: 1.5em;
box-sizing: border-box;
width: 100%;
}
.save-non-active {
display: inline-block;
border: solid lightgrey 2px;
color: grey;
padding: 0.2em 0.3em;
font-size: x-large;
font-weight: bold;
border-radius: 1.5em;
}

View file

@ -4,7 +4,6 @@ import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson";
import {OsmConnection} from "./Logic/Osm/OsmConnection";
import CustomGeneratorPanel from "./UI/CustomGenerator/CustomGeneratorPanel";
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
import {TagRendering} from "./UI/Popup/TagRendering";
let layout = GenerateEmpty.createEmptyLayout();
if (window.location.hash.length > 10) {
@ -17,8 +16,6 @@ if (window.location.hash.length > 10) {
}
}
TagRendering.injectFunction();
const connection = new OsmConnection(false, new UIEventSource<string>(undefined), "customGenerator", false);
new CustomGeneratorPanel(connection, layout)

104
index.css
View file

@ -338,115 +338,15 @@ body {
width: 40em !important;
max-height: 80vh;
overflow-y: auto;
overflow-x: hidden;
}
.featureinfoboxtitle span {
width: unset !important;
}
.question .form-text-field > input {
width: 100%;
box-sizing: border-box;
}
.osm-logo path {
fill: #7ebc6f;
}
.infoboxcontents {
margin: 1em 0.5em 0.5em;
}
.infobox-information {
width: 100%;
margin-top: 1em;
}
.question {
display: block;
margin-top: 1em;
background-color: #e5f5ff;
padding: 1em;
border-radius: 1em;
font-size: larger;
}
.question-text {
font-size: larger;
font-weight: bold;
}
.question-text img {
max-width: 100%;
}
.question-subtext {
font-size: medium;
font-weight: normal;
}
.question-option-with-border{
border: 2px solid lightgray;
border-radius: 0.5em;
display: inline-block;
width: 90%;
box-sizing: border-box;
padding: 1em;
}
input:checked+label .question-option-with-border{
border: 2px solid black;
}
/**** The save button *****/
.save {
display: inline-block;
border: solid white 2px;
background-color: #3a3aeb;
color: white;
padding: 0.2em 0.6em;
font-size: x-large;
font-weight: bold;
border-radius: 1.5em;
}
.login-button-friendly {
display: inline-block;
border: solid white 2px;
background-color: #3a3aeb;
color: white;
padding: 0.2em 0.6em;
font-size: large;
font-weight: bold;
border-radius: 1.5em;
box-sizing: border-box;
width: 100%;
}
.save-non-active {
display: inline-block;
border: solid lightgrey 2px;
color: grey;
padding: 0.2em 0.3em;
font-size: x-large;
font-weight: bold;
border-radius: 1.5em;
}
/****** ShareScreen *****/
.literal-code {
display: inline-block;
background-color: lightgray;
padding: 0.5em;
word-break: break-all;
word-break: break-word;
color: black;
box-sizing: border-box;
}

View file

@ -12,6 +12,7 @@
<link rel="stylesheet" href="./css/slideshow.css"/>
<link rel="stylesheet" href="./css/mobile.css"/>
<link rel="stylesheet" href="./css/openinghourstable.css"/>
<link rel="stylesheet" href="./css/tagrendering.css"/>
<!-- $$$CUSTOM-CSS -->
<link rel="manifest" href="./manifest.manifest">
<link rel="icon" href="assets/add.svg" sizes="any" type="image/svg+xml">

View file

@ -6,9 +6,7 @@ import {QueryParameters} from "./Logic/Web/QueryParameters";
import {UIEventSource} from "./Logic/UIEventSource";
import * as $ from "jquery";
import {FromJSON} from "./Customizations/JSON/FromJSON";
import {TagRendering} from "./UI/Popup/TagRendering";
TagRendering.injectFunction();
import SharedLayers from "./Customizations/SharedLayers";
let defaultLayout = "bookcases"
// --------------------- Special actions based on the parameters -----------------
@ -96,7 +94,7 @@ if (layoutFromBase64.startsWith("wiki:")) {
.firstChild.textContent;
try {
console.log("DOWNLOADED:",layoutJson);
const layout = FromJSON.LayoutFromJSON(JSON.parse(layoutJson));
const layout = Layout.LayoutFromJSON(JSON.parse(layoutJson), SharedLayers.sharedLayers);
layout.id = layoutFromBase64;
InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(layoutJson));
} catch (e) {

View file

@ -6,6 +6,7 @@
<link href="css/slideshow.css" rel="stylesheet"/>
<link href="css/tabbedComponent.css" rel="stylesheet"/>
<link href="css/openinghourstable.css" rel="stylesheet"/>
<link href="css/tagrendering.css" rel="stylesheet"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<style>
.tag-input-row {

50
test.ts
View file

@ -1,33 +1,35 @@
/*
//*
import OpeningHoursPickerTable from "./UI/Input/OpeningHours/OpeningHoursPickerTable";
import {UIElement} from "./UI/UIElement";
import {UIEventSource} from "./Logic/UIEventSource";
import {OpeningHour} from "./Logic/OpeningHours";
import {TagRendering} from "./UI/Popup/TagRendering";
import {Tag} from "./Logic/Tags";
import {TagRenderingConfigJson} from "./Customizations/JSON/TagRenderingConfigJson";
import TagRenderingConfig from "./Customizations/JSON/TagRenderingConfig";
import Locale from "./UI/i18n/Locale";
import EditableTagRendering from "./UI/Popup/EditableTagRendering";
const tagRendering: TagRenderingConfigJson = {
question: {"en": "What is the name of?", nl: "Wat is de naam van?", fr: "C'est quoi le nom"},
mappings: [
{
if: "noname=yes",
then: "Has no name"
}
],
render: "The name is {name}",
freeform: {
key: "name",
type: "string",
addExtraTags: ["noname="]
}//*/
}
const tr = new TagRendering(
new UIEventSource<any>({
id: "node/-1",
amenity: "bench"
}),
{
question: "Does this bench have a backrest?",
mappings: [{
k: new Tag("backrest", "yes"),
txt: "Has backrest"
},
{
k: new Tag("backrest", "no"),
txt: "Has no backrest"
}]
}
)
tr.AttachTo("maindiv")
const config = new TagRenderingConfig(tagRendering)
const tags = new UIEventSource({id: "node/-1", "amenity": "bench", name: "pietervdvn"})
// new TagRenderingQuestion(tags, config).AttachTo("maindiv")
new EditableTagRendering(tags, config).AttachTo('maindiv')
Locale.CreateLanguagePicker(["nl", "en", "fr"]).AttachTo("extradiv")
/*/

View file

@ -1,5 +1,4 @@
import {UIElement} from "../UI/UIElement";
UIElement.runningFromConsole = true;
import {equal} from "assert";
import Translation from "../UI/i18n/Translation";
import T from "./TestHelper";
@ -8,10 +7,11 @@ import {And, Tag} from "../Logic/Tags";
import Locale from "../UI/i18n/Locale";
import Translations from "../UI/i18n/Translations";
import {UIEventSource} from "../Logic/UIEventSource";
import {TagRendering} from "../UI/TagRendering";
import {OH, OpeningHour} from "../Logic/OpeningHours";
import PublicHolidayInput from "../UI/Input/OpeningHours/PublicHolidayInput";
import {TagRendering} from "../UI/Popup/TagRendering";
UIElement.runningFromConsole = true;
new T([
@ -104,18 +104,17 @@ new T([
"hideInAnswer": true
},
{
"if":"access=no",
"then":"Niet toegankelijk"
"if": "access=no",
"then": "Niet toegankelijk"
}
]
};
const constr = FromJSON.TagRendering(def, "test");
TagRendering.injectFunction();
const uiEl = constr.construct({
tags: new UIEventSource<any>(
{leisure: "park", "access": "no"})
});
const uiEl = constr.construct(new UIEventSource<any>(
{leisure: "park", "access": "no"})
);
const rendered = uiEl.InnerRender();
equal(true, rendered.indexOf("Niet toegankelijk") > 0)
@ -292,11 +291,11 @@ new T([
const rules = OH.ParseRule("Th[-1] off");
equal(rules, null);
}],
["OHNo parsePH 12:00-17:00",() => {
["OHNo parsePH 12:00-17:00", () => {
const rules = OH.ParseRule("PH 12:00-17:00");
equal(rules, null);
}],
["OH Parse PH 12:00-17:00",() => {
["OH Parse PH 12:00-17:00", () => {
const rules = PublicHolidayInput.LoadValue("PH 12:00-17:00");
equal(rules.mode, " ");
}]