forked from MapComplete/MapComplete
Reformat all files with prettier
This commit is contained in:
parent
e22d189376
commit
b541d3eab4
382 changed files with 50893 additions and 35566 deletions
|
@ -1,12 +1,12 @@
|
|||
import {TileLayer} from "leaflet";
|
||||
import { TileLayer } from "leaflet"
|
||||
|
||||
export default interface BaseLayer {
|
||||
id: string,
|
||||
name: string,
|
||||
layer: () => TileLayer,
|
||||
max_zoom: number,
|
||||
min_zoom: number;
|
||||
feature: any,
|
||||
isBest?: boolean,
|
||||
id: string
|
||||
name: string
|
||||
layer: () => TileLayer
|
||||
max_zoom: number
|
||||
min_zoom: number
|
||||
feature: any
|
||||
isBest?: boolean
|
||||
category?: "map" | "osmbasedmap" | "photo" | "historicphoto" | string
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import {Utils} from "../Utils";
|
||||
import { Utils } from "../Utils"
|
||||
|
||||
export default class Constants {
|
||||
public static vNumber = "0.23.2"
|
||||
|
||||
public static vNumber = "0.23.2";
|
||||
|
||||
public static ImgurApiKey = '7070e7167f0a25a'
|
||||
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
||||
public static ImgurApiKey = "7070e7167f0a25a"
|
||||
public static readonly mapillary_client_token_v4 =
|
||||
"MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
||||
|
||||
/**
|
||||
* API key for Maproulette
|
||||
*
|
||||
*
|
||||
* Currently there is no user-friendly way to get the user's API key.
|
||||
* See https://github.com/maproulette/maproulette2/issues/476 for more information.
|
||||
* Using an empty string however does work for most actions, but will attribute all actions to the Superuser.
|
||||
*/
|
||||
public static readonly MaprouletteApiKey = "";
|
||||
public static readonly MaprouletteApiKey = ""
|
||||
|
||||
public static defaultOverpassUrls = [
|
||||
// The official instance, 10000 queries per day per project allowed
|
||||
|
@ -26,14 +26,30 @@ export default class Constants {
|
|||
// Doesn't support nwr: "https://overpass.openstreetmap.fr/api/interpreter"
|
||||
]
|
||||
|
||||
|
||||
public static readonly added_by_default: string[] = ["gps_location", "gps_location_history", "home_location", "gps_track"]
|
||||
public static readonly no_include: string[] = ["conflation", "left_right_style", "split_point", "current_view", "matchpoint"]
|
||||
public static readonly added_by_default: string[] = [
|
||||
"gps_location",
|
||||
"gps_location_history",
|
||||
"home_location",
|
||||
"gps_track",
|
||||
]
|
||||
public static readonly no_include: string[] = [
|
||||
"conflation",
|
||||
"left_right_style",
|
||||
"split_point",
|
||||
"current_view",
|
||||
"matchpoint",
|
||||
]
|
||||
/**
|
||||
* Layer IDs of layers which have special properties through built-in hooks
|
||||
*/
|
||||
public static readonly priviliged_layers: string[] = [...Constants.added_by_default, "type_node", "note", "import_candidate", "direction", ...Constants.no_include]
|
||||
|
||||
public static readonly priviliged_layers: string[] = [
|
||||
...Constants.added_by_default,
|
||||
"type_node",
|
||||
"note",
|
||||
"import_candidate",
|
||||
"direction",
|
||||
...Constants.no_include,
|
||||
]
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
|
@ -48,45 +64,66 @@ export default class Constants {
|
|||
themeGeneratorReadOnlyUnlock: 50,
|
||||
themeGeneratorFullUnlock: 500,
|
||||
addNewPointWithUnreadMessagesUnlock: 500,
|
||||
minZoomLevelToAddNewPoints: (Constants.isRetina() ? 18 : 19),
|
||||
minZoomLevelToAddNewPoints: Constants.isRetina() ? 18 : 19,
|
||||
|
||||
importHelperUnlock: 5000
|
||||
};
|
||||
importHelperUnlock: 5000,
|
||||
}
|
||||
/**
|
||||
* Used by 'PendingChangesUploader', which waits this amount of seconds to upload changes.
|
||||
* (Note that pendingChanges might upload sooner if the popup is closed or similar)
|
||||
*/
|
||||
static updateTimeoutSec: number = 30;
|
||||
static updateTimeoutSec: number = 30
|
||||
/**
|
||||
* If the contributor has their GPS location enabled and makes a change,
|
||||
* the points visited less then `nearbyVisitTime`-seconds ago will be inspected.
|
||||
* The point closest to the changed feature will be considered and this distance will be tracked.
|
||||
* ALl these distances are used to calculate a nearby-score
|
||||
*/
|
||||
static nearbyVisitTime: number = 30 * 60;
|
||||
static nearbyVisitTime: number = 30 * 60
|
||||
/**
|
||||
* If a user makes a change, the distance to the changed object is calculated.
|
||||
* If a user makes multiple changes, all these distances are put into multiple bins, depending on this distance.
|
||||
* For every bin, the totals are uploaded as metadata
|
||||
*/
|
||||
static distanceToChangeObjectBins = [25, 50, 100, 500, 1000, 5000, Number.MAX_VALUE]
|
||||
static themeOrder = ["personal", "cyclofix", "waste" , "etymology", "food","cafes_and_pubs", "playgrounds", "hailhydrant", "toilets", "aed", "bookcases"];
|
||||
static themeOrder = [
|
||||
"personal",
|
||||
"cyclofix",
|
||||
"waste",
|
||||
"etymology",
|
||||
"food",
|
||||
"cafes_and_pubs",
|
||||
"playgrounds",
|
||||
"hailhydrant",
|
||||
"toilets",
|
||||
"aed",
|
||||
"bookcases",
|
||||
]
|
||||
/**
|
||||
* Upon initialization, the GPS will search the location.
|
||||
* If the location is found within the given timout, it'll automatically fly to it.
|
||||
*
|
||||
*
|
||||
* In seconds
|
||||
*/
|
||||
static zoomToLocationTimeout = 60;
|
||||
static countryCoderEndpoint: string = "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country";
|
||||
static zoomToLocationTimeout = 60
|
||||
static countryCoderEndpoint: string =
|
||||
"https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country"
|
||||
|
||||
private static isRetina(): boolean {
|
||||
if(Utils.runningFromConsole){
|
||||
return false;
|
||||
if (Utils.runningFromConsole) {
|
||||
return false
|
||||
}
|
||||
// The cause for this line of code: https://github.com/pietervdvn/MapComplete/issues/115
|
||||
// See https://stackoverflow.com/questions/19689715/what-is-the-best-way-to-detect-retina-support-on-a-device-using-javascript
|
||||
return ((window.matchMedia && (window.matchMedia('only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx), only screen and (min-resolution: 75.6dpcm)').matches || window.matchMedia('only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min--moz-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)').matches)) || (window.devicePixelRatio && window.devicePixelRatio >= 2));
|
||||
return (
|
||||
(window.matchMedia &&
|
||||
(window.matchMedia(
|
||||
"only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx), only screen and (min-resolution: 75.6dpcm)"
|
||||
).matches ||
|
||||
window.matchMedia(
|
||||
"only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min--moz-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)"
|
||||
).matches)) ||
|
||||
(window.devicePixelRatio && window.devicePixelRatio >= 2)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import {Translation} from "../UI/i18n/Translation";
|
||||
import {DenominationConfigJson} from "./ThemeConfig/Json/UnitConfigJson";
|
||||
import Translations from "../UI/i18n/Translations";
|
||||
import {Store} from "../Logic/UIEventSource";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import Toggle from "../UI/Input/Toggle";
|
||||
import { Translation } from "../UI/i18n/Translation"
|
||||
import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson"
|
||||
import Translations from "../UI/i18n/Translations"
|
||||
import { Store } from "../Logic/UIEventSource"
|
||||
import BaseUIElement from "../UI/BaseUIElement"
|
||||
import Toggle from "../UI/Input/Toggle"
|
||||
|
||||
export class Denomination {
|
||||
public readonly canonical: string;
|
||||
public readonly _canonicalSingular: string;
|
||||
public readonly canonical: string
|
||||
public readonly _canonicalSingular: string
|
||||
public readonly useAsDefaultInput: boolean | string[]
|
||||
public readonly useIfNoUnitGiven : boolean | string[]
|
||||
public readonly prefix: boolean;
|
||||
public readonly alternativeDenominations: string [];
|
||||
private readonly _human: Translation;
|
||||
private readonly _humanSingular?: Translation;
|
||||
|
||||
public readonly useIfNoUnitGiven: boolean | string[]
|
||||
public readonly prefix: boolean
|
||||
public readonly alternativeDenominations: string[]
|
||||
private readonly _human: Translation
|
||||
private readonly _humanSingular?: Translation
|
||||
|
||||
constructor(json: DenominationConfigJson, context: string) {
|
||||
context = `${context}.unit(${json.canonicalDenomination})`
|
||||
|
@ -24,26 +23,24 @@ export class Denomination {
|
|||
}
|
||||
this._canonicalSingular = json.canonicalDenominationSingular?.trim()
|
||||
|
||||
|
||||
json.alternativeDenomination.forEach((v, i) => {
|
||||
if (((v?.trim() ?? "") === "")) {
|
||||
if ((v?.trim() ?? "") === "") {
|
||||
throw `${context}.alternativeDenomination.${i}: invalid alternative denomination: undefined, null or only whitespace`
|
||||
}
|
||||
})
|
||||
|
||||
this.alternativeDenominations = json.alternativeDenomination?.map(v => v.trim()) ?? []
|
||||
this.alternativeDenominations = json.alternativeDenomination?.map((v) => v.trim()) ?? []
|
||||
|
||||
if(json["default"] !== undefined) {
|
||||
if (json["default"] !== undefined) {
|
||||
throw `${context} uses the old 'default'-key. Use "useIfNoUnitGiven" or "useAsDefaultInput" instead`
|
||||
}
|
||||
this.useIfNoUnitGiven = json.useIfNoUnitGiven
|
||||
this.useAsDefaultInput = json.useAsDefaultInput ?? json.useIfNoUnitGiven
|
||||
|
||||
|
||||
this._human = Translations.T(json.human, context + "human")
|
||||
this._humanSingular = Translations.T(json.humanSingular, context + "humanSingular")
|
||||
|
||||
this.prefix = json.prefix ?? false;
|
||||
|
||||
this.prefix = json.prefix ?? false
|
||||
}
|
||||
|
||||
get human(): Translation {
|
||||
|
@ -58,18 +55,14 @@ export class Denomination {
|
|||
if (this._humanSingular === undefined) {
|
||||
return this.human
|
||||
}
|
||||
return new Toggle(
|
||||
this.humanSingular,
|
||||
this.human,
|
||||
isSingular
|
||||
)
|
||||
return new Toggle(this.humanSingular, this.human, isSingular)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a representation of the given value
|
||||
* @param value: the value from OSM
|
||||
* @param actAsDefault: if set and the value can be parsed as number, will be parsed and trimmed
|
||||
*
|
||||
*
|
||||
* const unit = new Denomination({
|
||||
* canonicalDenomination: "m",
|
||||
* alternativeDenomination: ["meter"],
|
||||
|
@ -83,7 +76,7 @@ export class Denomination {
|
|||
* unit.canonicalValue("42 meter", true) // =>"42 m"
|
||||
* unit.canonicalValue("42m", true) // =>"42 m"
|
||||
* unit.canonicalValue("42", true) // =>"42 m"
|
||||
*
|
||||
*
|
||||
* // Should be trimmed if canonical is empty
|
||||
* const unit = new Denomination({
|
||||
* canonicalDenomination: "",
|
||||
|
@ -97,18 +90,18 @@ export class Denomination {
|
|||
* unit.canonicalValue("42 m", true) // =>"42"
|
||||
* unit.canonicalValue("42 meter", true) // =>"42"
|
||||
*/
|
||||
public canonicalValue(value: string, actAsDefault: boolean) : string {
|
||||
public canonicalValue(value: string, actAsDefault: boolean): string {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const stripped = this.StrippedValue(value, actAsDefault)
|
||||
if (stripped === null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (stripped === "1" && this._canonicalSingular !== undefined) {
|
||||
return ("1 " + this._canonicalSingular).trim()
|
||||
}
|
||||
return (stripped + " " + this.canonical).trim();
|
||||
return (stripped + " " + this.canonical).trim()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -119,13 +112,12 @@ export class Denomination {
|
|||
* Returns null if it doesn't match this unit
|
||||
*/
|
||||
public StrippedValue(value: string, actAsDefault: boolean): string {
|
||||
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
value = value.toLowerCase()
|
||||
const self = this;
|
||||
const self = this
|
||||
|
||||
function startsWith(key) {
|
||||
if (self.prefix) {
|
||||
|
@ -147,36 +139,39 @@ export class Denomination {
|
|||
return substr(this.canonical)
|
||||
}
|
||||
|
||||
if (this._canonicalSingular !== undefined && this._canonicalSingular !== "" && startsWith(this._canonicalSingular)) {
|
||||
if (
|
||||
this._canonicalSingular !== undefined &&
|
||||
this._canonicalSingular !== "" &&
|
||||
startsWith(this._canonicalSingular)
|
||||
) {
|
||||
return substr(this._canonicalSingular)
|
||||
}
|
||||
|
||||
for (const alternativeValue of this.alternativeDenominations) {
|
||||
if (startsWith(alternativeValue)) {
|
||||
return substr(alternativeValue);
|
||||
return substr(alternativeValue)
|
||||
}
|
||||
}
|
||||
|
||||
if (!actAsDefault) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const parsed = Number(value.trim())
|
||||
if (!isNaN(parsed)) {
|
||||
return value.trim();
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
isDefaultUnit(country: () => string) {
|
||||
if(this.useIfNoUnitGiven === true){
|
||||
if (this.useIfNoUnitGiven === true) {
|
||||
return true
|
||||
}
|
||||
if(this.useIfNoUnitGiven === false){
|
||||
if (this.useIfNoUnitGiven === false) {
|
||||
return false
|
||||
}
|
||||
return this.useIfNoUnitGiven.indexOf(country()) >= 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import LayerConfig from "./ThemeConfig/LayerConfig";
|
||||
import {TagsFilter} from "../Logic/Tags/TagsFilter";
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import LayerConfig from "./ThemeConfig/LayerConfig"
|
||||
import { TagsFilter } from "../Logic/Tags/TagsFilter"
|
||||
|
||||
export interface FilterState {
|
||||
currentFilter: TagsFilter,
|
||||
currentFilter: TagsFilter
|
||||
state: string | number
|
||||
}
|
||||
|
||||
export default interface FilteredLayer {
|
||||
readonly isDisplayed: UIEventSource<boolean>;
|
||||
readonly appliedFilters: UIEventSource<Map<string, FilterState>>;
|
||||
readonly layerDef: LayerConfig;
|
||||
}
|
||||
readonly isDisplayed: UIEventSource<boolean>
|
||||
readonly appliedFilters: UIEventSource<Map<string, FilterState>>
|
||||
readonly layerDef: LayerConfig
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export default interface LeafletMap {
|
||||
|
||||
getBounds(): [[number, number], [number, number]];
|
||||
}
|
||||
getBounds(): [[number, number], [number, number]]
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export default interface Loc {
|
||||
lat: number,
|
||||
lon: number,
|
||||
lat: number
|
||||
lon: number
|
||||
zoom: number
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {Feature, Geometry} from "@turf/turf";
|
||||
import { Feature, Geometry } from "@turf/turf"
|
||||
|
||||
export type RelationId = `relation/${number}`
|
||||
export type WayId = `way/${number}`
|
||||
export type NodeId = `node/${number}`
|
||||
export type OsmId = NodeId | WayId | RelationId
|
||||
|
||||
export type OsmTags = Record<string, string> & {id: string}
|
||||
export type OsmFeature = Feature<Geometry, OsmTags>
|
||||
export type OsmTags = Record<string, string> & { id: string }
|
||||
export type OsmFeature = Feature<Geometry, OsmTags>
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import {DesugaringStep} from "./Conversion";
|
||||
import {Utils} from "../../../Utils";
|
||||
import Translations from "../../../UI/i18n/Translations";
|
||||
import { DesugaringStep } from "./Conversion"
|
||||
import { Utils } from "../../../Utils"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
|
||||
export class AddContextToTranslations<T> extends DesugaringStep<T> {
|
||||
private readonly _prefix: string;
|
||||
private readonly _prefix: string
|
||||
|
||||
constructor(prefix = "") {
|
||||
super("Adds a '_context' to every object that is probably a translation", ["_context"], "AddContextToTranslation");
|
||||
this._prefix = prefix;
|
||||
super(
|
||||
"Adds a '_context' to every object that is probably a translation",
|
||||
["_context"],
|
||||
"AddContextToTranslation"
|
||||
)
|
||||
this._prefix = prefix
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -21,7 +25,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
|
|||
* }
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* ]
|
||||
* }
|
||||
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
|
||||
* const expected = {
|
||||
|
@ -35,10 +39,10 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
|
|||
* }
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* ]
|
||||
* }
|
||||
* rewritten // => expected
|
||||
*
|
||||
*
|
||||
* // should use the ID if one is present instead of the index
|
||||
* const theme = {
|
||||
* layers: [
|
||||
|
@ -51,7 +55,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
|
|||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* ]
|
||||
* }
|
||||
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
|
||||
* const expected = {
|
||||
|
@ -66,10 +70,10 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
|
|||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* ]
|
||||
* }
|
||||
* rewritten // => expected
|
||||
*
|
||||
*
|
||||
* // should preserve nulls
|
||||
* const theme = {
|
||||
* layers: [
|
||||
|
@ -79,7 +83,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
|
|||
* name:null
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* ]
|
||||
* }
|
||||
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
|
||||
* const expected = {
|
||||
|
@ -90,11 +94,11 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
|
|||
* name: null
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* ]
|
||||
* }
|
||||
* rewritten // => expected
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* // Should ignore all if '#dont-translate' is set
|
||||
* const theme = {
|
||||
* "#dont-translate": "*",
|
||||
|
@ -107,43 +111,47 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
|
|||
* }
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* ]
|
||||
* }
|
||||
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
|
||||
* rewritten // => theme
|
||||
*
|
||||
*
|
||||
*/
|
||||
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
|
||||
if(json["#dont-translate"] === "*"){
|
||||
return {result: json}
|
||||
convert(
|
||||
json: T,
|
||||
context: string
|
||||
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
if (json["#dont-translate"] === "*") {
|
||||
return { result: json }
|
||||
}
|
||||
|
||||
const result = Utils.WalkJson(json, (leaf, path) => {
|
||||
if(leaf === undefined || leaf === null){
|
||||
return leaf
|
||||
}
|
||||
if (typeof leaf === "object") {
|
||||
|
||||
// follow the path. If we encounter a number, check that there is no ID we can use instead
|
||||
let breadcrumb = json;
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const pointer = path[i]
|
||||
breadcrumb = breadcrumb[pointer]
|
||||
if(pointer.match("[0-9]+") && breadcrumb["id"] !== undefined){
|
||||
path[i] = breadcrumb["id"]
|
||||
}
|
||||
|
||||
const result = Utils.WalkJson(
|
||||
json,
|
||||
(leaf, path) => {
|
||||
if (leaf === undefined || leaf === null) {
|
||||
return leaf
|
||||
}
|
||||
|
||||
return {...leaf, _context: this._prefix + context + "." + path.join(".")}
|
||||
} else {
|
||||
return leaf
|
||||
}
|
||||
}, obj => obj === undefined || obj === null || Translations.isProbablyATranslation(obj))
|
||||
if (typeof leaf === "object") {
|
||||
// follow the path. If we encounter a number, check that there is no ID we can use instead
|
||||
let breadcrumb = json
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const pointer = path[i]
|
||||
breadcrumb = breadcrumb[pointer]
|
||||
if (pointer.match("[0-9]+") && breadcrumb["id"] !== undefined) {
|
||||
path[i] = breadcrumb["id"]
|
||||
}
|
||||
}
|
||||
|
||||
return { ...leaf, _context: this._prefix + context + "." + path.join(".") }
|
||||
} else {
|
||||
return leaf
|
||||
}
|
||||
},
|
||||
(obj) => obj === undefined || obj === null || Translations.isProbablyATranslation(obj)
|
||||
)
|
||||
|
||||
return {
|
||||
result
|
||||
};
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +1,41 @@
|
|||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import {Utils} from "../../../Utils";
|
||||
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
|
||||
export interface DesugaringContext {
|
||||
tagRenderings: Map<string, TagRenderingConfigJson>
|
||||
sharedLayers: Map<string, LayerConfigJson>,
|
||||
sharedLayers: Map<string, LayerConfigJson>
|
||||
publicLayers?: Set<string>
|
||||
}
|
||||
|
||||
export abstract class Conversion<TIn, TOut> {
|
||||
public readonly modifiedAttributes: string[];
|
||||
public readonly modifiedAttributes: string[]
|
||||
public readonly name: string
|
||||
protected readonly doc: string;
|
||||
protected readonly doc: string
|
||||
|
||||
constructor(doc: string, modifiedAttributes: string[] = [], name: string) {
|
||||
this.modifiedAttributes = modifiedAttributes;
|
||||
this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ");
|
||||
this.modifiedAttributes = modifiedAttributes
|
||||
this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ")
|
||||
this.name = name
|
||||
}
|
||||
|
||||
public static strict<T>(fixed: { errors?: string[], warnings?: string[], information?: string[], result?: T }): T {
|
||||
|
||||
fixed.information?.forEach(i => console.log(" ", i))
|
||||
public static strict<T>(fixed: {
|
||||
errors?: string[]
|
||||
warnings?: string[]
|
||||
information?: string[]
|
||||
result?: T
|
||||
}): T {
|
||||
fixed.information?.forEach((i) => console.log(" ", i))
|
||||
const yellow = (s) => "\x1b[33m" + s + "\x1b[0m"
|
||||
const red = s => '\x1b[31m' + s + '\x1b[0m'
|
||||
fixed.warnings?.forEach(w => console.warn(red(`<!> `), yellow(w)))
|
||||
const red = (s) => "\x1b[31m" + s + "\x1b[0m"
|
||||
fixed.warnings?.forEach((w) => console.warn(red(`<!> `), yellow(w)))
|
||||
|
||||
if (fixed?.errors !== undefined && fixed?.errors?.length > 0) {
|
||||
fixed.errors?.forEach(e => console.error(red(`ERR ` + e)))
|
||||
fixed.errors?.forEach((e) => console.error(red(`ERR ` + e)))
|
||||
throw "Detected one or more errors, stopping now"
|
||||
}
|
||||
|
||||
return fixed.result;
|
||||
return fixed.result
|
||||
}
|
||||
|
||||
public convertStrict(json: TIn, context: string): TOut {
|
||||
|
@ -39,7 +43,13 @@ export abstract class Conversion<TIn, TOut> {
|
|||
return DesugaringStep.strict(fixed)
|
||||
}
|
||||
|
||||
public convertJoin(json: TIn, context: string, errors: string[], warnings?: string[], information?: string[]): TOut {
|
||||
public convertJoin(
|
||||
json: TIn,
|
||||
context: string,
|
||||
errors: string[],
|
||||
warnings?: string[],
|
||||
information?: string[]
|
||||
): TOut {
|
||||
const fixed = this.convert(json, context)
|
||||
errors?.push(...(fixed.errors ?? []))
|
||||
warnings?.push(...(fixed.warnings ?? []))
|
||||
|
@ -47,41 +57,41 @@ export abstract class Conversion<TIn, TOut> {
|
|||
return fixed.result
|
||||
}
|
||||
|
||||
public andThenF<X>(f: (tout:TOut) => X ): Conversion<TIn, X>{
|
||||
return new Pipe(
|
||||
this,
|
||||
new Pure(f)
|
||||
)
|
||||
public andThenF<X>(f: (tout: TOut) => X): Conversion<TIn, X> {
|
||||
return new Pipe(this, new Pure(f))
|
||||
}
|
||||
|
||||
abstract convert(json: TIn, context: string): { result: TOut, errors?: string[], warnings?: string[], information?: string[] }
|
||||
|
||||
abstract convert(
|
||||
json: TIn,
|
||||
context: string
|
||||
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] }
|
||||
}
|
||||
|
||||
export abstract class DesugaringStep<T> extends Conversion<T, T> {
|
||||
|
||||
}
|
||||
export abstract class DesugaringStep<T> extends Conversion<T, T> {}
|
||||
|
||||
class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
|
||||
private readonly _step0: Conversion<TIn, TInter>;
|
||||
private readonly _step1: Conversion<TInter, TOut>;
|
||||
constructor(step0: Conversion<TIn, TInter>, step1: Conversion<TInter,TOut>) {
|
||||
super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`);
|
||||
this._step0 = step0;
|
||||
this._step1 = step1;
|
||||
private readonly _step0: Conversion<TIn, TInter>
|
||||
private readonly _step1: Conversion<TInter, TOut>
|
||||
constructor(step0: Conversion<TIn, TInter>, step1: Conversion<TInter, TOut>) {
|
||||
super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`)
|
||||
this._step0 = step0
|
||||
this._step1 = step1
|
||||
}
|
||||
|
||||
convert(json: TIn, context: string): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
|
||||
const r0 = this._step0.convert(json, context);
|
||||
const {result, errors, information, warnings } = r0;
|
||||
if(result === undefined && errors.length > 0){
|
||||
convert(
|
||||
json: TIn,
|
||||
context: string
|
||||
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
const r0 = this._step0.convert(json, context)
|
||||
const { result, errors, information, warnings } = r0
|
||||
if (result === undefined && errors.length > 0) {
|
||||
return {
|
||||
...r0,
|
||||
result: undefined
|
||||
};
|
||||
result: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const r = this._step1.convert(result, context);
|
||||
|
||||
const r = this._step1.convert(result, context)
|
||||
errors.push(...r.errors)
|
||||
information.push(...r.information)
|
||||
warnings.push(...r.warnings)
|
||||
|
@ -89,35 +99,44 @@ class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
|
|||
result: r.result,
|
||||
errors,
|
||||
warnings,
|
||||
information
|
||||
information,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Pure<TIn, TOut> extends Conversion<TIn, TOut> {
|
||||
private readonly _f: (t: TIn) => TOut;
|
||||
constructor(f: ((t:TIn) => TOut)) {
|
||||
super("Wrapper around a pure function",[], "Pure");
|
||||
this._f = f;
|
||||
private readonly _f: (t: TIn) => TOut
|
||||
constructor(f: (t: TIn) => TOut) {
|
||||
super("Wrapper around a pure function", [], "Pure")
|
||||
this._f = f
|
||||
}
|
||||
|
||||
convert(json: TIn, context: string): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
return {result: this._f(json)};
|
||||
|
||||
convert(
|
||||
json: TIn,
|
||||
context: string
|
||||
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
return { result: this._f(json) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Each<X, Y> extends Conversion<X[], Y[]> {
|
||||
private readonly _step: Conversion<X, Y>;
|
||||
private readonly _step: Conversion<X, Y>
|
||||
|
||||
constructor(step: Conversion<X, Y>) {
|
||||
super("Applies the given step on every element of the list", [], "OnEach(" + step.name + ")");
|
||||
this._step = step;
|
||||
super(
|
||||
"Applies the given step on every element of the list",
|
||||
[],
|
||||
"OnEach(" + step.name + ")"
|
||||
)
|
||||
this._step = step
|
||||
}
|
||||
|
||||
convert(values: X[], context: string): { result: Y[]; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
convert(
|
||||
values: X[],
|
||||
context: string
|
||||
): { result: Y[]; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
if (values === undefined || values === null) {
|
||||
return {result: undefined}
|
||||
return { result: undefined }
|
||||
}
|
||||
const information: string[] = []
|
||||
const warnings: string[] = []
|
||||
|
@ -132,68 +151,83 @@ export class Each<X, Y> extends Conversion<X[], Y[]> {
|
|||
result.push(r.result)
|
||||
}
|
||||
return {
|
||||
information, errors, warnings,
|
||||
result
|
||||
};
|
||||
information,
|
||||
errors,
|
||||
warnings,
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class On<P, T> extends DesugaringStep<T> {
|
||||
private readonly key: string;
|
||||
private readonly step: ((t: T) => Conversion<P, P>);
|
||||
private readonly key: string
|
||||
private readonly step: (t: T) => Conversion<P, P>
|
||||
|
||||
constructor(key: string, step: Conversion<P, P> | ((t: T )=> Conversion<P, P>)) {
|
||||
super("Applies " + step.name + " onto property `"+key+"`", [key], `On(${key}, ${step.name})`);
|
||||
if(typeof step === "function"){
|
||||
this.step = step;
|
||||
}else{
|
||||
this.step = _ => step
|
||||
constructor(key: string, step: Conversion<P, P> | ((t: T) => Conversion<P, P>)) {
|
||||
super(
|
||||
"Applies " + step.name + " onto property `" + key + "`",
|
||||
[key],
|
||||
`On(${key}, ${step.name})`
|
||||
)
|
||||
if (typeof step === "function") {
|
||||
this.step = step
|
||||
} else {
|
||||
this.step = (_) => step
|
||||
}
|
||||
this.key = key;
|
||||
this.key = key
|
||||
}
|
||||
|
||||
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[], information?: string[] } {
|
||||
json = {...json}
|
||||
convert(
|
||||
json: T,
|
||||
context: string
|
||||
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
json = { ...json }
|
||||
const step = this.step(json)
|
||||
const key = this.key;
|
||||
const key = this.key
|
||||
const value: P = json[key]
|
||||
if (value === undefined || value === null) {
|
||||
return { result: json };
|
||||
return { result: json }
|
||||
}
|
||||
const r = step.convert(value, context + "." + key)
|
||||
json[key] = r.result
|
||||
return {
|
||||
...r,
|
||||
result: json,
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Pass<T> extends Conversion<T, T> {
|
||||
constructor(message?: string) {
|
||||
super(message??"Does nothing, often to swap out steps in testing", [], "Pass");
|
||||
super(message ?? "Does nothing, often to swap out steps in testing", [], "Pass")
|
||||
}
|
||||
|
||||
|
||||
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
convert(
|
||||
json: T,
|
||||
context: string
|
||||
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
return {
|
||||
result: json
|
||||
};
|
||||
result: json,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Concat<X, T> extends Conversion<X[], T[]> {
|
||||
private readonly _step: Conversion<X, T[]>;
|
||||
private readonly _step: Conversion<X, T[]>
|
||||
|
||||
constructor(step: Conversion<X, T[]>) {
|
||||
super("Executes the given step, flattens the resulting list", [], "Concat(" + step.name + ")");
|
||||
this._step = step;
|
||||
super(
|
||||
"Executes the given step, flattens the resulting list",
|
||||
[],
|
||||
"Concat(" + step.name + ")"
|
||||
)
|
||||
this._step = step
|
||||
}
|
||||
|
||||
convert(values: X[], context: string): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
convert(
|
||||
values: X[],
|
||||
context: string
|
||||
): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
if (values === undefined || values === null) {
|
||||
// Move on - nothing to see here!
|
||||
return {
|
||||
|
@ -208,56 +242,68 @@ export class Concat<X, T> extends Conversion<X[], T[]> {
|
|||
return {
|
||||
...r,
|
||||
result: flattened,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FirstOf<T, X> extends Conversion<T, X>{
|
||||
private readonly _conversion: Conversion<T, X[]>;
|
||||
|
||||
export class FirstOf<T, X> extends Conversion<T, X> {
|
||||
private readonly _conversion: Conversion<T, X[]>
|
||||
|
||||
constructor(conversion: Conversion<T, X[]>) {
|
||||
super("Picks the first result of the conversion step", [], "FirstOf("+conversion.name+")");
|
||||
this._conversion = conversion;
|
||||
super(
|
||||
"Picks the first result of the conversion step",
|
||||
[],
|
||||
"FirstOf(" + conversion.name + ")"
|
||||
)
|
||||
this._conversion = conversion
|
||||
}
|
||||
|
||||
convert(json: T, context: string): { result: X; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
const reslt = this._conversion.convert(json, context);
|
||||
convert(
|
||||
json: T,
|
||||
context: string
|
||||
): { result: X; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
const reslt = this._conversion.convert(json, context)
|
||||
return {
|
||||
...reslt,
|
||||
result: reslt.result[0]
|
||||
};
|
||||
result: reslt.result[0],
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Fuse<T> extends DesugaringStep<T> {
|
||||
private readonly steps: DesugaringStep<T>[];
|
||||
private readonly steps: DesugaringStep<T>[]
|
||||
|
||||
constructor(doc: string, ...steps: DesugaringStep<T>[]) {
|
||||
super((doc ?? "") + "This fused pipeline of the following steps: " + steps.map(s => s.name).join(", "),
|
||||
Utils.Dedup([].concat(...steps.map(step => step.modifiedAttributes))),
|
||||
"Fuse of " + steps.map(s => s.name).join(", ")
|
||||
);
|
||||
this.steps = Utils.NoNull(steps);
|
||||
super(
|
||||
(doc ?? "") +
|
||||
"This fused pipeline of the following steps: " +
|
||||
steps.map((s) => s.name).join(", "),
|
||||
Utils.Dedup([].concat(...steps.map((step) => step.modifiedAttributes))),
|
||||
"Fuse of " + steps.map((s) => s.name).join(", ")
|
||||
)
|
||||
this.steps = Utils.NoNull(steps)
|
||||
}
|
||||
|
||||
convert(json: T, context: string): { result: T; errors: string[]; warnings: string[], information: string[] } {
|
||||
convert(
|
||||
json: T,
|
||||
context: string
|
||||
): { result: T; errors: string[]; warnings: string[]; information: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
const information = []
|
||||
for (let i = 0; i < this.steps.length; i++) {
|
||||
const step = this.steps[i];
|
||||
try{
|
||||
const step = this.steps[i]
|
||||
try {
|
||||
let r = step.convert(json, "While running step " + step.name + ": " + context)
|
||||
errors.push(...r.errors ?? [])
|
||||
warnings.push(...r.warnings ?? [])
|
||||
information.push(...r.information ?? [])
|
||||
errors.push(...(r.errors ?? []))
|
||||
warnings.push(...(r.warnings ?? []))
|
||||
information.push(...(r.information ?? []))
|
||||
json = r.result
|
||||
if (errors.length > 0) {
|
||||
break;
|
||||
break
|
||||
}
|
||||
}catch(e){
|
||||
console.error("Step "+step.name+" failed due to ",e,e.stack);
|
||||
} catch (e) {
|
||||
console.error("Step " + step.name + " failed due to ", e, e.stack)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
@ -265,32 +311,31 @@ export class Fuse<T> extends DesugaringStep<T> {
|
|||
result: json,
|
||||
errors,
|
||||
warnings,
|
||||
information
|
||||
};
|
||||
information,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class SetDefault<T> extends DesugaringStep<T> {
|
||||
private readonly value: any;
|
||||
private readonly key: string;
|
||||
private readonly _overrideEmptyString: boolean;
|
||||
private readonly value: any
|
||||
private readonly key: string
|
||||
private readonly _overrideEmptyString: boolean
|
||||
|
||||
constructor(key: string, value: any, overrideEmptyString = false) {
|
||||
super("Sets " + key + " to a default value if undefined", [], "SetDefault of " + key);
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this._overrideEmptyString = overrideEmptyString;
|
||||
super("Sets " + key + " to a default value if undefined", [], "SetDefault of " + key)
|
||||
this.key = key
|
||||
this.value = value
|
||||
this._overrideEmptyString = overrideEmptyString
|
||||
}
|
||||
|
||||
convert(json: T, context: string): { result: T } {
|
||||
if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) {
|
||||
json = {...json}
|
||||
json = { ...json }
|
||||
json[this.key] = this.value
|
||||
}
|
||||
|
||||
return {
|
||||
result: json
|
||||
};
|
||||
result: json,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
import {Conversion} from "./Conversion";
|
||||
import LayerConfig from "../LayerConfig";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import Translations from "../../../UI/i18n/Translations";
|
||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson";
|
||||
import {Translation, TypedTranslation} from "../../../UI/i18n/Translation";
|
||||
import { Conversion } from "./Conversion"
|
||||
import LayerConfig from "../LayerConfig"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
|
||||
import { Translation, TypedTranslation } from "../../../UI/i18n/Translation"
|
||||
|
||||
export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> {
|
||||
/**
|
||||
* A closed note is included if it is less then 'n'-days closed
|
||||
* @private
|
||||
*/
|
||||
private readonly _includeClosedNotesDays: number;
|
||||
private readonly _includeClosedNotesDays: number
|
||||
|
||||
constructor(includeClosedNotesDays = 0) {
|
||||
super([
|
||||
"Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').",
|
||||
"The import buttons and matches will be based on the presets of the given theme",
|
||||
].join("\n\n"), [], "CreateNoteImportLayer")
|
||||
this._includeClosedNotesDays = includeClosedNotesDays;
|
||||
super(
|
||||
[
|
||||
"Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').",
|
||||
"The import buttons and matches will be based on the presets of the given theme",
|
||||
].join("\n\n"),
|
||||
[],
|
||||
"CreateNoteImportLayer"
|
||||
)
|
||||
this._includeClosedNotesDays = includeClosedNotesDays
|
||||
}
|
||||
|
||||
convert(layerJson: LayerConfigJson, context: string): { result: LayerConfigJson } {
|
||||
const t = Translations.t.importLayer;
|
||||
const t = Translations.t.importLayer
|
||||
|
||||
/**
|
||||
* The note itself will contain `tags=k=v;k=v;k=v;...
|
||||
|
@ -35,14 +39,16 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
|
|||
for (const tag of preset.tags) {
|
||||
const key = tag.key
|
||||
const value = tag.value
|
||||
const condition = "_tags~(^|.*;)" + key + "\=" + value + "($|;.*)"
|
||||
const condition = "_tags~(^|.*;)" + key + "=" + value + "($|;.*)"
|
||||
mustMatchAll.push(condition)
|
||||
}
|
||||
isShownIfAny.push({and: mustMatchAll})
|
||||
isShownIfAny.push({ and: mustMatchAll })
|
||||
}
|
||||
|
||||
const pointRenderings = (layerJson.mapRendering ?? []).filter(r => r !== null && r["location"] !== undefined);
|
||||
const firstRender = <PointRenderingConfigJson>(pointRenderings [0])
|
||||
const pointRenderings = (layerJson.mapRendering ?? []).filter(
|
||||
(r) => r !== null && r["location"] !== undefined
|
||||
)
|
||||
const firstRender = <PointRenderingConfigJson>pointRenderings[0]
|
||||
if (firstRender === undefined) {
|
||||
throw `Layer ${layerJson.id} does not have a pointRendering: ` + context
|
||||
}
|
||||
|
@ -50,7 +56,10 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
|
|||
|
||||
const importButton = {}
|
||||
{
|
||||
const translations = trs(t.importButton, {layerId: layer.id, title: layer.presets[0].title})
|
||||
const translations = trs(t.importButton, {
|
||||
layerId: layer.id,
|
||||
title: layer.presets[0].title,
|
||||
})
|
||||
for (const key in translations) {
|
||||
if (key !== "_context") {
|
||||
importButton[key] = "{" + translations[key] + "}"
|
||||
|
@ -70,116 +79,117 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
|
|||
}
|
||||
|
||||
function tr(translation: Translation) {
|
||||
return {...translation.translations, "_context": translation.context}
|
||||
return { ...translation.translations, _context: translation.context }
|
||||
}
|
||||
|
||||
function trs<T>(translation: TypedTranslation<T>, subs: T): object {
|
||||
return {...translation.Subs(subs).translations, "_context": translation.context}
|
||||
return { ...translation.Subs(subs).translations, _context: translation.context }
|
||||
}
|
||||
|
||||
const result: LayerConfigJson = {
|
||||
"id": "note_import_" + layer.id,
|
||||
id: "note_import_" + layer.id,
|
||||
// By disabling the name, the import-layers won't pollute the filter view "name": t.layerName.Subs({title: layer.title.render}).translations,
|
||||
"description": trs(t.description, {title: layer.title.render}),
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"id~*"
|
||||
]
|
||||
description: trs(t.description, { title: layer.title.render }),
|
||||
source: {
|
||||
osmTags: {
|
||||
and: ["id~*"],
|
||||
},
|
||||
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=" + this._includeClosedNotesDays + "&bbox={x_min},{y_min},{x_max},{y_max}",
|
||||
"geoJsonZoomLevel": 10,
|
||||
"maxCacheAge": 0
|
||||
geoJson:
|
||||
"https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=" +
|
||||
this._includeClosedNotesDays +
|
||||
"&bbox={x_min},{y_min},{x_max},{y_max}",
|
||||
geoJsonZoomLevel: 10,
|
||||
maxCacheAge: 0,
|
||||
},
|
||||
"minzoom": Math.min(12, layerJson.minzoom - 2),
|
||||
"title": {
|
||||
"render": trs(t.popupTitle, {title})
|
||||
minzoom: Math.min(12, layerJson.minzoom - 2),
|
||||
title: {
|
||||
render: trs(t.popupTitle, { title }),
|
||||
},
|
||||
"calculatedTags": [
|
||||
calculatedTags: [
|
||||
"_first_comment=feat.get('comments')[0].text.toLowerCase()",
|
||||
"_trigger_index=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\)?.*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()",
|
||||
"_comments_count=feat.get('comments').length",
|
||||
"_intro=(() => {const lines = feat.get('comments')[0].text.split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.filter(l => l !== '').join('<br/>');})()",
|
||||
"_tags=(() => {let lines = feat.get('comments')[0].text.split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()"
|
||||
"_tags=(() => {let lines = feat.get('comments')[0].text.split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()",
|
||||
],
|
||||
"isShown": {
|
||||
and:
|
||||
["_trigger_index~*",
|
||||
{or: isShownIfAny}
|
||||
]
|
||||
isShown: {
|
||||
and: ["_trigger_index~*", { or: isShownIfAny }],
|
||||
},
|
||||
"titleIcons": [
|
||||
titleIcons: [
|
||||
{
|
||||
"render": "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>"
|
||||
}
|
||||
render: "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>",
|
||||
},
|
||||
],
|
||||
"tagRenderings": [
|
||||
tagRenderings: [
|
||||
{
|
||||
"id": "Intro",
|
||||
render: "{_intro}"
|
||||
id: "Intro",
|
||||
render: "{_intro}",
|
||||
},
|
||||
{
|
||||
"id": "conversation",
|
||||
"render": "{visualize_note_comments(comments,1)}",
|
||||
condition: "_comments_count>1"
|
||||
id: "conversation",
|
||||
render: "{visualize_note_comments(comments,1)}",
|
||||
condition: "_comments_count>1",
|
||||
},
|
||||
{
|
||||
"id": "import",
|
||||
"render": importButton,
|
||||
condition: "closed_at="
|
||||
id: "import",
|
||||
render: importButton,
|
||||
condition: "closed_at=",
|
||||
},
|
||||
{
|
||||
"id": "close_note_",
|
||||
"render": embed(
|
||||
"{close_note(", t.notFound.Subs({title}), ", ./assets/svg/close.svg, id, This feature does not exist, 18)}"),
|
||||
condition: "closed_at="
|
||||
id: "close_note_",
|
||||
render: embed(
|
||||
"{close_note(",
|
||||
t.notFound.Subs({ title }),
|
||||
", ./assets/svg/close.svg, id, This feature does not exist, 18)}"
|
||||
),
|
||||
condition: "closed_at=",
|
||||
},
|
||||
{
|
||||
"id": "close_note_mapped",
|
||||
"render": embed("{close_note(", t.alreadyMapped.Subs({title}), ", ./assets/svg/duplicate.svg, id, Already mapped, 18)}"),
|
||||
condition: "closed_at="
|
||||
id: "close_note_mapped",
|
||||
render: embed(
|
||||
"{close_note(",
|
||||
t.alreadyMapped.Subs({ title }),
|
||||
", ./assets/svg/duplicate.svg, id, Already mapped, 18)}"
|
||||
),
|
||||
condition: "closed_at=",
|
||||
},
|
||||
{
|
||||
"id": "handled",
|
||||
"render": tr(t.importHandled),
|
||||
condition: "closed_at~*"
|
||||
id: "handled",
|
||||
render: tr(t.importHandled),
|
||||
condition: "closed_at~*",
|
||||
},
|
||||
{
|
||||
"id": "comment",
|
||||
"render": "{add_note_comment()}"
|
||||
id: "comment",
|
||||
render: "{add_note_comment()}",
|
||||
},
|
||||
{
|
||||
"id": "add_image",
|
||||
"render": "{add_image_to_note()}"
|
||||
id: "add_image",
|
||||
render: "{add_image_to_note()}",
|
||||
},
|
||||
{
|
||||
"id": "nearby_images",
|
||||
render: tr(t.nearbyImagesIntro)
|
||||
|
||||
}
|
||||
id: "nearby_images",
|
||||
render: tr(t.nearbyImagesIntro),
|
||||
},
|
||||
],
|
||||
"mapRendering": [
|
||||
mapRendering: [
|
||||
{
|
||||
"location": [
|
||||
"point"
|
||||
],
|
||||
"icon": {
|
||||
"render": "circle:white;help:black",
|
||||
mappings: [{
|
||||
if: {or: ["closed_at~*", "_imported=yes"]},
|
||||
then: "circle:white;checkmark:black"
|
||||
}]
|
||||
location: ["point"],
|
||||
icon: {
|
||||
render: "circle:white;help:black",
|
||||
mappings: [
|
||||
{
|
||||
if: { or: ["closed_at~*", "_imported=yes"] },
|
||||
then: "circle:white;checkmark:black",
|
||||
},
|
||||
],
|
||||
},
|
||||
"iconSize": "40,40,center"
|
||||
}
|
||||
]
|
||||
iconSize: "40,40,center",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
result
|
||||
};
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,37 @@
|
|||
import {Conversion, DesugaringStep} from "./Conversion";
|
||||
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
|
||||
import {Utils} from "../../../Utils";
|
||||
import * as metapaths from "../../../assets/layoutconfigmeta.json";
|
||||
import * as tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json";
|
||||
import Translations from "../../../UI/i18n/Translations";
|
||||
import { Conversion, DesugaringStep } from "./Conversion"
|
||||
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import * as metapaths from "../../../assets/layoutconfigmeta.json"
|
||||
import * as tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
|
||||
export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
|
||||
private _isOfficial: boolean;
|
||||
private _sharedTagRenderings: Map<string, any>;
|
||||
|
||||
private static readonly layoutMetaPaths = (metapaths["default"] ?? metapaths)
|
||||
.filter(mp => (ExtractImages.mightBeTagRendering(mp)) || mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon"))
|
||||
private static readonly tagRenderingMetaPaths = (tagrenderingmetapaths["default"] ?? tagrenderingmetapaths)
|
||||
private _isOfficial: boolean
|
||||
private _sharedTagRenderings: Map<string, any>
|
||||
|
||||
private static readonly layoutMetaPaths = (metapaths["default"] ?? metapaths).filter(
|
||||
(mp) =>
|
||||
ExtractImages.mightBeTagRendering(mp) ||
|
||||
(mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon"))
|
||||
)
|
||||
private static readonly tagRenderingMetaPaths =
|
||||
tagrenderingmetapaths["default"] ?? tagrenderingmetapaths
|
||||
|
||||
constructor(isOfficial: boolean, sharedTagRenderings: Map<string, any>) {
|
||||
super("Extract all images from a layoutConfig using the meta paths.",[],"ExctractImages");
|
||||
this._isOfficial = isOfficial;
|
||||
this._sharedTagRenderings = sharedTagRenderings;
|
||||
super("Extract all images from a layoutConfig using the meta paths.", [], "ExctractImages")
|
||||
this._isOfficial = isOfficial
|
||||
this._sharedTagRenderings = sharedTagRenderings
|
||||
}
|
||||
|
||||
public static mightBeTagRendering(metapath: {type: string | string[]}) : boolean{
|
||||
if(!Array.isArray(metapath.type)){
|
||||
|
||||
public static mightBeTagRendering(metapath: { type: string | string[] }): boolean {
|
||||
if (!Array.isArray(metapath.type)) {
|
||||
return false
|
||||
}
|
||||
return metapath.type.some(t =>
|
||||
t["$ref"] == "#/definitions/TagRenderingConfigJson" || t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson")
|
||||
return metapath.type.some(
|
||||
(t) =>
|
||||
t["$ref"] == "#/definitions/TagRenderingConfigJson" ||
|
||||
t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,105 +67,131 @@ export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
|
|||
* images.length // => 2
|
||||
* images.findIndex(img => img == "./assets/layers/bike_parking/staple.svg") // => 0
|
||||
* images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") // => 1
|
||||
*
|
||||
*
|
||||
* // should not pickup rotation, should drop color
|
||||
* const images = new ExtractImages(true, new Map<string, any>()).convert(<any>{"layers": [{mapRendering: [{"location": ["point", "centroid"],"icon": "pin:black",rotation: 180,iconSize: "40,40,center"}]}]
|
||||
* }, "test").result
|
||||
* images.length // => 1
|
||||
* images[0] // => "pin"
|
||||
*
|
||||
*
|
||||
*/
|
||||
convert(json: LayoutConfigJson, context: string): { result: string[], errors: string[], warnings: string[] } {
|
||||
const allFoundImages : string[] = []
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): { result: string[]; errors: string[]; warnings: string[] } {
|
||||
const allFoundImages: string[] = []
|
||||
const errors = []
|
||||
const warnings = []
|
||||
for (const metapath of ExtractImages.layoutMetaPaths) {
|
||||
const mightBeTr = ExtractImages.mightBeTagRendering(metapath)
|
||||
const allRenderedValuesAreImages = metapath.typeHint === "icon" || metapath.typeHint === "image"
|
||||
const allRenderedValuesAreImages =
|
||||
metapath.typeHint === "icon" || metapath.typeHint === "image"
|
||||
const found = Utils.CollectPath(metapath.path, json)
|
||||
if (mightBeTr) {
|
||||
// We might have tagRenderingConfigs containing icons here
|
||||
for (const el of found) {
|
||||
const path = el.path
|
||||
const foundImage = el.leaf;
|
||||
if (typeof foundImage === "string") {
|
||||
|
||||
if(!allRenderedValuesAreImages){
|
||||
continue
|
||||
}
|
||||
|
||||
if(foundImage == ""){
|
||||
warnings.push(context+"."+path.join(".")+" Found an empty image")
|
||||
const foundImage = el.leaf
|
||||
if (typeof foundImage === "string") {
|
||||
if (!allRenderedValuesAreImages) {
|
||||
continue
|
||||
}
|
||||
|
||||
if(this._sharedTagRenderings?.has(foundImage)){
|
||||
|
||||
if (foundImage == "") {
|
||||
warnings.push(context + "." + path.join(".") + " Found an empty image")
|
||||
}
|
||||
|
||||
if (this._sharedTagRenderings?.has(foundImage)) {
|
||||
// This is not an image, but a shared tag rendering
|
||||
// At key positions for checking, they'll be expanded already, so we can safely ignore them here
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
allFoundImages.push(foundImage)
|
||||
} else{
|
||||
} else {
|
||||
// This is a tagRendering.
|
||||
// Either every rendered value might be an icon
|
||||
// Either every rendered value might be an icon
|
||||
// or -in the case of a normal tagrendering- only the 'icons' in the mappings have an icon (or exceptionally an '<img>' tag in the translation
|
||||
for (const trpath of ExtractImages.tagRenderingMetaPaths) {
|
||||
// Inspect all the rendered values
|
||||
const fromPath = Utils.CollectPath(trpath.path, foundImage)
|
||||
const isRendered = trpath.typeHint === "rendered"
|
||||
const isImage = trpath.typeHint === "icon" || trpath.typeHint === "image"
|
||||
const isImage =
|
||||
trpath.typeHint === "icon" || trpath.typeHint === "image"
|
||||
for (const img of fromPath) {
|
||||
if (allRenderedValuesAreImages && isRendered) {
|
||||
// What we found is an image
|
||||
if(img.leaf === "" || img.leaf["path"] == ""){
|
||||
warnings.push(context+[...path,...img.path].join(".")+": Found an empty image at ")
|
||||
}else if(typeof img.leaf !== "string"){
|
||||
(this._isOfficial ? errors: warnings).push(context+"."+img.path.join(".")+": found an image path that is not a string: " + JSON.stringify(img.leaf))
|
||||
}else{
|
||||
if (img.leaf === "" || img.leaf["path"] == "") {
|
||||
warnings.push(
|
||||
context +
|
||||
[...path, ...img.path].join(".") +
|
||||
": Found an empty image at "
|
||||
)
|
||||
} else if (typeof img.leaf !== "string") {
|
||||
;(this._isOfficial ? errors : warnings).push(
|
||||
context +
|
||||
"." +
|
||||
img.path.join(".") +
|
||||
": found an image path that is not a string: " +
|
||||
JSON.stringify(img.leaf)
|
||||
)
|
||||
} else {
|
||||
allFoundImages.push(img.leaf)
|
||||
}
|
||||
}
|
||||
if(!allRenderedValuesAreImages && isImage){
|
||||
}
|
||||
if (!allRenderedValuesAreImages && isImage) {
|
||||
// Extract images from the translations
|
||||
allFoundImages.push(...(Translations.T(img.leaf, "extract_images from "+img.path.join(".")).ExtractImages(false)))
|
||||
allFoundImages.push(
|
||||
...Translations.T(
|
||||
img.leaf,
|
||||
"extract_images from " + img.path.join(".")
|
||||
).ExtractImages(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const foundElement of found) {
|
||||
if(foundElement.leaf === ""){
|
||||
warnings.push(context+"."+foundElement.path.join(".")+" Found an empty image")
|
||||
if (foundElement.leaf === "") {
|
||||
warnings.push(
|
||||
context + "." + foundElement.path.join(".") + " Found an empty image"
|
||||
)
|
||||
continue
|
||||
}
|
||||
allFoundImages.push(foundElement.leaf)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const splitParts = [].concat(...Utils.NoNull(allFoundImages)
|
||||
.map(img => img["path"] ?? img)
|
||||
.map(img => img.split(";")))
|
||||
.map(img => img.split(":")[0])
|
||||
.filter(img => img !== "")
|
||||
return {result: Utils.Dedup(splitParts), errors, warnings};
|
||||
const splitParts = []
|
||||
.concat(
|
||||
...Utils.NoNull(allFoundImages)
|
||||
.map((img) => img["path"] ?? img)
|
||||
.map((img) => img.split(";"))
|
||||
)
|
||||
.map((img) => img.split(":")[0])
|
||||
.filter((img) => img !== "")
|
||||
return { result: Utils.Dedup(splitParts), errors, warnings }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class FixImages extends DesugaringStep<LayoutConfigJson> {
|
||||
private readonly _knownImages: Set<string>;
|
||||
private readonly _knownImages: Set<string>
|
||||
|
||||
constructor(knownImages: Set<string>) {
|
||||
super("Walks over the entire theme and replaces images to the relative URL. Only works if the ID of the theme is an URL",[],"fixImages");
|
||||
this._knownImages = knownImages;
|
||||
super(
|
||||
"Walks over the entire theme and replaces images to the relative URL. Only works if the ID of the theme is an URL",
|
||||
[],
|
||||
"fixImages"
|
||||
)
|
||||
this._knownImages = knownImages
|
||||
}
|
||||
|
||||
/**
|
||||
* If the id is an URL to a json file, replaces "./" in images with the path to the json file
|
||||
*
|
||||
*
|
||||
* const theme = {
|
||||
* "id": "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/verkeerdeborden.json"
|
||||
* "layers": [
|
||||
|
@ -191,43 +223,50 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
|
|||
* fixed.layers[0]["mapRendering"][0].icon // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/TS_bolt.svg"
|
||||
* fixed.layers[0]["mapRendering"][0].iconBadges[0].then.mappings[0].then // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg"
|
||||
*/
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson, warnings?: string[] } {
|
||||
let url: URL;
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): { result: LayoutConfigJson; warnings?: string[] } {
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(json.id)
|
||||
} catch (e) {
|
||||
// Not a URL, we don't rewrite
|
||||
return {result: json}
|
||||
return { result: json }
|
||||
}
|
||||
|
||||
const warnings: string[] = []
|
||||
const absolute = url.protocol + "//" + url.host
|
||||
let relative = url.protocol + "//" + url.host + url.pathname
|
||||
relative = relative.substring(0, relative.lastIndexOf("/"))
|
||||
const self = this;
|
||||
|
||||
if(relative.endsWith("assets/generated/themes")){
|
||||
warnings.push("Detected 'assets/generated/themes' as relative URL. I'm assuming that you are loading your file for the MC-repository, so I'm rewriting all image links as if they were absolute instead of relative")
|
||||
const self = this
|
||||
|
||||
if (relative.endsWith("assets/generated/themes")) {
|
||||
warnings.push(
|
||||
"Detected 'assets/generated/themes' as relative URL. I'm assuming that you are loading your file for the MC-repository, so I'm rewriting all image links as if they were absolute instead of relative"
|
||||
)
|
||||
relative = absolute
|
||||
}
|
||||
|
||||
function replaceString(leaf: string) {
|
||||
if (self._knownImages.has(leaf)) {
|
||||
return leaf;
|
||||
return leaf
|
||||
}
|
||||
|
||||
if(typeof leaf !== "string"){
|
||||
warnings.push("Found a non-string object while replacing images: "+JSON.stringify(leaf))
|
||||
return leaf;
|
||||
|
||||
if (typeof leaf !== "string") {
|
||||
warnings.push(
|
||||
"Found a non-string object while replacing images: " + JSON.stringify(leaf)
|
||||
)
|
||||
return leaf
|
||||
}
|
||||
|
||||
|
||||
if (leaf.startsWith("./")) {
|
||||
return relative + leaf.substring(1)
|
||||
}
|
||||
if (leaf.startsWith("/")) {
|
||||
return absolute + leaf
|
||||
}
|
||||
return leaf;
|
||||
return leaf
|
||||
}
|
||||
|
||||
json = Utils.Clone(json)
|
||||
|
@ -252,21 +291,19 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
|
|||
if (trpath.typeHint !== "rendered") {
|
||||
continue
|
||||
}
|
||||
Utils.WalkPath(trpath.path, leaf, (rendered => {
|
||||
Utils.WalkPath(trpath.path, leaf, (rendered) => {
|
||||
return replaceString(rendered)
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return leaf;
|
||||
return leaf
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
warnings,
|
||||
result: json
|
||||
};
|
||||
result: json,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +1,51 @@
|
|||
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
|
||||
import {Utils} from "../../../Utils";
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import {DesugaringStep, Each, Fuse, On} from "./Conversion";
|
||||
|
||||
export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string | { builtin, override }> {
|
||||
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { DesugaringStep, Each, Fuse, On } from "./Conversion"
|
||||
|
||||
export class UpdateLegacyLayer extends DesugaringStep<
|
||||
LayerConfigJson | string | { builtin; override }
|
||||
> {
|
||||
constructor() {
|
||||
super("Updates various attributes from the old data format to the new to provide backwards compatibility with the formats",
|
||||
super(
|
||||
"Updates various attributes from the old data format to the new to provide backwards compatibility with the formats",
|
||||
["overpassTags", "source.osmtags", "tagRenderings[*].id", "mapRendering"],
|
||||
"UpdateLegacyLayer");
|
||||
"UpdateLegacyLayer"
|
||||
)
|
||||
}
|
||||
|
||||
convert(json: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
|
||||
convert(
|
||||
json: LayerConfigJson,
|
||||
context: string
|
||||
): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
|
||||
const warnings = []
|
||||
if (typeof json === "string" || json["builtin"] !== undefined) {
|
||||
// Reuse of an already existing layer; return as-is
|
||||
return {result: json, errors: [], warnings: []}
|
||||
return { result: json, errors: [], warnings: [] }
|
||||
}
|
||||
let config = {...json};
|
||||
let config = { ...json }
|
||||
|
||||
if (config["overpassTags"]) {
|
||||
config.source = config.source ?? {
|
||||
osmTags: config["overpassTags"]
|
||||
osmTags: config["overpassTags"],
|
||||
}
|
||||
config.source.osmTags = config["overpassTags"]
|
||||
delete config["overpassTags"]
|
||||
}
|
||||
|
||||
if (config.tagRenderings !== undefined) {
|
||||
let i = 0;
|
||||
let i = 0
|
||||
for (const tagRendering of config.tagRenderings) {
|
||||
i++;
|
||||
if (typeof tagRendering === "string" || tagRendering["builtin"] !== undefined || tagRendering["rewrite"] !== undefined) {
|
||||
i++
|
||||
if (
|
||||
typeof tagRendering === "string" ||
|
||||
tagRendering["builtin"] !== undefined ||
|
||||
tagRendering["rewrite"] !== undefined
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (tagRendering["id"] === undefined) {
|
||||
|
||||
if (tagRendering["#"] !== undefined) {
|
||||
tagRendering["id"] = tagRendering["#"]
|
||||
delete tagRendering["#"]
|
||||
|
@ -49,7 +58,6 @@ export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string |
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
if (config.mapRendering === undefined) {
|
||||
config.mapRendering = []
|
||||
// This is a legacy format, lets create a pointRendering
|
||||
|
@ -59,14 +67,13 @@ export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string |
|
|||
location = ["point", "centroid"]
|
||||
}
|
||||
if (config["icon"] ?? config["label"] !== undefined) {
|
||||
|
||||
const pointConfig = {
|
||||
icon: config["icon"],
|
||||
iconBadges: config["iconOverlays"],
|
||||
label: config["label"],
|
||||
iconSize: config["iconSize"],
|
||||
location,
|
||||
rotation: config["rotation"]
|
||||
rotation: config["rotation"],
|
||||
}
|
||||
config.mapRendering.push(pointConfig)
|
||||
}
|
||||
|
@ -75,19 +82,20 @@ export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string |
|
|||
const lineRenderConfig = <LineRenderingConfigJson>{
|
||||
color: config["color"],
|
||||
width: config["width"],
|
||||
dashArray: config["dashArray"]
|
||||
dashArray: config["dashArray"],
|
||||
}
|
||||
if (Object.keys(lineRenderConfig).length > 0) {
|
||||
config.mapRendering.push(lineRenderConfig)
|
||||
}
|
||||
}
|
||||
if (config.mapRendering.length === 0) {
|
||||
throw "Could not convert the legacy theme into a new theme: no renderings defined for layer " + config.id
|
||||
throw (
|
||||
"Could not convert the legacy theme into a new theme: no renderings defined for layer " +
|
||||
config.id
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
delete config["color"]
|
||||
delete config["width"]
|
||||
delete config["dashArray"]
|
||||
|
@ -100,7 +108,7 @@ export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string |
|
|||
delete config["wayHandling"]
|
||||
delete config["hideUnderlayingFeaturesMinPercentage"]
|
||||
|
||||
for (const mapRenderingElement of (config.mapRendering ?? [])) {
|
||||
for (const mapRenderingElement of config.mapRendering ?? []) {
|
||||
if (mapRenderingElement["iconOverlays"] !== undefined) {
|
||||
mapRenderingElement["iconBadges"] = mapRenderingElement["iconOverlays"]
|
||||
}
|
||||
|
@ -115,34 +123,37 @@ export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string |
|
|||
return {
|
||||
result: config,
|
||||
errors: [],
|
||||
warnings
|
||||
};
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
|
||||
constructor() {
|
||||
super("Small fixes in the theme config", ["roamingRenderings"], "UpdateLegacyTheme");
|
||||
super("Small fixes in the theme config", ["roamingRenderings"], "UpdateLegacyTheme")
|
||||
}
|
||||
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
const oldThemeConfig = {...json}
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
const oldThemeConfig = { ...json }
|
||||
|
||||
if (oldThemeConfig.socialImage === "") {
|
||||
delete oldThemeConfig.socialImage
|
||||
}
|
||||
|
||||
|
||||
if (oldThemeConfig["roamingRenderings"] !== undefined) {
|
||||
|
||||
if (oldThemeConfig["roamingRenderings"].length == 0) {
|
||||
delete oldThemeConfig["roamingRenderings"]
|
||||
} else {
|
||||
return {
|
||||
result: null,
|
||||
errors: [context + ": The theme contains roamingRenderings. These are not supported anymore"],
|
||||
warnings: []
|
||||
errors: [
|
||||
context +
|
||||
": The theme contains roamingRenderings. These are not supported anymore",
|
||||
],
|
||||
warnings: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,8 +163,12 @@ class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
delete oldThemeConfig["version"]
|
||||
|
||||
if (oldThemeConfig["maintainer"] !== undefined) {
|
||||
|
||||
console.log("Maintainer: ", oldThemeConfig["maintainer"], "credits: ", oldThemeConfig["credits"])
|
||||
console.log(
|
||||
"Maintainer: ",
|
||||
oldThemeConfig["maintainer"],
|
||||
"credits: ",
|
||||
oldThemeConfig["credits"]
|
||||
)
|
||||
if (oldThemeConfig.credits === undefined) {
|
||||
oldThemeConfig["credits"] = oldThemeConfig["maintainer"]
|
||||
delete oldThemeConfig["maintainer"]
|
||||
|
@ -167,7 +182,7 @@ class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
return {
|
||||
errors: [],
|
||||
warnings: [],
|
||||
result: oldThemeConfig
|
||||
result: oldThemeConfig,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -178,8 +193,6 @@ export class FixLegacyTheme extends Fuse<LayoutConfigJson> {
|
|||
"Fixes a legacy theme to the modern JSON format geared to humans. Syntactic sugars are kept (i.e. no tagRenderings are expandend, no dependencies are automatically gathered)",
|
||||
new UpdateLegacyTheme(),
|
||||
new On("layers", new Each(new UpdateLegacyLayer()))
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,50 +1,74 @@
|
|||
import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault} from "./Conversion";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
||||
import {Utils} from "../../../Utils";
|
||||
import RewritableConfigJson from "../Json/RewritableConfigJson";
|
||||
import SpecialVisualizations from "../../../UI/SpecialVisualizations";
|
||||
import Translations from "../../../UI/i18n/Translations";
|
||||
import {Translation} from "../../../UI/i18n/Translation";
|
||||
import {
|
||||
Concat,
|
||||
Conversion,
|
||||
DesugaringContext,
|
||||
DesugaringStep,
|
||||
Each,
|
||||
FirstOf,
|
||||
Fuse,
|
||||
On,
|
||||
SetDefault,
|
||||
} from "./Conversion"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import RewritableConfigJson from "../Json/RewritableConfigJson"
|
||||
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
import { Translation } from "../../../UI/i18n/Translation"
|
||||
import * as tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json"
|
||||
import {AddContextToTranslations} from "./AddContextToTranslations";
|
||||
import { AddContextToTranslations } from "./AddContextToTranslations"
|
||||
|
||||
|
||||
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
|
||||
private readonly _state: DesugaringContext;
|
||||
private readonly _self: LayerConfigJson;
|
||||
class ExpandTagRendering extends Conversion<
|
||||
string | TagRenderingConfigJson | { builtin: string | string[]; override: any },
|
||||
TagRenderingConfigJson[]
|
||||
> {
|
||||
private readonly _state: DesugaringContext
|
||||
private readonly _self: LayerConfigJson
|
||||
private readonly _options: {
|
||||
/* If true, will copy the 'osmSource'-tags into the condition */
|
||||
applyCondition?: true | boolean;
|
||||
};
|
||||
|
||||
constructor(state: DesugaringContext, self: LayerConfigJson, options?: { applyCondition?: true | boolean;}) {
|
||||
super("Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question", [], "ExpandTagRendering");
|
||||
this._state = state;
|
||||
this._self = self;
|
||||
this._options = options;
|
||||
applyCondition?: true | boolean
|
||||
}
|
||||
|
||||
convert(json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
|
||||
constructor(
|
||||
state: DesugaringContext,
|
||||
self: LayerConfigJson,
|
||||
options?: { applyCondition?: true | boolean }
|
||||
) {
|
||||
super(
|
||||
"Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question",
|
||||
[],
|
||||
"ExpandTagRendering"
|
||||
)
|
||||
this._state = state
|
||||
this._self = self
|
||||
this._options = options
|
||||
}
|
||||
|
||||
convert(
|
||||
json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any },
|
||||
context: string
|
||||
): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
|
||||
return {
|
||||
result: this.convertUntilStable(json, warnings, errors, context),
|
||||
errors, warnings
|
||||
};
|
||||
errors,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
private lookup(name: string): TagRenderingConfigJson[] {
|
||||
const state = this._state;
|
||||
const state = this._state
|
||||
if (state.tagRenderings.has(name)) {
|
||||
return [state.tagRenderings.get(name)]
|
||||
}
|
||||
if (name.indexOf(".") < 0) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const spl = name.split(".");
|
||||
const spl = name.split(".")
|
||||
let layer = state.sharedLayers.get(spl[0])
|
||||
if (spl[0] === this._self.id) {
|
||||
layer = this._self
|
||||
|
@ -54,29 +78,30 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
|
|||
return undefined
|
||||
}
|
||||
|
||||
const id = spl[1];
|
||||
const id = spl[1]
|
||||
|
||||
const layerTrs = <TagRenderingConfigJson[]>layer.tagRenderings.filter(tr => tr["id"] !== undefined)
|
||||
const layerTrs = <TagRenderingConfigJson[]>(
|
||||
layer.tagRenderings.filter((tr) => tr["id"] !== undefined)
|
||||
)
|
||||
let matchingTrs: TagRenderingConfigJson[]
|
||||
if (id === "*") {
|
||||
matchingTrs = layerTrs
|
||||
} else if (id.startsWith("*")) {
|
||||
const id_ = id.substring(1)
|
||||
matchingTrs = layerTrs.filter(tr => tr.group === id_ || tr.labels?.indexOf(id_) >= 0)
|
||||
matchingTrs = layerTrs.filter((tr) => tr.group === id_ || tr.labels?.indexOf(id_) >= 0)
|
||||
} else {
|
||||
matchingTrs = layerTrs.filter(tr => tr.id === id)
|
||||
matchingTrs = layerTrs.filter((tr) => tr.id === id)
|
||||
}
|
||||
|
||||
|
||||
const contextWriter = new AddContextToTranslations<TagRenderingConfigJson>("layers:")
|
||||
for (let i = 0; i < matchingTrs.length; i++) {
|
||||
let found: TagRenderingConfigJson = Utils.Clone(matchingTrs[i]);
|
||||
if(this._options?.applyCondition){
|
||||
let found: TagRenderingConfigJson = Utils.Clone(matchingTrs[i])
|
||||
if (this._options?.applyCondition) {
|
||||
// The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
|
||||
if (found.condition === undefined) {
|
||||
found.condition = layer.source.osmTags
|
||||
} else {
|
||||
found.condition = {and: [found.condition, layer.source.osmTags]}
|
||||
found.condition = { and: [found.condition, layer.source.osmTags] }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,29 +112,37 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
|
|||
if (matchingTrs.length !== 0) {
|
||||
return matchingTrs
|
||||
}
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
private convertOnce(tr: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
|
||||
private convertOnce(
|
||||
tr: string | any,
|
||||
warnings: string[],
|
||||
errors: string[],
|
||||
ctx: string
|
||||
): TagRenderingConfigJson[] {
|
||||
const state = this._state
|
||||
if (tr === "questions") {
|
||||
return [{
|
||||
id: "questions"
|
||||
}]
|
||||
return [
|
||||
{
|
||||
id: "questions",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
if (typeof tr === "string") {
|
||||
const lookup = this.lookup(tr);
|
||||
const lookup = this.lookup(tr)
|
||||
if (lookup === undefined) {
|
||||
const isTagRendering = ctx.indexOf("On(mapRendering") < 0
|
||||
if (isTagRendering) {
|
||||
warnings.push(ctx + "A literal rendering was detected: " + tr)
|
||||
}
|
||||
return [{
|
||||
render: tr,
|
||||
id: tr.replace(/[^a-zA-Z0-9]/g, "")
|
||||
}]
|
||||
return [
|
||||
{
|
||||
render: tr,
|
||||
id: tr.replace(/[^a-zA-Z0-9]/g, ""),
|
||||
},
|
||||
]
|
||||
}
|
||||
return lookup
|
||||
}
|
||||
|
@ -121,10 +154,22 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
|
|||
}
|
||||
|
||||
for (const key of Object.keys(tr)) {
|
||||
if (key === "builtin" || key === "override" || key === "id" || key.startsWith("#")) {
|
||||
if (
|
||||
key === "builtin" ||
|
||||
key === "override" ||
|
||||
key === "id" ||
|
||||
key.startsWith("#")
|
||||
) {
|
||||
continue
|
||||
}
|
||||
errors.push("At " + ctx + ": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" + key + "` was found. This won't be picked up! The full object is: " + JSON.stringify(tr))
|
||||
errors.push(
|
||||
"At " +
|
||||
ctx +
|
||||
": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" +
|
||||
key +
|
||||
"` was found. This won't be picked up! The full object is: " +
|
||||
JSON.stringify(tr)
|
||||
)
|
||||
}
|
||||
|
||||
const trs: TagRenderingConfigJson[] = []
|
||||
|
@ -136,21 +181,50 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
|
|||
const [layerName, search] = name.split(".")
|
||||
let layer = state.sharedLayers.get(layerName)
|
||||
if (layerName === this._self.id) {
|
||||
layer = this._self;
|
||||
layer = this._self
|
||||
}
|
||||
if (layer === undefined) {
|
||||
const candidates = Utils.sortedByLevenshteinDistance(layerName, Array.from(state.sharedLayers.keys()), s => s)
|
||||
const candidates = Utils.sortedByLevenshteinDistance(
|
||||
layerName,
|
||||
Array.from(state.sharedLayers.keys()),
|
||||
(s) => s
|
||||
)
|
||||
if (state.sharedLayers.size === 0) {
|
||||
warnings.push(ctx + ": BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " + name + ": layer " + layerName + " not found. Maybe you meant on of " + candidates.slice(0, 3).join(", "))
|
||||
warnings.push(
|
||||
ctx +
|
||||
": BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " +
|
||||
name +
|
||||
": layer " +
|
||||
layerName +
|
||||
" not found. Maybe you meant on of " +
|
||||
candidates.slice(0, 3).join(", ")
|
||||
)
|
||||
} else {
|
||||
errors.push(ctx + ": While reusing tagrendering: " + name + ": layer " + layerName + " not found. Maybe you meant on of " + candidates.slice(0, 3).join(", "))
|
||||
errors.push(
|
||||
ctx +
|
||||
": While reusing tagrendering: " +
|
||||
name +
|
||||
": layer " +
|
||||
layerName +
|
||||
" not found. Maybe you meant on of " +
|
||||
candidates.slice(0, 3).join(", ")
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
candidates = Utils.NoNull(layer.tagRenderings.map(tr => tr["id"])).map(id => layerName + "." + id)
|
||||
candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map(
|
||||
(id) => layerName + "." + id
|
||||
)
|
||||
}
|
||||
candidates = Utils.sortedByLevenshteinDistance(name, candidates, i => i);
|
||||
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + candidates.join(", ") + "?")
|
||||
candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i)
|
||||
errors.push(
|
||||
ctx +
|
||||
": The tagRendering with identifier " +
|
||||
name +
|
||||
" was not found.\n\tDid you mean one of " +
|
||||
candidates.join(", ") +
|
||||
"?"
|
||||
)
|
||||
continue
|
||||
}
|
||||
for (let foundTr of lookup) {
|
||||
|
@ -159,36 +233,44 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
|
|||
trs.push(foundTr)
|
||||
}
|
||||
}
|
||||
return trs;
|
||||
return trs
|
||||
}
|
||||
|
||||
return [tr]
|
||||
}
|
||||
|
||||
private convertUntilStable(spec: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
|
||||
const trs = this.convertOnce(spec, warnings, errors, ctx);
|
||||
private convertUntilStable(
|
||||
spec: string | any,
|
||||
warnings: string[],
|
||||
errors: string[],
|
||||
ctx: string
|
||||
): TagRenderingConfigJson[] {
|
||||
const trs = this.convertOnce(spec, warnings, errors, ctx)
|
||||
|
||||
const result = []
|
||||
for (const tr of trs) {
|
||||
if (typeof tr === "string" || tr["builtin"] !== undefined) {
|
||||
const stable = this.convertUntilStable(tr, warnings, errors, ctx + "(RECURSIVE RESOLVE)")
|
||||
const stable = this.convertUntilStable(
|
||||
tr,
|
||||
warnings,
|
||||
errors,
|
||||
ctx + "(RECURSIVE RESOLVE)"
|
||||
)
|
||||
result.push(...stable)
|
||||
} else {
|
||||
result.push(tr)
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[]> {
|
||||
|
||||
constructor() {
|
||||
super("Applies a rewrite", [], "ExpandRewrite");
|
||||
super("Applies a rewrite", [], "ExpandRewrite")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used for left|right group creation and replacement.
|
||||
* Every 'keyToRewrite' will be replaced with 'target' recursively. This substitution will happen in place in the object 'tr'
|
||||
|
@ -210,7 +292,6 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
|
|||
const targetIsTranslation = Translations.isProbablyATranslation(target)
|
||||
|
||||
function replaceRecursive(obj: string | any, target) {
|
||||
|
||||
if (obj === keyToRewrite) {
|
||||
return target
|
||||
}
|
||||
|
@ -224,11 +305,11 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
|
|||
}
|
||||
if (Array.isArray(obj)) {
|
||||
// This is a list of items
|
||||
return obj.map(o => replaceRecursive(o, target))
|
||||
return obj.map((o) => replaceRecursive(o, target))
|
||||
}
|
||||
|
||||
if (typeof obj === "object") {
|
||||
obj = {...obj}
|
||||
obj = { ...obj }
|
||||
|
||||
const isTr = targetIsTranslation && Translations.isProbablyATranslation(obj)
|
||||
|
||||
|
@ -257,7 +338,7 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
|
|||
* sourceString: ["xyz","abc"],
|
||||
* into: [
|
||||
* ["X", "A"],
|
||||
* ["Y", "B"],
|
||||
* ["Y", "B"],
|
||||
* ["Z", "C"]],
|
||||
* },
|
||||
* renderings: "The value of xyz is abc"
|
||||
|
@ -286,25 +367,27 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
|
|||
* ]
|
||||
* new ExpandRewrite().convertStrict(spec, "test") // => expected
|
||||
*/
|
||||
convert(json: T | RewritableConfigJson<T>, context: string): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
|
||||
convert(
|
||||
json: T | RewritableConfigJson<T>,
|
||||
context: string
|
||||
): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
if (json === null || json === undefined) {
|
||||
return {result: []}
|
||||
return { result: [] }
|
||||
}
|
||||
|
||||
if (json["rewrite"] === undefined) {
|
||||
|
||||
// not a rewrite
|
||||
return {result: [(<T>json)]}
|
||||
return { result: [<T>json] }
|
||||
}
|
||||
|
||||
const rewrite = <RewritableConfigJson<T>>json;
|
||||
const rewrite = <RewritableConfigJson<T>>json
|
||||
const keysToRewrite = rewrite.rewrite
|
||||
const ts: T[] = []
|
||||
|
||||
{// sanity check: rewrite: ["xyz", "longer_xyz"] is not allowed as "longer_xyz" will never be triggered
|
||||
{
|
||||
// sanity check: rewrite: ["xyz", "longer_xyz"] is not allowed as "longer_xyz" will never be triggered
|
||||
for (let i = 0; i < keysToRewrite.sourceString.length; i++) {
|
||||
const guard = keysToRewrite.sourceString[i];
|
||||
const guard = keysToRewrite.sourceString[i]
|
||||
for (let j = i + 1; j < keysToRewrite.sourceString.length; j++) {
|
||||
const toRewrite = keysToRewrite.sourceString[j]
|
||||
if (toRewrite.indexOf(guard) >= 0) {
|
||||
|
@ -314,12 +397,12 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
|
|||
}
|
||||
}
|
||||
|
||||
{// sanity check: {rewrite: ["a", "b"] should have the right amount of 'intos' in every case
|
||||
{
|
||||
// sanity check: {rewrite: ["a", "b"] should have the right amount of 'intos' in every case
|
||||
for (let i = 0; i < rewrite.rewrite.into.length; i++) {
|
||||
const into = keysToRewrite.into[i]
|
||||
if (into.length !== rewrite.rewrite.sourceString.length) {
|
||||
throw `${context}.into.${i} Error in rewrite: there are ${rewrite.rewrite.sourceString.length} keys to rewrite, but entry ${i} has only ${into.length} values`
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -327,17 +410,15 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
|
|||
for (let i = 0; i < keysToRewrite.into.length; i++) {
|
||||
let t = Utils.Clone(rewrite.renderings)
|
||||
for (let j = 0; j < keysToRewrite.sourceString.length; j++) {
|
||||
const key = keysToRewrite.sourceString[j];
|
||||
const key = keysToRewrite.sourceString[j]
|
||||
const target = keysToRewrite.into[i][j]
|
||||
t = ExpandRewrite.RewriteParts(key, target, t)
|
||||
}
|
||||
ts.push(t)
|
||||
}
|
||||
|
||||
|
||||
return {result: ts};
|
||||
return { result: ts }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -345,7 +426,11 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
|
|||
*/
|
||||
export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
||||
constructor() {
|
||||
super("Converts a 'special' translation into a regular translation which uses parameters", ["special"], "RewriteSpecial");
|
||||
super(
|
||||
"Converts a 'special' translation into a regular translation which uses parameters",
|
||||
["special"],
|
||||
"RewriteSpecial"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -406,7 +491,11 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
* RewriteSpecial.convertIfNeeded(special, errors, "test") // => {"en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:{multi(_entrance_properties_with_width,An <a href='#&LBRACEid&RBRACE'>entrance</a> of &LBRACEcanonical&LPARENSwidth&RPARENS&RBRACE)}{_entrances_count_without_width_count} entrances don't have width information yet"}
|
||||
* errors // => []
|
||||
*/
|
||||
private static convertIfNeeded(input: (object & { special: { type: string } }) | any, errors: string[], context: string): any {
|
||||
private static convertIfNeeded(
|
||||
input: (object & { special: { type: string } }) | any,
|
||||
errors: string[],
|
||||
context: string
|
||||
): any {
|
||||
const special = input["special"]
|
||||
if (special === undefined) {
|
||||
return input
|
||||
|
@ -414,37 +503,55 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
|
||||
const type = special["type"]
|
||||
if (type === undefined) {
|
||||
errors.push("A 'special'-block should define 'type' to indicate which visualisation should be used")
|
||||
errors.push(
|
||||
"A 'special'-block should define 'type' to indicate which visualisation should be used"
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const vis = SpecialVisualizations.specialVisualizations.find(sp => sp.funcName === type)
|
||||
const vis = SpecialVisualizations.specialVisualizations.find((sp) => sp.funcName === type)
|
||||
if (vis === undefined) {
|
||||
const options = Utils.sortedByLevenshteinDistance(type, SpecialVisualizations.specialVisualizations, sp => sp.funcName)
|
||||
errors.push(`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`)
|
||||
const options = Utils.sortedByLevenshteinDistance(
|
||||
type,
|
||||
SpecialVisualizations.specialVisualizations,
|
||||
(sp) => sp.funcName
|
||||
)
|
||||
errors.push(
|
||||
`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
errors.push(...
|
||||
Array.from(Object.keys(input)).filter(k => k !== "special" && k !== "before" && k !== "after")
|
||||
.map(k => {
|
||||
return `The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`;
|
||||
}))
|
||||
errors.push(
|
||||
...Array.from(Object.keys(input))
|
||||
.filter((k) => k !== "special" && k !== "before" && k !== "after")
|
||||
.map((k) => {
|
||||
return `The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`
|
||||
})
|
||||
)
|
||||
|
||||
const argNamesList = vis.args.map(a => a.name)
|
||||
const argNamesList = vis.args.map((a) => a.name)
|
||||
const argNames = new Set<string>(argNamesList)
|
||||
// Check for obsolete and misspelled arguments
|
||||
errors.push(...Object.keys(special)
|
||||
.filter(k => !argNames.has(k))
|
||||
.filter(k => k !== "type" && k !== "before" && k !== "after")
|
||||
.map(wrongArg => {
|
||||
const byDistance = Utils.sortedByLevenshteinDistance(wrongArg, argNamesList, x => x)
|
||||
return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${byDistance[0]}?\n\tAll known arguments are ${argNamesList.join(", ")}`;
|
||||
}))
|
||||
errors.push(
|
||||
...Object.keys(special)
|
||||
.filter((k) => !argNames.has(k))
|
||||
.filter((k) => k !== "type" && k !== "before" && k !== "after")
|
||||
.map((wrongArg) => {
|
||||
const byDistance = Utils.sortedByLevenshteinDistance(
|
||||
wrongArg,
|
||||
argNamesList,
|
||||
(x) => x
|
||||
)
|
||||
return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
|
||||
byDistance[0]
|
||||
}?\n\tAll known arguments are ${argNamesList.join(", ")}`
|
||||
})
|
||||
)
|
||||
|
||||
// Check that all obligated arguments are present. They are obligated if they don't have a preset value
|
||||
for (const arg of vis.args) {
|
||||
if (arg.required !== true) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const param = special[arg.name]
|
||||
if (param === undefined) {
|
||||
|
@ -453,9 +560,10 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
}
|
||||
|
||||
const foundLanguages = new Set<string>()
|
||||
const translatedArgs = argNamesList.map(nm => special[nm])
|
||||
.filter(v => v !== undefined)
|
||||
.filter(v => Translations.isProbablyATranslation(v))
|
||||
const translatedArgs = argNamesList
|
||||
.map((nm) => special[nm])
|
||||
.filter((v) => v !== undefined)
|
||||
.filter((v) => Translations.isProbablyATranslation(v))
|
||||
for (const translatedArg of translatedArgs) {
|
||||
for (const ln of Object.keys(translatedArg)) {
|
||||
foundLanguages.add(ln)
|
||||
|
@ -473,9 +581,9 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
}
|
||||
|
||||
if (foundLanguages.size === 0) {
|
||||
const args = argNamesList.map(nm => special[nm] ?? "").join(",")
|
||||
const args = argNamesList.map((nm) => special[nm] ?? "").join(",")
|
||||
return {
|
||||
'*': `{${type}(${args})}`
|
||||
"*": `{${type}(${args})}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -487,16 +595,16 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
for (const argName of argNamesList) {
|
||||
let v = special[argName] ?? ""
|
||||
if (Translations.isProbablyATranslation(v)) {
|
||||
v = new Translation(v).textFor(ln)
|
||||
|
||||
}
|
||||
|
||||
v = new Translation(v).textFor(ln)
|
||||
}
|
||||
|
||||
if (typeof v === "string") {
|
||||
const txt = v.replace(/,/g, "&COMMA")
|
||||
const txt = v
|
||||
.replace(/,/g, "&COMMA")
|
||||
.replace(/\{/g, "&LBRACE")
|
||||
.replace(/}/g, "&RBRACE")
|
||||
.replace(/\(/g, "&LPARENS")
|
||||
.replace(/\)/g, '&RPARENS')
|
||||
.replace(/\)/g, "&RPARENS")
|
||||
args.push(txt)
|
||||
} else if (typeof v === "object") {
|
||||
args.push(JSON.stringify(v))
|
||||
|
@ -506,7 +614,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
}
|
||||
const beforeText = before?.textFor(ln) ?? ""
|
||||
const afterText = after?.textFor(ln) ?? ""
|
||||
result[ln] = `${beforeText}{${type}(${args.map(a => a).join(",")})}${afterText}`
|
||||
result[ln] = `${beforeText}{${type}(${args.map((a) => a).join(",")})}${afterText}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -541,23 +649,33 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
* const expected = {render: {'en': "{image_carousel(image)}Some footer"}}
|
||||
* result // => expected
|
||||
*/
|
||||
convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
convert(
|
||||
json: TagRenderingConfigJson,
|
||||
context: string
|
||||
): {
|
||||
result: TagRenderingConfigJson
|
||||
errors?: string[]
|
||||
warnings?: string[]
|
||||
information?: string[]
|
||||
} {
|
||||
const errors = []
|
||||
json = Utils.Clone(json)
|
||||
const paths: { path: string[], type?: any, typeHint?: string }[] = tagrenderingconfigmeta["default"] ?? tagrenderingconfigmeta
|
||||
const paths: { path: string[]; type?: any; typeHint?: string }[] =
|
||||
tagrenderingconfigmeta["default"] ?? tagrenderingconfigmeta
|
||||
for (const path of paths) {
|
||||
if (path.typeHint !== "rendered") {
|
||||
continue
|
||||
}
|
||||
Utils.WalkPath(path.path, json, ((leaf, travelled) => RewriteSpecial.convertIfNeeded(leaf, errors, travelled.join("."))))
|
||||
Utils.WalkPath(path.path, json, (leaf, travelled) =>
|
||||
RewriteSpecial.convertIfNeeded(leaf, errors, travelled.join("."))
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
result: json,
|
||||
errors
|
||||
};
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PrepareLayer extends Fuse<LayerConfigJson> {
|
||||
|
@ -566,11 +684,22 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
"Fully prepares and expands a layer for the LayerConfig.",
|
||||
new On("tagRenderings", new Each(new RewriteSpecial())),
|
||||
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
|
||||
new On("tagRenderings", layer => new Concat(new ExpandTagRendering(state, layer))),
|
||||
new On("tagRenderings", (layer) => new Concat(new ExpandTagRendering(state, layer))),
|
||||
new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
|
||||
new On("mapRendering", layer => new Each(new On("icon", new FirstOf(new ExpandTagRendering(state, layer, {applyCondition: false}))))),
|
||||
new On(
|
||||
"mapRendering",
|
||||
(layer) =>
|
||||
new Each(
|
||||
new On(
|
||||
"icon",
|
||||
new FirstOf(
|
||||
new ExpandTagRendering(state, layer, { applyCondition: false })
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
new SetDefault("titleIcons", ["defaults"]),
|
||||
new On("titleIcons", layer => new Concat(new ExpandTagRendering(state, layer)))
|
||||
);
|
||||
new On("titleIcons", (layer) => new Concat(new ExpandTagRendering(state, layer)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +1,59 @@
|
|||
import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault} from "./Conversion";
|
||||
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
|
||||
import {PrepareLayer} from "./PrepareLayer";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import {Utils} from "../../../Utils";
|
||||
import Constants from "../../Constants";
|
||||
import CreateNoteImportLayer from "./CreateNoteImportLayer";
|
||||
import LayerConfig from "../LayerConfig";
|
||||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
||||
import {SubstitutedTranslation} from "../../../UI/SubstitutedTranslation";
|
||||
import DependencyCalculator from "../DependencyCalculator";
|
||||
import {AddContextToTranslations} from "./AddContextToTranslations";
|
||||
import {
|
||||
Concat,
|
||||
Conversion,
|
||||
DesugaringContext,
|
||||
DesugaringStep,
|
||||
Each,
|
||||
Fuse,
|
||||
On,
|
||||
Pass,
|
||||
SetDefault,
|
||||
} from "./Conversion"
|
||||
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||
import { PrepareLayer } from "./PrepareLayer"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import Constants from "../../Constants"
|
||||
import CreateNoteImportLayer from "./CreateNoteImportLayer"
|
||||
import LayerConfig from "../LayerConfig"
|
||||
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||
import { SubstitutedTranslation } from "../../../UI/SubstitutedTranslation"
|
||||
import DependencyCalculator from "../DependencyCalculator"
|
||||
import { AddContextToTranslations } from "./AddContextToTranslations"
|
||||
|
||||
class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> {
|
||||
private readonly _state: DesugaringContext;
|
||||
class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJson[]> {
|
||||
private readonly _state: DesugaringContext
|
||||
|
||||
constructor(
|
||||
state: DesugaringContext,
|
||||
) {
|
||||
super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", [], "SubstituteLayer");
|
||||
this._state = state;
|
||||
constructor(state: DesugaringContext) {
|
||||
super(
|
||||
"Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form",
|
||||
[],
|
||||
"SubstituteLayer"
|
||||
)
|
||||
this._state = state
|
||||
}
|
||||
|
||||
convert(json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[], information?: string[] } {
|
||||
convert(
|
||||
json: string | LayerConfigJson,
|
||||
context: string
|
||||
): { result: LayerConfigJson[]; errors: string[]; information?: string[] } {
|
||||
const errors = []
|
||||
const information = []
|
||||
const state = this._state
|
||||
|
||||
function reportNotFound(name: string) {
|
||||
const knownLayers = Array.from(state.sharedLayers.keys())
|
||||
const withDistance = knownLayers.map(lname => [lname, Utils.levenshteinDistance(name, lname)])
|
||||
const withDistance = knownLayers.map((lname) => [
|
||||
lname,
|
||||
Utils.levenshteinDistance(name, lname),
|
||||
])
|
||||
withDistance.sort((a, b) => a[1] - b[1])
|
||||
const ids = withDistance.map(n => n[0])
|
||||
const ids = withDistance.map((n) => n[0])
|
||||
// Known builtin layers are "+.join(",")+"\n For more information, see "
|
||||
errors.push(`${context}: The layer with name ${name} was not found as a builtin layer. Perhaps you meant ${ids[0]}, ${ids[1]} or ${ids[2]}?
|
||||
For an overview of all available layers, refer to https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinLayers.md`)
|
||||
}
|
||||
|
||||
|
||||
if (typeof json === "string") {
|
||||
const found = state.sharedLayers.get(json)
|
||||
if (found === undefined) {
|
||||
|
@ -48,7 +65,7 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig
|
|||
}
|
||||
return {
|
||||
result: [found],
|
||||
errors
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,49 +82,80 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig
|
|||
reportNotFound(name)
|
||||
continue
|
||||
}
|
||||
if (json["override"]["tagRenderings"] !== undefined && (found["tagRenderings"] ?? []).length > 0) {
|
||||
errors.push(`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`)
|
||||
if (
|
||||
json["override"]["tagRenderings"] !== undefined &&
|
||||
(found["tagRenderings"] ?? []).length > 0
|
||||
) {
|
||||
errors.push(
|
||||
`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`
|
||||
)
|
||||
}
|
||||
try {
|
||||
Utils.Merge(json["override"], found);
|
||||
Utils.Merge(json["override"], found)
|
||||
layers.push(found)
|
||||
} catch (e) {
|
||||
errors.push(`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(json["override"],)}`)
|
||||
errors.push(
|
||||
`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(
|
||||
json["override"]
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
if (json["hideTagRenderingsWithLabels"]) {
|
||||
const hideLabels: Set<string> = new Set(json["hideTagRenderingsWithLabels"])
|
||||
// These labels caused at least one deletion
|
||||
const usedLabels: Set<string> = new Set<string>();
|
||||
const usedLabels: Set<string> = new Set<string>()
|
||||
const filtered = []
|
||||
for (const tr of found.tagRenderings) {
|
||||
const labels = tr["labels"]
|
||||
if (labels !== undefined) {
|
||||
const forbiddenLabel = labels.findIndex(l => hideLabels.has(l))
|
||||
const forbiddenLabel = labels.findIndex((l) => hideLabels.has(l))
|
||||
if (forbiddenLabel >= 0) {
|
||||
usedLabels.add(labels[forbiddenLabel])
|
||||
information.push(context + ": Dropping tagRendering " + tr["id"] + " as it has a forbidden label: " + labels[forbiddenLabel])
|
||||
information.push(
|
||||
context +
|
||||
": Dropping tagRendering " +
|
||||
tr["id"] +
|
||||
" as it has a forbidden label: " +
|
||||
labels[forbiddenLabel]
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (hideLabels.has(tr["id"])) {
|
||||
usedLabels.add(tr["id"])
|
||||
information.push(context + ": Dropping tagRendering " + tr["id"] + " as its id is a forbidden label")
|
||||
information.push(
|
||||
context +
|
||||
": Dropping tagRendering " +
|
||||
tr["id"] +
|
||||
" as its id is a forbidden label"
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (hideLabels.has(tr["group"])) {
|
||||
usedLabels.add(tr["group"])
|
||||
information.push(context + ": Dropping tagRendering " + tr["id"] + " as its group `" + tr["group"] + "` is a forbidden label")
|
||||
information.push(
|
||||
context +
|
||||
": Dropping tagRendering " +
|
||||
tr["id"] +
|
||||
" as its group `" +
|
||||
tr["group"] +
|
||||
"` is a forbidden label"
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
filtered.push(tr)
|
||||
}
|
||||
const unused = Array.from(hideLabels).filter(l => !usedLabels.has(l))
|
||||
const unused = Array.from(hideLabels).filter((l) => !usedLabels.has(l))
|
||||
if (unused.length > 0) {
|
||||
errors.push("This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " + unused.join(", ") + "\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore")
|
||||
errors.push(
|
||||
"This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " +
|
||||
unused.join(", ") +
|
||||
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
|
||||
)
|
||||
}
|
||||
found.tagRenderings = filtered
|
||||
}
|
||||
|
@ -115,33 +163,38 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig
|
|||
return {
|
||||
result: layers,
|
||||
errors,
|
||||
information
|
||||
information,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
result: [json],
|
||||
errors
|
||||
};
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
|
||||
private _state: DesugaringContext;
|
||||
private _state: DesugaringContext
|
||||
|
||||
constructor(state: DesugaringContext) {
|
||||
super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"], "AddDefaultLayers");
|
||||
this._state = state;
|
||||
super(
|
||||
"Adds the default layers, namely: " + Constants.added_by_default.join(", "),
|
||||
["layers"],
|
||||
"AddDefaultLayers"
|
||||
)
|
||||
this._state = state
|
||||
}
|
||||
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
const state = this._state
|
||||
json.layers = [...json.layers]
|
||||
const alreadyLoaded = new Set(json.layers.map(l => l["id"]))
|
||||
const alreadyLoaded = new Set(json.layers.map((l) => l["id"]))
|
||||
|
||||
for (const layerName of Constants.added_by_default) {
|
||||
const v = state.sharedLayers.get(layerName)
|
||||
|
@ -150,7 +203,13 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
|
|||
continue
|
||||
}
|
||||
if (alreadyLoaded.has(v.id)) {
|
||||
warnings.push("Layout " + context + " already has a layer with name " + v.id + "; skipping inclusion of this builtin layer")
|
||||
warnings.push(
|
||||
"Layout " +
|
||||
context +
|
||||
" already has a layer with name " +
|
||||
v.id +
|
||||
"; skipping inclusion of this builtin layer"
|
||||
)
|
||||
continue
|
||||
}
|
||||
json.layers.push(v)
|
||||
|
@ -159,34 +218,43 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
|
|||
return {
|
||||
result: json,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
|
||||
constructor() {
|
||||
super("For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)", ["layers"], "AddImportLayers");
|
||||
super(
|
||||
"For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)",
|
||||
["layers"],
|
||||
"AddImportLayers"
|
||||
)
|
||||
}
|
||||
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[], warnings?: string[] } {
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
|
||||
if (!(json.enableNoteImports ?? true)) {
|
||||
return {
|
||||
warnings: ["Not creating a note import layers for theme "+json.id+" as they are disabled"],
|
||||
result: json
|
||||
};
|
||||
warnings: [
|
||||
"Not creating a note import layers for theme " +
|
||||
json.id +
|
||||
" as they are disabled",
|
||||
],
|
||||
result: json,
|
||||
}
|
||||
}
|
||||
const errors = []
|
||||
|
||||
json = {...json}
|
||||
const allLayers: LayerConfigJson[] = <LayerConfigJson[]>json.layers;
|
||||
json = { ...json }
|
||||
const allLayers: LayerConfigJson[] = <LayerConfigJson[]>json.layers
|
||||
json.layers = [...json.layers]
|
||||
|
||||
|
||||
const creator = new CreateNoteImportLayer()
|
||||
for (let i1 = 0; i1 < allLayers.length; i1++) {
|
||||
const layer = allLayers[i1];
|
||||
const layer = allLayers[i1]
|
||||
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
|
||||
// Priviliged layers are skipped
|
||||
continue
|
||||
|
@ -204,12 +272,14 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
|
|||
|
||||
if (layer.presets === undefined || layer.presets.length == 0) {
|
||||
// A preset is needed to be able to generate a new point
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const importLayerResult = creator.convert(layer, context + ".(noteimportlayer)[" + i1 + "]")
|
||||
const importLayerResult = creator.convert(
|
||||
layer,
|
||||
context + ".(noteimportlayer)[" + i1 + "]"
|
||||
)
|
||||
if (importLayerResult.result !== undefined) {
|
||||
json.layers.push(importLayerResult.result)
|
||||
}
|
||||
|
@ -220,18 +290,21 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
|
|||
|
||||
return {
|
||||
errors,
|
||||
result: json
|
||||
};
|
||||
result: json,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
|
||||
private readonly _state: DesugaringContext;
|
||||
private readonly _state: DesugaringContext
|
||||
|
||||
constructor(state: DesugaringContext,) {
|
||||
super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"], "AddMiniMap");
|
||||
this._state = state;
|
||||
constructor(state: DesugaringContext) {
|
||||
super(
|
||||
"Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap",
|
||||
["tagRenderings"],
|
||||
"AddMiniMap"
|
||||
)
|
||||
this._state = state
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -249,72 +322,94 @@ export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
|
|||
* AddMiniMap.hasMinimap({render: "Some random value {minimap}"}) // => false
|
||||
*/
|
||||
static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean {
|
||||
const translations: any[] = Utils.NoNull([renderingConfig.render, ...(renderingConfig.mappings ?? []).map(m => m.then)]);
|
||||
const translations: any[] = Utils.NoNull([
|
||||
renderingConfig.render,
|
||||
...(renderingConfig.mappings ?? []).map((m) => m.then),
|
||||
])
|
||||
for (let translation of translations) {
|
||||
if (typeof translation == "string") {
|
||||
translation = {"*": translation}
|
||||
translation = { "*": translation }
|
||||
}
|
||||
|
||||
for (const key in translation) {
|
||||
if (!translation.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
const template = translation[key]
|
||||
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
|
||||
const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
|
||||
const hasMiniMap = parts
|
||||
.filter((part) => part.special !== undefined)
|
||||
.some((special) => special.special.func.funcName === "minimap")
|
||||
if (hasMiniMap) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson } {
|
||||
|
||||
const state = this._state;
|
||||
const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(<TagRenderingConfigJson>tr)) ?? true
|
||||
const state = this._state
|
||||
const hasMinimap =
|
||||
layerConfig.tagRenderings?.some((tr) =>
|
||||
AddMiniMap.hasMinimap(<TagRenderingConfigJson>tr)
|
||||
) ?? true
|
||||
if (!hasMinimap) {
|
||||
layerConfig = {...layerConfig}
|
||||
layerConfig = { ...layerConfig }
|
||||
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
|
||||
layerConfig.tagRenderings.push(state.tagRenderings.get("questions"))
|
||||
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
|
||||
}
|
||||
|
||||
return {
|
||||
result: layerConfig
|
||||
};
|
||||
result: layerConfig,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AddContextToTransltionsInLayout extends DesugaringStep <LayoutConfigJson> {
|
||||
|
||||
class AddContextToTransltionsInLayout extends DesugaringStep<LayoutConfigJson> {
|
||||
constructor() {
|
||||
super("Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too", ["_context"], "AddContextToTranlationsInLayout");
|
||||
super(
|
||||
"Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too",
|
||||
["_context"],
|
||||
"AddContextToTranlationsInLayout"
|
||||
)
|
||||
}
|
||||
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): {
|
||||
result: LayoutConfigJson
|
||||
errors?: string[]
|
||||
warnings?: string[]
|
||||
information?: string[]
|
||||
} {
|
||||
const conversion = new AddContextToTranslations<LayoutConfigJson>("themes:")
|
||||
return conversion.convert(json, json.id);
|
||||
return conversion.convert(json, json.id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
|
||||
|
||||
constructor() {
|
||||
super("Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards", ["overrideAll", "layers"], "ApplyOverrideAll");
|
||||
super(
|
||||
"Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards",
|
||||
["overrideAll", "layers"],
|
||||
"ApplyOverrideAll"
|
||||
)
|
||||
}
|
||||
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
|
||||
const overrideAll = json.overrideAll;
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
||||
const overrideAll = json.overrideAll
|
||||
if (overrideAll === undefined) {
|
||||
return {result: json, warnings: [], errors: []}
|
||||
return { result: json, warnings: [], errors: [] }
|
||||
}
|
||||
|
||||
json = {...json}
|
||||
json = { ...json }
|
||||
|
||||
delete json.overrideAll
|
||||
const newLayers = []
|
||||
|
@ -325,157 +420,215 @@ class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
|
|||
}
|
||||
json.layers = newLayers
|
||||
|
||||
|
||||
return {result: json, warnings: [], errors: []};
|
||||
return { result: json, warnings: [], errors: [] }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
|
||||
private readonly _state: DesugaringContext;
|
||||
private readonly _state: DesugaringContext
|
||||
|
||||
constructor(state: DesugaringContext,) {
|
||||
constructor(state: DesugaringContext) {
|
||||
super(
|
||||
`If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)
|
||||
|
||||
Note that these layers are added _at the start_ of the layer list, meaning that they will see _every_ feature.
|
||||
Furthermore, \`passAllFeatures\` will be set, so that they won't steal away features from further layers.
|
||||
Some layers (e.g. \`all_buildings_and_walls\' or \'streets_with_a_name\') are invisible, so by default, \'force_load\' is set too.
|
||||
`, ["layers"], "AddDependencyLayersToTheme");
|
||||
this._state = state;
|
||||
`,
|
||||
["layers"],
|
||||
"AddDependencyLayersToTheme"
|
||||
)
|
||||
this._state = state
|
||||
}
|
||||
|
||||
private static CalculateDependencies(alreadyLoaded: LayerConfigJson[], allKnownLayers: Map<string, LayerConfigJson>, themeId: string):
|
||||
{config: LayerConfigJson, reason: string}[] {
|
||||
const dependenciesToAdd: {config: LayerConfigJson, reason: string}[] = []
|
||||
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map(l => l.id));
|
||||
private static CalculateDependencies(
|
||||
alreadyLoaded: LayerConfigJson[],
|
||||
allKnownLayers: Map<string, LayerConfigJson>,
|
||||
themeId: string
|
||||
): { config: LayerConfigJson; reason: string }[] {
|
||||
const dependenciesToAdd: { config: LayerConfigJson; reason: string }[] = []
|
||||
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map((l) => l.id))
|
||||
|
||||
// Verify cross-dependencies
|
||||
let unmetDependencies: { neededLayer: string, neededBy: string, reason: string, context?: string }[] = []
|
||||
let unmetDependencies: {
|
||||
neededLayer: string
|
||||
neededBy: string
|
||||
reason: string
|
||||
context?: string
|
||||
}[] = []
|
||||
do {
|
||||
const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
|
||||
const dependencies: {
|
||||
neededLayer: string
|
||||
reason: string
|
||||
context?: string
|
||||
neededBy: string
|
||||
}[] = []
|
||||
|
||||
for (const layerConfig of alreadyLoaded) {
|
||||
try {
|
||||
const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig, themeId+"(dependencies)"))
|
||||
const layerDeps = DependencyCalculator.getLayerDependencies(
|
||||
new LayerConfig(layerConfig, themeId + "(dependencies)")
|
||||
)
|
||||
dependencies.push(...layerDeps)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw "Detecting layer dependencies for " + layerConfig.id + " failed due to " + e
|
||||
throw (
|
||||
"Detecting layer dependencies for " + layerConfig.id + " failed due to " + e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const dependency of dependencies) {
|
||||
if (loadedLayerIds.has(dependency.neededLayer)) {
|
||||
// We mark the needed layer as 'mustLoad'
|
||||
alreadyLoaded.find(l => l.id === dependency.neededLayer).forceLoad = true
|
||||
alreadyLoaded.find((l) => l.id === dependency.neededLayer).forceLoad = true
|
||||
}
|
||||
}
|
||||
|
||||
// During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
|
||||
// Their existence is checked elsewhere, so this is fine
|
||||
unmetDependencies = dependencies.filter(dep => !loadedLayerIds.has(dep.neededLayer))
|
||||
unmetDependencies = dependencies.filter((dep) => !loadedLayerIds.has(dep.neededLayer))
|
||||
for (const unmetDependency of unmetDependencies) {
|
||||
if (loadedLayerIds.has(unmetDependency.neededLayer)) {
|
||||
continue
|
||||
}
|
||||
const dep = Utils.Clone(allKnownLayers.get(unmetDependency.neededLayer))
|
||||
const reason = "This layer is needed by " + unmetDependency.neededBy +" because " +
|
||||
unmetDependency.reason + " (at " + unmetDependency.context + ")";
|
||||
const reason =
|
||||
"This layer is needed by " +
|
||||
unmetDependency.neededBy +
|
||||
" because " +
|
||||
unmetDependency.reason +
|
||||
" (at " +
|
||||
unmetDependency.context +
|
||||
")"
|
||||
if (dep === undefined) {
|
||||
const message =
|
||||
["Loading a dependency failed: layer " + unmetDependency.neededLayer + " is not found, neither as layer of " + themeId + " nor as builtin layer.",
|
||||
reason,
|
||||
"Loaded layers are: " + alreadyLoaded.map(l => l.id).join(",")
|
||||
|
||||
]
|
||||
throw message.join("\n\t");
|
||||
const message = [
|
||||
"Loading a dependency failed: layer " +
|
||||
unmetDependency.neededLayer +
|
||||
" is not found, neither as layer of " +
|
||||
themeId +
|
||||
" nor as builtin layer.",
|
||||
reason,
|
||||
"Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(","),
|
||||
]
|
||||
throw message.join("\n\t")
|
||||
}
|
||||
|
||||
dep.forceLoad = true;
|
||||
dep.passAllFeatures = true;
|
||||
dep.description = reason;
|
||||
|
||||
dep.forceLoad = true
|
||||
dep.passAllFeatures = true
|
||||
dep.description = reason
|
||||
dependenciesToAdd.unshift({
|
||||
config: dep,
|
||||
reason
|
||||
reason,
|
||||
})
|
||||
loadedLayerIds.add(dep.id);
|
||||
unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer)
|
||||
loadedLayerIds.add(dep.id)
|
||||
unmetDependencies = unmetDependencies.filter(
|
||||
(d) => d.neededLayer !== unmetDependency.neededLayer
|
||||
)
|
||||
}
|
||||
|
||||
} while (unmetDependencies.length > 0)
|
||||
|
||||
return dependenciesToAdd
|
||||
}
|
||||
|
||||
convert(theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; information: string[] } {
|
||||
convert(
|
||||
theme: LayoutConfigJson,
|
||||
context: string
|
||||
): { result: LayoutConfigJson; information: string[] } {
|
||||
const state = this._state
|
||||
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers;
|
||||
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings;
|
||||
const information = [];
|
||||
const layers: LayerConfigJson[] = <LayerConfigJson[]>theme.layers; // Layers should be expanded at this point
|
||||
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers
|
||||
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings
|
||||
const information = []
|
||||
const layers: LayerConfigJson[] = <LayerConfigJson[]>theme.layers // Layers should be expanded at this point
|
||||
|
||||
knownTagRenderings.forEach((value, key) => {
|
||||
value.id = key;
|
||||
value.id = key
|
||||
})
|
||||
|
||||
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id);
|
||||
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(
|
||||
layers,
|
||||
allKnownLayers,
|
||||
theme.id
|
||||
)
|
||||
for (const dependency of dependencies) {
|
||||
|
||||
}
|
||||
if (dependencies.length > 0) {
|
||||
for (const dependency of dependencies) {
|
||||
information.push(context + ": added " + dependency.config.id + " to the theme. "+dependency.reason)
|
||||
|
||||
information.push(
|
||||
context +
|
||||
": added " +
|
||||
dependency.config.id +
|
||||
" to the theme. " +
|
||||
dependency.reason
|
||||
)
|
||||
}
|
||||
}
|
||||
layers.unshift(...dependencies.map(l => l.config));
|
||||
layers.unshift(...dependencies.map((l) => l.config))
|
||||
|
||||
return {
|
||||
result: {
|
||||
...theme,
|
||||
layers: layers
|
||||
layers: layers,
|
||||
},
|
||||
information
|
||||
};
|
||||
information,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PreparePersonalTheme extends DesugaringStep<LayoutConfigJson> {
|
||||
private readonly _state: DesugaringContext;
|
||||
private readonly _state: DesugaringContext
|
||||
|
||||
constructor(state: DesugaringContext) {
|
||||
super("Adds every public layer to the personal theme", ["layers"], "PreparePersonalTheme");
|
||||
this._state = state;
|
||||
super("Adds every public layer to the personal theme", ["layers"], "PreparePersonalTheme")
|
||||
this._state = state
|
||||
}
|
||||
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): {
|
||||
result: LayoutConfigJson
|
||||
errors?: string[]
|
||||
warnings?: string[]
|
||||
information?: string[]
|
||||
} {
|
||||
if (json.id !== "personal") {
|
||||
return {result: json}
|
||||
return { result: json }
|
||||
}
|
||||
|
||||
|
||||
// The only thing this _really_ does, is adding the layer-ids into 'layers'
|
||||
// All other preparations are done by the 'override-all'-block in personal.json
|
||||
|
||||
json.layers = Array.from(this._state.sharedLayers.keys())
|
||||
.filter(l => Constants.priviliged_layers.indexOf(l) < 0)
|
||||
.filter(l => this._state.publicLayers.has(l))
|
||||
return {result: json, information: [
|
||||
"The personal theme has "+json.layers.length+" public layers"
|
||||
]};
|
||||
.filter((l) => Constants.priviliged_layers.indexOf(l) < 0)
|
||||
.filter((l) => this._state.publicLayers.has(l))
|
||||
return {
|
||||
result: json,
|
||||
information: ["The personal theme has " + json.layers.length + " public layers"],
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson> {
|
||||
|
||||
constructor() {
|
||||
super("Generates a warning if a theme uses an unsubstituted layer", ["layers"], "WarnForUnsubstitutedLayersInTheme");
|
||||
super(
|
||||
"Generates a warning if a theme uses an unsubstituted layer",
|
||||
["layers"],
|
||||
"WarnForUnsubstitutedLayersInTheme"
|
||||
)
|
||||
}
|
||||
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): {
|
||||
result: LayoutConfigJson
|
||||
errors?: string[]
|
||||
warnings?: string[]
|
||||
information?: string[]
|
||||
} {
|
||||
if (json.hideFromOverview === true) {
|
||||
return {result: json}
|
||||
return { result: json }
|
||||
}
|
||||
const warnings = []
|
||||
for (const layer of json.layers) {
|
||||
|
@ -490,21 +643,28 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
|
|||
continue
|
||||
}
|
||||
|
||||
const wrn = "The theme " + json.id + " has an inline layer: " + layer["id"] + ". This is discouraged."
|
||||
const wrn =
|
||||
"The theme " +
|
||||
json.id +
|
||||
" has an inline layer: " +
|
||||
layer["id"] +
|
||||
". This is discouraged."
|
||||
warnings.push(wrn)
|
||||
}
|
||||
return {
|
||||
result: json,
|
||||
warnings
|
||||
};
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PrepareTheme extends Fuse<LayoutConfigJson> {
|
||||
constructor(state: DesugaringContext, options?: {
|
||||
skipDefaultLayers: false | boolean
|
||||
}) {
|
||||
constructor(
|
||||
state: DesugaringContext,
|
||||
options?: {
|
||||
skipDefaultLayers: false | boolean
|
||||
}
|
||||
) {
|
||||
super(
|
||||
"Fully prepares and expands a theme",
|
||||
|
||||
|
@ -519,10 +679,12 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
|
|||
new ApplyOverrideAll(),
|
||||
// And then we prepare all the layers _again_ in case that an override all contained unexpanded tagrenderings!
|
||||
new On("layers", new Each(new PrepareLayer(state))),
|
||||
options?.skipDefaultLayers ? new Pass("AddDefaultLayers is disabled due to the set flag") : new AddDefaultLayers(state),
|
||||
options?.skipDefaultLayers
|
||||
? new Pass("AddDefaultLayers is disabled due to the set flag")
|
||||
: new AddDefaultLayers(state),
|
||||
new AddDependencyLayersToTheme(state),
|
||||
new AddImportLayers(),
|
||||
new On("layers", new Each(new AddMiniMap(state)))
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +1,119 @@
|
|||
import {DesugaringStep, Each, Fuse, On} from "./Conversion";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import LayerConfig from "../LayerConfig";
|
||||
import {Utils} from "../../../Utils";
|
||||
import Constants from "../../Constants";
|
||||
import {Translation} from "../../../UI/i18n/Translation";
|
||||
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
|
||||
import LayoutConfig from "../LayoutConfig";
|
||||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||
import {ExtractImages} from "./FixImages";
|
||||
import ScriptUtils from "../../../scripts/ScriptUtils";
|
||||
import {And} from "../../../Logic/Tags/And";
|
||||
import Translations from "../../../UI/i18n/Translations";
|
||||
import Svg from "../../../Svg";
|
||||
import {QuestionableTagRenderingConfigJson} from "../Json/QuestionableTagRenderingConfigJson";
|
||||
|
||||
import { DesugaringStep, Each, Fuse, On } from "./Conversion"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import LayerConfig from "../LayerConfig"
|
||||
import { Utils } from "../../../Utils"
|
||||
import Constants from "../../Constants"
|
||||
import { Translation } from "../../../UI/i18n/Translation"
|
||||
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||
import LayoutConfig from "../LayoutConfig"
|
||||
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import { ExtractImages } from "./FixImages"
|
||||
import ScriptUtils from "../../../scripts/ScriptUtils"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
import Svg from "../../../Svg"
|
||||
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||
|
||||
class ValidateLanguageCompleteness extends DesugaringStep<any> {
|
||||
|
||||
private readonly _languages: string[];
|
||||
private readonly _languages: string[]
|
||||
|
||||
constructor(...languages: string[]) {
|
||||
super("Checks that the given object is fully translated in the specified languages", [], "ValidateLanguageCompleteness");
|
||||
this._languages = languages ?? ["en"];
|
||||
super(
|
||||
"Checks that the given object is fully translated in the specified languages",
|
||||
[],
|
||||
"ValidateLanguageCompleteness"
|
||||
)
|
||||
this._languages = languages ?? ["en"]
|
||||
}
|
||||
|
||||
convert(obj: any, context: string): { result: LayerConfig; errors: string[] } {
|
||||
const errors = []
|
||||
const translations = Translation.ExtractAllTranslationsFrom(
|
||||
obj
|
||||
)
|
||||
const translations = Translation.ExtractAllTranslationsFrom(obj)
|
||||
for (const neededLanguage of this._languages) {
|
||||
translations
|
||||
.filter(t => t.tr.translations[neededLanguage] === undefined && t.tr.translations["*"] === undefined)
|
||||
.forEach(missing => {
|
||||
errors.push(context + "A theme should be translation-complete for " + neededLanguage + ", but it lacks a translation for " + missing.context + ".\n\tThe known translation is " + missing.tr.textFor('en'))
|
||||
.filter(
|
||||
(t) =>
|
||||
t.tr.translations[neededLanguage] === undefined &&
|
||||
t.tr.translations["*"] === undefined
|
||||
)
|
||||
.forEach((missing) => {
|
||||
errors.push(
|
||||
context +
|
||||
"A theme should be translation-complete for " +
|
||||
neededLanguage +
|
||||
", but it lacks a translation for " +
|
||||
missing.context +
|
||||
".\n\tThe known translation is " +
|
||||
missing.tr.textFor("en")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
result: obj,
|
||||
errors
|
||||
};
|
||||
errors,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DoesImageExist extends DesugaringStep<string> {
|
||||
private readonly _knownImagePaths: Set<string>
|
||||
private readonly doesPathExist: (path: string) => boolean = undefined
|
||||
|
||||
private readonly _knownImagePaths: Set<string>;
|
||||
private readonly doesPathExist: (path: string) => boolean = undefined;
|
||||
|
||||
constructor(knownImagePaths: Set<string>, checkExistsSync: (path: string) => boolean = undefined) {
|
||||
super("Checks if an image exists", [], "DoesImageExist");
|
||||
this._knownImagePaths = knownImagePaths;
|
||||
this.doesPathExist = checkExistsSync;
|
||||
constructor(
|
||||
knownImagePaths: Set<string>,
|
||||
checkExistsSync: (path: string) => boolean = undefined
|
||||
) {
|
||||
super("Checks if an image exists", [], "DoesImageExist")
|
||||
this._knownImagePaths = knownImagePaths
|
||||
this.doesPathExist = checkExistsSync
|
||||
}
|
||||
|
||||
convert(image: string, context: string): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
convert(
|
||||
image: string,
|
||||
context: string
|
||||
): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
const information = []
|
||||
if (image.indexOf("{") >= 0) {
|
||||
information.push("Ignoring image with { in the path: " + image)
|
||||
return {result: image}
|
||||
return { result: image }
|
||||
}
|
||||
|
||||
if (image === "assets/SocialImage.png") {
|
||||
return {result: image}
|
||||
return { result: image }
|
||||
}
|
||||
if (image.match(/[a-z]*/)) {
|
||||
|
||||
if (Svg.All[image + ".svg"] !== undefined) {
|
||||
// This is a builtin img, e.g. 'checkmark' or 'crosshair'
|
||||
return {result: image};
|
||||
return { result: image }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!this._knownImagePaths.has(image)) {
|
||||
if (this.doesPathExist === undefined) {
|
||||
errors.push(`Image with path ${image} not found or not attributed; it is used in ${context}`)
|
||||
errors.push(
|
||||
`Image with path ${image} not found or not attributed; it is used in ${context}`
|
||||
)
|
||||
} else if (!this.doesPathExist(image)) {
|
||||
errors.push(`Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.`)
|
||||
errors.push(
|
||||
`Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.`
|
||||
)
|
||||
} else {
|
||||
errors.push(`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`)
|
||||
errors.push(
|
||||
`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: image,
|
||||
errors, warnings, information
|
||||
errors,
|
||||
warnings,
|
||||
information,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
||||
|
@ -98,20 +121,28 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
* The paths where this layer is originally saved. Triggers some extra checks
|
||||
* @private
|
||||
*/
|
||||
private readonly _path?: string;
|
||||
private readonly _isBuiltin: boolean;
|
||||
private _sharedTagRenderings: Map<string, any>;
|
||||
private readonly _validateImage: DesugaringStep<string>;
|
||||
private readonly _path?: string
|
||||
private readonly _isBuiltin: boolean
|
||||
private _sharedTagRenderings: Map<string, any>
|
||||
private readonly _validateImage: DesugaringStep<string>
|
||||
|
||||
constructor(doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, sharedTagRenderings: Map<string, any>) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme");
|
||||
this._validateImage = doesImageExist;
|
||||
this._path = path;
|
||||
this._isBuiltin = isBuiltin;
|
||||
this._sharedTagRenderings = sharedTagRenderings;
|
||||
constructor(
|
||||
doesImageExist: DoesImageExist,
|
||||
path: string,
|
||||
isBuiltin: boolean,
|
||||
sharedTagRenderings: Map<string, any>
|
||||
) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme")
|
||||
this._validateImage = doesImageExist
|
||||
this._path = path
|
||||
this._isBuiltin = isBuiltin
|
||||
this._sharedTagRenderings = sharedTagRenderings
|
||||
}
|
||||
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[], warnings: string[], information: string[] } {
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): { result: LayoutConfigJson; errors: string[]; warnings: string[]; information: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
const information = []
|
||||
|
@ -119,55 +150,77 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
const theme = new LayoutConfig(json, true)
|
||||
|
||||
{
|
||||
// Legacy format checks
|
||||
// Legacy format checks
|
||||
if (this._isBuiltin) {
|
||||
if (json["units"] !== undefined) {
|
||||
errors.push("The theme " + json.id + " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ")
|
||||
errors.push(
|
||||
"The theme " +
|
||||
json.id +
|
||||
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) "
|
||||
)
|
||||
}
|
||||
if (json["roamingRenderings"] !== undefined) {
|
||||
errors.push("Theme " + json.id + " contains an old 'roamingRenderings'. Use an 'overrideAll' instead")
|
||||
errors.push(
|
||||
"Theme " +
|
||||
json.id +
|
||||
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
// Check images: are they local, are the licenses there, is the theme icon square, ...
|
||||
const images = new ExtractImages(this._isBuiltin, this._sharedTagRenderings).convertStrict(json, "validation")
|
||||
const remoteImages = images.filter(img => img.indexOf("http") == 0)
|
||||
const images = new ExtractImages(
|
||||
this._isBuiltin,
|
||||
this._sharedTagRenderings
|
||||
).convertStrict(json, "validation")
|
||||
const remoteImages = images.filter((img) => img.indexOf("http") == 0)
|
||||
for (const remoteImage of remoteImages) {
|
||||
errors.push("Found a remote image: " + remoteImage + " in theme " + json.id + ", please download it.")
|
||||
errors.push(
|
||||
"Found a remote image: " +
|
||||
remoteImage +
|
||||
" in theme " +
|
||||
json.id +
|
||||
", please download it."
|
||||
)
|
||||
}
|
||||
for (const image of images) {
|
||||
this._validateImage.convertJoin(image, context === undefined ? "" : ` in a layer defined in the theme ${context}`, errors, warnings, information)
|
||||
this._validateImage.convertJoin(
|
||||
image,
|
||||
context === undefined ? "" : ` in a layer defined in the theme ${context}`,
|
||||
errors,
|
||||
warnings,
|
||||
information
|
||||
)
|
||||
}
|
||||
|
||||
if (json.icon.endsWith(".svg")) {
|
||||
try {
|
||||
ScriptUtils.ReadSvgSync(json.icon, svg => {
|
||||
const width: string = svg.$.width;
|
||||
const height: string = svg.$.height;
|
||||
ScriptUtils.ReadSvgSync(json.icon, (svg) => {
|
||||
const width: string = svg.$.width
|
||||
const height: string = svg.$.height
|
||||
if (width !== height) {
|
||||
const e = `the icon for theme ${json.id} is not square. Please square the icon at ${json.icon}` +
|
||||
` Width = ${width} height = ${height}`;
|
||||
(json.hideFromOverview ? warnings : errors).push(e)
|
||||
const e =
|
||||
`the icon for theme ${json.id} is not square. Please square the icon at ${json.icon}` +
|
||||
` Width = ${width} height = ${height}`
|
||||
;(json.hideFromOverview ? warnings : errors).push(e)
|
||||
}
|
||||
|
||||
const w = parseInt(width);
|
||||
const w = parseInt(width)
|
||||
const h = parseInt(height)
|
||||
if (w < 370 || h < 370) {
|
||||
const e: string = [
|
||||
`the icon for theme ${json.id} is too small. Please rescale the icon at ${json.icon}`,
|
||||
`Even though an SVG is 'infinitely scaleable', the icon should be dimensioned bigger. One of the build steps of the theme does convert the image to a PNG (to serve as PWA-icon) and having a small dimension will cause blurry images.`,
|
||||
` Width = ${width} height = ${height}; we recommend a size of at least 500px * 500px and to use a square aspect ratio.`,
|
||||
].join("\n");
|
||||
(json.hideFromOverview ? warnings : errors).push(e)
|
||||
].join("\n")
|
||||
;(json.hideFromOverview ? warnings : errors).push(e)
|
||||
}
|
||||
|
||||
})
|
||||
} catch (e) {
|
||||
console.error("Could not read " + json.icon + " due to " + e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -175,36 +228,53 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
errors.push("Theme ids should be in lowercase, but it is " + theme.id)
|
||||
}
|
||||
|
||||
const filename = this._path.substring(this._path.lastIndexOf("/") + 1, this._path.length - 5)
|
||||
const filename = this._path.substring(
|
||||
this._path.lastIndexOf("/") + 1,
|
||||
this._path.length - 5
|
||||
)
|
||||
if (theme.id !== filename) {
|
||||
errors.push("Theme ids should be the same as the name.json, but we got id: " + theme.id + " and filename " + filename + " (" + this._path + ")")
|
||||
errors.push(
|
||||
"Theme ids should be the same as the name.json, but we got id: " +
|
||||
theme.id +
|
||||
" and filename " +
|
||||
filename +
|
||||
" (" +
|
||||
this._path +
|
||||
")"
|
||||
)
|
||||
}
|
||||
this._validateImage.convertJoin(theme.icon, context + ".icon", errors, warnings, information);
|
||||
const dups = Utils.Dupiclates(json.layers.map(layer => layer["id"]))
|
||||
this._validateImage.convertJoin(
|
||||
theme.icon,
|
||||
context + ".icon",
|
||||
errors,
|
||||
warnings,
|
||||
information
|
||||
)
|
||||
const dups = Utils.Dupiclates(json.layers.map((layer) => layer["id"]))
|
||||
if (dups.length > 0) {
|
||||
errors.push(`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`)
|
||||
errors.push(
|
||||
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`
|
||||
)
|
||||
}
|
||||
if (json["mustHaveLanguage"] !== undefined) {
|
||||
const checked = new ValidateLanguageCompleteness(...json["mustHaveLanguage"])
|
||||
.convert(theme, theme.id)
|
||||
const checked = new ValidateLanguageCompleteness(
|
||||
...json["mustHaveLanguage"]
|
||||
).convert(theme, theme.id)
|
||||
errors.push(...checked.errors)
|
||||
}
|
||||
if (!json.hideFromOverview && theme.id !== "personal") {
|
||||
|
||||
// The first key in the the title-field must be english, otherwise the title in the loading page will be the different language
|
||||
const targetLanguage = theme.title.SupportedLanguages()[0]
|
||||
if (targetLanguage !== "en") {
|
||||
warnings.push(`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`)
|
||||
warnings.push(
|
||||
`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`
|
||||
)
|
||||
}
|
||||
|
||||
// Official, public themes must have a full english translation
|
||||
const checked = new ValidateLanguageCompleteness("en")
|
||||
.convert(theme, theme.id)
|
||||
const checked = new ValidateLanguageCompleteness("en").convert(theme, theme.id)
|
||||
errors.push(...checked.errors)
|
||||
|
||||
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
errors.push(e)
|
||||
}
|
||||
|
@ -213,61 +283,86 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
result: json,
|
||||
errors,
|
||||
warnings,
|
||||
information
|
||||
};
|
||||
information,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
||||
constructor(doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, sharedTagRenderings: Map<string, any>) {
|
||||
super("Validates a theme and the contained layers",
|
||||
constructor(
|
||||
doesImageExist: DoesImageExist,
|
||||
path: string,
|
||||
isBuiltin: boolean,
|
||||
sharedTagRenderings: Map<string, any>
|
||||
) {
|
||||
super(
|
||||
"Validates a theme and the contained layers",
|
||||
new ValidateTheme(doesImageExist, path, isBuiltin, sharedTagRenderings),
|
||||
new On("layers", new Each(new ValidateLayer(undefined, false, doesImageExist)))
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
|
||||
|
||||
constructor() {
|
||||
super("Checks that an 'overrideAll' does not override a single override", [], "OverrideShadowingCheck");
|
||||
super(
|
||||
"Checks that an 'overrideAll' does not override a single override",
|
||||
[],
|
||||
"OverrideShadowingCheck"
|
||||
)
|
||||
}
|
||||
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
|
||||
|
||||
const overrideAll = json.overrideAll;
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
|
||||
const overrideAll = json.overrideAll
|
||||
if (overrideAll === undefined) {
|
||||
return {result: json}
|
||||
return { result: json }
|
||||
}
|
||||
|
||||
const errors = []
|
||||
const withOverride = json.layers.filter(l => l["override"] !== undefined)
|
||||
const withOverride = json.layers.filter((l) => l["override"] !== undefined)
|
||||
|
||||
for (const layer of withOverride) {
|
||||
for (const key in overrideAll) {
|
||||
if(key.endsWith("+") || key.startsWith("+")){
|
||||
if (key.endsWith("+") || key.startsWith("+")) {
|
||||
// This key will _add_ to the list, not overwrite it - so no warning is needed
|
||||
continue
|
||||
}
|
||||
if (layer["override"][key] !== undefined || layer["override"]["=" + key] !== undefined) {
|
||||
const w = "The override of layer " + JSON.stringify(layer["builtin"]) + " has a shadowed property: " + key + " is overriden by overrideAll of the theme";
|
||||
if (
|
||||
layer["override"][key] !== undefined ||
|
||||
layer["override"]["=" + key] !== undefined
|
||||
) {
|
||||
const w =
|
||||
"The override of layer " +
|
||||
JSON.stringify(layer["builtin"]) +
|
||||
" has a shadowed property: " +
|
||||
key +
|
||||
" is overriden by overrideAll of the theme"
|
||||
errors.push(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {result: json, errors}
|
||||
return { result: json, errors }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MiscThemeChecks extends DesugaringStep<LayoutConfigJson> {
|
||||
constructor() {
|
||||
super("Miscelleanous checks on the theme", [], "MiscThemesChecks");
|
||||
super("Miscelleanous checks on the theme", [], "MiscThemesChecks")
|
||||
}
|
||||
|
||||
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
convert(
|
||||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): {
|
||||
result: LayoutConfigJson
|
||||
errors?: string[]
|
||||
warnings?: string[]
|
||||
information?: string[]
|
||||
} {
|
||||
const warnings = []
|
||||
const errors = []
|
||||
if (json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)) {
|
||||
|
@ -279,29 +374,27 @@ class MiscThemeChecks extends DesugaringStep<LayoutConfigJson> {
|
|||
return {
|
||||
result: json,
|
||||
warnings,
|
||||
errors
|
||||
};
|
||||
errors,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PrevalidateTheme extends Fuse<LayoutConfigJson> {
|
||||
|
||||
constructor() {
|
||||
super("Various consistency checks on the raw JSON",
|
||||
super(
|
||||
"Various consistency checks on the raw JSON",
|
||||
new MiscThemeChecks(),
|
||||
new OverrideShadowingCheck()
|
||||
);
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRenderingConfigJson> {
|
||||
private readonly _calculatedTagNames: string[];
|
||||
private readonly _calculatedTagNames: string[]
|
||||
|
||||
constructor(layerConfig?: LayerConfigJson) {
|
||||
super("Checks that the mappings don't shadow each other", [], "DetectShadowedMappings");
|
||||
this._calculatedTagNames = DetectShadowedMappings.extractCalculatedTagNames(layerConfig);
|
||||
super("Checks that the mappings don't shadow each other", [], "DetectShadowedMappings")
|
||||
this._calculatedTagNames = DetectShadowedMappings.extractCalculatedTagNames(layerConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -309,14 +402,17 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
|
|||
* DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc:=js()"]}) // => ["_abc"]
|
||||
* DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc=js()"]}) // => ["_abc"]
|
||||
*/
|
||||
private static extractCalculatedTagNames(layerConfig?: LayerConfigJson | { calculatedTags: string [] }) {
|
||||
return layerConfig?.calculatedTags?.map(ct => {
|
||||
if (ct.indexOf(':=') >= 0) {
|
||||
return ct.split(':=')[0]
|
||||
}
|
||||
return ct.split("=")[0]
|
||||
}) ?? []
|
||||
|
||||
private static extractCalculatedTagNames(
|
||||
layerConfig?: LayerConfigJson | { calculatedTags: string[] }
|
||||
) {
|
||||
return (
|
||||
layerConfig?.calculatedTags?.map((ct) => {
|
||||
if (ct.indexOf(":=") >= 0) {
|
||||
return ct.split(":=")[0]
|
||||
}
|
||||
return ct.split("=")[0]
|
||||
}) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -352,20 +448,28 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
|
|||
* r.errors.length // => 1
|
||||
* r.errors[0].indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true
|
||||
*/
|
||||
convert(json: QuestionableTagRenderingConfigJson, context: string): { result: QuestionableTagRenderingConfigJson; errors?: string[]; warnings?: string[] } {
|
||||
convert(
|
||||
json: QuestionableTagRenderingConfigJson,
|
||||
context: string
|
||||
): { result: QuestionableTagRenderingConfigJson; errors?: string[]; warnings?: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
if (json.mappings === undefined || json.mappings.length === 0) {
|
||||
return {result: json}
|
||||
return { result: json }
|
||||
}
|
||||
const defaultProperties = {}
|
||||
for (const calculatedTagName of this._calculatedTagNames) {
|
||||
defaultProperties[calculatedTagName] = "some_calculated_tag_value_for_" + calculatedTagName
|
||||
defaultProperties[calculatedTagName] =
|
||||
"some_calculated_tag_value_for_" + calculatedTagName
|
||||
}
|
||||
const parsedConditions = json.mappings.map((m, i) => {
|
||||
const ctx = `${context}.mappings[${i}]`
|
||||
const ifTags = TagUtils.Tag(m.if, ctx);
|
||||
if (m.hideInAnswer !== undefined && m.hideInAnswer !== false && m.hideInAnswer !== true) {
|
||||
const ifTags = TagUtils.Tag(m.if, ctx)
|
||||
if (
|
||||
m.hideInAnswer !== undefined &&
|
||||
m.hideInAnswer !== false &&
|
||||
m.hideInAnswer !== true
|
||||
) {
|
||||
let conditionTags = TagUtils.Tag(m.hideInAnswer)
|
||||
// Merge the condition too!
|
||||
return new And([conditionTags, ifTags])
|
||||
|
@ -378,19 +482,29 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
|
|||
// Yes, it might be shadowed, but running this check is to difficult right now
|
||||
continue
|
||||
}
|
||||
const keyValues = parsedConditions[i].asChange(defaultProperties);
|
||||
const keyValues = parsedConditions[i].asChange(defaultProperties)
|
||||
const properties = {}
|
||||
keyValues.forEach(({k, v}) => {
|
||||
keyValues.forEach(({ k, v }) => {
|
||||
properties[k] = v
|
||||
})
|
||||
for (let j = 0; j < i; j++) {
|
||||
const doesMatch = parsedConditions[j].matchesProperties(properties)
|
||||
if (doesMatch && json.mappings[j].hideInAnswer === true && json.mappings[i].hideInAnswer !== true) {
|
||||
warnings.push(`At ${context}: Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`)
|
||||
if (
|
||||
doesMatch &&
|
||||
json.mappings[j].hideInAnswer === true &&
|
||||
json.mappings[i].hideInAnswer !== true
|
||||
) {
|
||||
warnings.push(
|
||||
`At ${context}: Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`
|
||||
)
|
||||
} else if (doesMatch) {
|
||||
// The current mapping is shadowed!
|
||||
errors.push(`At ${context}: Mapping ${i} is shadowed by mapping ${j} and will thus never be shown:
|
||||
The mapping ${parsedConditions[i].asHumanString(false, false, {})} is fully matched by a previous mapping (namely ${j}), which matches:
|
||||
The mapping ${parsedConditions[i].asHumanString(
|
||||
false,
|
||||
false,
|
||||
{}
|
||||
)} is fully matched by a previous mapping (namely ${j}), which matches:
|
||||
${parsedConditions[j].asHumanString(false, false, {})}.
|
||||
|
||||
To fix this problem, you can try to:
|
||||
|
@ -404,23 +518,26 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
|
|||
`)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
warnings,
|
||||
result: json
|
||||
};
|
||||
result: json,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJson> {
|
||||
private readonly _doesImageExist: DoesImageExist;
|
||||
private readonly _doesImageExist: DoesImageExist
|
||||
|
||||
constructor(doesImageExist: DoesImageExist) {
|
||||
super("Checks that 'then'clauses in mappings don't have images, but use 'icon' instead", [], "DetectMappingsWithImages");
|
||||
this._doesImageExist = doesImageExist;
|
||||
super(
|
||||
"Checks that 'then'clauses in mappings don't have images, but use 'icon' instead",
|
||||
[],
|
||||
"DetectMappingsWithImages"
|
||||
)
|
||||
this._doesImageExist = doesImageExist
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -443,31 +560,44 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
|
|||
* r.errors.length > 0 // => true
|
||||
* r.errors.some(msg => msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
|
||||
*/
|
||||
convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[], information?: string[] } {
|
||||
convert(
|
||||
json: TagRenderingConfigJson,
|
||||
context: string
|
||||
): {
|
||||
result: TagRenderingConfigJson
|
||||
errors?: string[]
|
||||
warnings?: string[]
|
||||
information?: string[]
|
||||
} {
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
const information: string[] = []
|
||||
if (json.mappings === undefined || json.mappings.length === 0) {
|
||||
return {result: json}
|
||||
return { result: json }
|
||||
}
|
||||
const ignoreToken = "ignore-image-in-then"
|
||||
for (let i = 0; i < json.mappings.length; i++) {
|
||||
|
||||
const mapping = json.mappings[i]
|
||||
const ignore = mapping["#"]?.indexOf(ignoreToken) >= 0
|
||||
const images = Utils.Dedup(Translations.T(mapping.then)?.ExtractImages() ?? [])
|
||||
const ctx = `${context}.mappings[${i}]`
|
||||
if (images.length > 0) {
|
||||
if (!ignore) {
|
||||
errors.push(`${ctx}: A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": <your-image>\` instead. The images found are ${images.join(", ")}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`)
|
||||
errors.push(
|
||||
`${ctx}: A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": <your-image>\` instead. The images found are ${images.join(
|
||||
", "
|
||||
)}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`
|
||||
)
|
||||
} else {
|
||||
information.push(`${ctx}: Ignored image ${images.join(", ")} in 'then'-clause of a mapping as this check has been disabled`)
|
||||
information.push(
|
||||
`${ctx}: Ignored image ${images.join(
|
||||
", "
|
||||
)} in 'then'-clause of a mapping as this check has been disabled`
|
||||
)
|
||||
|
||||
for (const image of images) {
|
||||
this._doesImageExist.convertJoin(image, ctx, errors, warnings, information);
|
||||
|
||||
this._doesImageExist.convertJoin(image, ctx, errors, warnings, information)
|
||||
}
|
||||
|
||||
}
|
||||
} else if (ignore) {
|
||||
warnings.push(`${ctx}: unused '${ignoreToken}' - please remove this`)
|
||||
|
@ -478,17 +608,18 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
|
|||
errors,
|
||||
warnings,
|
||||
information,
|
||||
result: json
|
||||
};
|
||||
result: json,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
|
||||
constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) {
|
||||
super("Various validation on tagRenderingConfigs",
|
||||
super(
|
||||
"Various validation on tagRenderingConfigs",
|
||||
new DetectShadowedMappings(layerConfig),
|
||||
new DetectMappingsWithImages(doesImageExist)
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -497,36 +628,45 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
* The paths where this layer is originally saved. Triggers some extra checks
|
||||
* @private
|
||||
*/
|
||||
private readonly _path?: string;
|
||||
private readonly _isBuiltin: boolean;
|
||||
private readonly _doesImageExist: DoesImageExist;
|
||||
private readonly _path?: string
|
||||
private readonly _isBuiltin: boolean
|
||||
private readonly _doesImageExist: DoesImageExist
|
||||
|
||||
constructor(path: string, isBuiltin: boolean, doesImageExist: DoesImageExist) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer");
|
||||
this._path = path;
|
||||
this._isBuiltin = isBuiltin;
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
|
||||
this._path = path
|
||||
this._isBuiltin = isBuiltin
|
||||
this._doesImageExist = doesImageExist
|
||||
}
|
||||
|
||||
convert(json: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings?: string[], information?: string[] } {
|
||||
convert(
|
||||
json: LayerConfigJson,
|
||||
context: string
|
||||
): { result: LayerConfigJson; errors: string[]; warnings?: string[]; information?: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
const information = []
|
||||
context = "While validating a layer: "+context
|
||||
context = "While validating a layer: " + context
|
||||
if (typeof json === "string") {
|
||||
errors.push(context + ": This layer hasn't been expanded: " + json)
|
||||
return {
|
||||
result: null,
|
||||
errors
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
if(json.tagRenderings !== undefined && json.tagRenderings.length > 0){
|
||||
if(json.title === undefined){
|
||||
errors.push(context + ": this layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error.")
|
||||
|
||||
if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) {
|
||||
if (json.title === undefined) {
|
||||
errors.push(
|
||||
context +
|
||||
": this layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error."
|
||||
)
|
||||
}
|
||||
if(json.title === null){
|
||||
information.push(context + ": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set.")
|
||||
if (json.title === null) {
|
||||
information.push(
|
||||
context +
|
||||
": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -534,20 +674,28 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
errors.push(context + ": This layer hasn't been expanded: " + json)
|
||||
return {
|
||||
result: null,
|
||||
errors
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
if(json.minzoom > Constants.userJourney.minZoomLevelToAddNewPoints ){
|
||||
(json.presets?.length > 0 ? errors : warnings).push(`At ${context}: minzoom is ${json.minzoom}, this should be at most ${Constants.userJourney.minZoomLevelToAddNewPoints} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates`)
|
||||
|
||||
if (json.minzoom > Constants.userJourney.minZoomLevelToAddNewPoints) {
|
||||
;(json.presets?.length > 0 ? errors : warnings).push(
|
||||
`At ${context}: minzoom is ${json.minzoom}, this should be at most ${Constants.userJourney.minZoomLevelToAddNewPoints} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates`
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
// duplicate ids in tagrenderings check
|
||||
const duplicates = Utils.Dedup(Utils.Dupiclates(Utils.NoNull((json.tagRenderings ?? []).map(tr => tr["id"]))))
|
||||
.filter(dupl => dupl !== "questions")
|
||||
const duplicates = Utils.Dedup(
|
||||
Utils.Dupiclates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"])))
|
||||
).filter((dupl) => dupl !== "questions")
|
||||
if (duplicates.length > 0) {
|
||||
errors.push("At " + context + ": some tagrenderings have a duplicate id: " + duplicates.join(", "))
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
": some tagrenderings have a duplicate id: " +
|
||||
duplicates.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -556,18 +704,46 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
// Some checks for legacy elements
|
||||
|
||||
if (json["overpassTags"] !== undefined) {
|
||||
errors.push("Layer " + json.id + "still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": <tags>}' instead of \"overpassTags\": <tags> (note: this isn't your fault, the custom theme generator still spits out the old format)")
|
||||
errors.push(
|
||||
"Layer " +
|
||||
json.id +
|
||||
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
|
||||
)
|
||||
}
|
||||
const forbiddenTopLevel = ["icon", "wayHandling", "roamingRenderings", "roamingRendering", "label", "width", "color", "colour", "iconOverlays"]
|
||||
const forbiddenTopLevel = [
|
||||
"icon",
|
||||
"wayHandling",
|
||||
"roamingRenderings",
|
||||
"roamingRendering",
|
||||
"label",
|
||||
"width",
|
||||
"color",
|
||||
"colour",
|
||||
"iconOverlays",
|
||||
]
|
||||
for (const forbiddenKey of forbiddenTopLevel) {
|
||||
if (json[forbiddenKey] !== undefined)
|
||||
errors.push(context + ": layer " + json.id + " still has a forbidden key " + forbiddenKey)
|
||||
errors.push(
|
||||
context +
|
||||
": layer " +
|
||||
json.id +
|
||||
" still has a forbidden key " +
|
||||
forbiddenKey
|
||||
)
|
||||
}
|
||||
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
|
||||
errors.push(context + ": layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'")
|
||||
errors.push(
|
||||
context +
|
||||
": layer " +
|
||||
json.id +
|
||||
" contains an old 'hideUnderlayingFeaturesMinPercentage'"
|
||||
)
|
||||
}
|
||||
|
||||
if(json.isShown !== undefined && (json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)){
|
||||
|
||||
if (
|
||||
json.isShown !== undefined &&
|
||||
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
|
||||
) {
|
||||
warnings.push(context + " has a tagRendering as `isShown`")
|
||||
}
|
||||
}
|
||||
|
@ -575,83 +751,109 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
// Check location of layer file
|
||||
const expected: string = `assets/layers/${json.id}/${json.id}.json`
|
||||
if (this._path != undefined && this._path.indexOf(expected) < 0) {
|
||||
errors.push("Layer is in an incorrect place. The path is " + this._path + ", but expected " + expected)
|
||||
errors.push(
|
||||
"Layer is in an incorrect place. The path is " +
|
||||
this._path +
|
||||
", but expected " +
|
||||
expected
|
||||
)
|
||||
}
|
||||
}
|
||||
if (this._isBuiltin) {
|
||||
// Check for correct IDs
|
||||
if (json.tagRenderings?.some(tr => tr["id"] === "")) {
|
||||
if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
|
||||
const emptyIndexes: number[] = []
|
||||
for (let i = 0; i < json.tagRenderings.length; i++) {
|
||||
const tagRendering = json.tagRenderings[i];
|
||||
const tagRendering = json.tagRenderings[i]
|
||||
if (tagRendering["id"] === "") {
|
||||
emptyIndexes.push(i)
|
||||
}
|
||||
}
|
||||
errors.push(`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${context}.tagRenderings.[${emptyIndexes.join(",")}])`)
|
||||
errors.push(
|
||||
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${context}.tagRenderings.[${emptyIndexes.join(
|
||||
","
|
||||
)}])`
|
||||
)
|
||||
}
|
||||
|
||||
const duplicateIds = Utils.Dupiclates((json.tagRenderings ?? [])?.map(f => f["id"]).filter(id => id !== "questions"))
|
||||
const duplicateIds = Utils.Dupiclates(
|
||||
(json.tagRenderings ?? [])
|
||||
?.map((f) => f["id"])
|
||||
.filter((id) => id !== "questions")
|
||||
)
|
||||
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
|
||||
errors.push(`Some tagRenderings have a duplicate id: ${duplicateIds} (at ${context}.tagRenderings)`)
|
||||
errors.push(
|
||||
`Some tagRenderings have a duplicate id: ${duplicateIds} (at ${context}.tagRenderings)`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (json.description === undefined) {
|
||||
|
||||
if (Constants.priviliged_layers.indexOf(json.id) >= 0) {
|
||||
errors.push(
|
||||
context + ": A priviliged layer must have a description"
|
||||
)
|
||||
errors.push(context + ": A priviliged layer must have a description")
|
||||
} else {
|
||||
warnings.push(
|
||||
context + ": A builtin layer should have a description"
|
||||
)
|
||||
warnings.push(context + ": A builtin layer should have a description")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (json.tagRenderings !== undefined) {
|
||||
const r = new On("tagRenderings", new Each(new ValidateTagRenderings(json, this._doesImageExist))).convert(json, context)
|
||||
const r = new On(
|
||||
"tagRenderings",
|
||||
new Each(new ValidateTagRenderings(json, this._doesImageExist))
|
||||
).convert(json, context)
|
||||
warnings.push(...(r.warnings ?? []))
|
||||
errors.push(...(r.errors ?? []))
|
||||
information.push(...(r.information ?? []))
|
||||
}
|
||||
|
||||
{
|
||||
const hasCondition = json.mapRendering?.filter(mr => mr["icon"] !== undefined && mr["icon"]["condition"] !== undefined)
|
||||
if(hasCondition?.length > 0){
|
||||
errors.push("At "+context+":\n One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n"+JSON.stringify(hasCondition, null, " "))
|
||||
const hasCondition = json.mapRendering?.filter(
|
||||
(mr) => mr["icon"] !== undefined && mr["icon"]["condition"] !== undefined
|
||||
)
|
||||
if (hasCondition?.length > 0) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
":\n One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n" +
|
||||
JSON.stringify(hasCondition, null, " ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (json.presets !== undefined) {
|
||||
|
||||
// Check that a preset will be picked up by the layer itself
|
||||
const baseTags = TagUtils.Tag(json.source.osmTags)
|
||||
for (let i = 0; i < json.presets.length; i++) {
|
||||
const preset = json.presets[i];
|
||||
const tags: { k: string, v: string }[] = new And(preset.tags.map(t => TagUtils.Tag(t))).asChange({id: "node/-1"})
|
||||
const preset = json.presets[i]
|
||||
const tags: { k: string; v: string }[] = new And(
|
||||
preset.tags.map((t) => TagUtils.Tag(t))
|
||||
).asChange({ id: "node/-1" })
|
||||
const properties = {}
|
||||
for (const tag of tags) {
|
||||
properties[tag.k] = tag.v
|
||||
}
|
||||
const doMatch = baseTags.matchesProperties(properties)
|
||||
if (!doMatch) {
|
||||
errors.push(context + ".presets[" + i + "]: This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " + JSON.stringify(properties) + "\n The required tags are: " + baseTags.asHumanString(false, false, {}))
|
||||
errors.push(
|
||||
context +
|
||||
".presets[" +
|
||||
i +
|
||||
"]: This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
|
||||
JSON.stringify(properties) +
|
||||
"\n The required tags are: " +
|
||||
baseTags.asHumanString(false, false, {})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
errors.push(e)
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
result: json,
|
||||
errors,
|
||||
warnings,
|
||||
information
|
||||
};
|
||||
information,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +1,43 @@
|
|||
import {Translation, TypedTranslation} from "../../UI/i18n/Translation";
|
||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import {DeleteConfigJson} from "./Json/DeleteConfigJson";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import { Translation, TypedTranslation } from "../../UI/i18n/Translation"
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||
import { DeleteConfigJson } from "./Json/DeleteConfigJson"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
|
||||
export default class DeleteConfig {
|
||||
public static readonly defaultDeleteReasons : {changesetMessage: string, explanation: Translation} [] = [
|
||||
public static readonly defaultDeleteReasons: {
|
||||
changesetMessage: string
|
||||
explanation: Translation
|
||||
}[] = [
|
||||
{
|
||||
changesetMessage: "testing point",
|
||||
explanation: Translations.t.delete.reasons.test
|
||||
explanation: Translations.t.delete.reasons.test,
|
||||
},
|
||||
{
|
||||
changesetMessage:"disused",
|
||||
explanation: Translations.t.delete.reasons.disused
|
||||
changesetMessage: "disused",
|
||||
explanation: Translations.t.delete.reasons.disused,
|
||||
},
|
||||
{
|
||||
changesetMessage: "not found",
|
||||
explanation: Translations.t.delete.reasons.notFound
|
||||
explanation: Translations.t.delete.reasons.notFound,
|
||||
},
|
||||
{
|
||||
changesetMessage: "duplicate",
|
||||
explanation:Translations.t.delete.reasons.duplicate
|
||||
}
|
||||
explanation: Translations.t.delete.reasons.duplicate,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
public readonly extraDeleteReasons?: {
|
||||
explanation: TypedTranslation<object>,
|
||||
explanation: TypedTranslation<object>
|
||||
changesetMessage: string
|
||||
}[]
|
||||
|
||||
public readonly nonDeleteMappings?: { if: TagsFilter, then: TypedTranslation<object> }[]
|
||||
public readonly nonDeleteMappings?: { if: TagsFilter; then: TypedTranslation<object> }[]
|
||||
|
||||
public readonly softDeletionTags?: TagsFilter
|
||||
public readonly neededChangesets?: number
|
||||
|
||||
constructor(json: DeleteConfigJson, context: string) {
|
||||
|
||||
this.extraDeleteReasons = (json.extraDeleteReasons ?? []).map((reason, i) => {
|
||||
const ctx = `${context}.extraDeleteReasons[${i}]`
|
||||
if ((reason.changesetMessage ?? "").length <= 5) {
|
||||
|
@ -44,21 +45,23 @@ export default class DeleteConfig {
|
|||
}
|
||||
return {
|
||||
explanation: Translations.T(reason.explanation, ctx + ".explanation"),
|
||||
changesetMessage: reason.changesetMessage
|
||||
changesetMessage: reason.changesetMessage,
|
||||
}
|
||||
})
|
||||
this.nonDeleteMappings = (json.nonDeleteMappings??[]).map((nonDelete, i) => {
|
||||
this.nonDeleteMappings = (json.nonDeleteMappings ?? []).map((nonDelete, i) => {
|
||||
const ctx = `${context}.extraDeleteReasons[${i}]`
|
||||
return {
|
||||
if: TagUtils.Tag(nonDelete.if, ctx + ".if"),
|
||||
then: Translations.T(nonDelete.then, ctx + ".then")
|
||||
then: Translations.T(nonDelete.then, ctx + ".then"),
|
||||
}
|
||||
})
|
||||
|
||||
this.softDeletionTags = undefined;
|
||||
this.softDeletionTags = undefined
|
||||
if (json.softDeletionTags !== undefined) {
|
||||
this.softDeletionTags = TagUtils.Tag(json.softDeletionTags, `${context}.softDeletionTags`)
|
||||
|
||||
this.softDeletionTags = TagUtils.Tag(
|
||||
json.softDeletionTags,
|
||||
`${context}.softDeletionTags`
|
||||
)
|
||||
}
|
||||
|
||||
if (json["hardDeletionTags"] !== undefined) {
|
||||
|
@ -66,6 +69,4 @@ export default class DeleteConfig {
|
|||
}
|
||||
this.neededChangesets = json.neededChangesets
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,50 @@
|
|||
import {SpecialVisualization} from "../../UI/SpecialVisualizations";
|
||||
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
|
||||
import TagRenderingConfig from "./TagRenderingConfig";
|
||||
import {ExtraFuncParams, ExtraFunctions} from "../../Logic/ExtraFunctions";
|
||||
import LayerConfig from "./LayerConfig";
|
||||
import { SpecialVisualization } from "../../UI/SpecialVisualizations"
|
||||
import { SubstitutedTranslation } from "../../UI/SubstitutedTranslation"
|
||||
import TagRenderingConfig from "./TagRenderingConfig"
|
||||
import { ExtraFuncParams, ExtraFunctions } from "../../Logic/ExtraFunctions"
|
||||
import LayerConfig from "./LayerConfig"
|
||||
|
||||
export default class DependencyCalculator {
|
||||
|
||||
public static GetTagRenderingDependencies(tr: TagRenderingConfig): string[] {
|
||||
|
||||
if (tr === undefined) {
|
||||
throw "Got undefined tag rendering in getTagRenderingDependencies"
|
||||
}
|
||||
const deps: string[] = []
|
||||
|
||||
// All translated snippets
|
||||
const parts: string[] = [].concat(...(tr.EnumerateTranslations().map(tr => tr.AllValues())))
|
||||
const parts: string[] = [].concat(...tr.EnumerateTranslations().map((tr) => tr.AllValues()))
|
||||
|
||||
for (const part of parts) {
|
||||
const specialVizs: { func: SpecialVisualization, args: string[] }[]
|
||||
= SubstitutedTranslation.ExtractSpecialComponents(part).map(o => o.special)
|
||||
.filter(o => o?.func?.getLayerDependencies !== undefined)
|
||||
const specialVizs: { func: SpecialVisualization; args: string[] }[] =
|
||||
SubstitutedTranslation.ExtractSpecialComponents(part)
|
||||
.map((o) => o.special)
|
||||
.filter((o) => o?.func?.getLayerDependencies !== undefined)
|
||||
for (const specialViz of specialVizs) {
|
||||
deps.push(...specialViz.func.getLayerDependencies(specialViz.args))
|
||||
}
|
||||
}
|
||||
return deps;
|
||||
return deps
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of all other layer-ids that this layer needs to function.
|
||||
* E.g. if this layers does snap to another layer in the preset, this other layer id will be mentioned
|
||||
*/
|
||||
public static getLayerDependencies(layer: LayerConfig): { neededLayer: string, reason: string, context?: string, neededBy: string }[] {
|
||||
const deps: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
|
||||
public static getLayerDependencies(
|
||||
layer: LayerConfig
|
||||
): { neededLayer: string; reason: string; context?: string; neededBy: string }[] {
|
||||
const deps: { neededLayer: string; reason: string; context?: string; neededBy: string }[] =
|
||||
[]
|
||||
|
||||
for (let i = 0; layer.presets !== undefined && i < layer.presets.length; i++) {
|
||||
const preset = layer.presets[i];
|
||||
preset.preciseInput?.snapToLayers?.forEach(id => {
|
||||
const preset = layer.presets[i]
|
||||
preset.preciseInput?.snapToLayers?.forEach((id) => {
|
||||
deps.push({
|
||||
neededLayer: id,
|
||||
reason: "a preset snaps to this layer",
|
||||
context: "presets[" + i + "]",
|
||||
neededBy: layer.id
|
||||
});
|
||||
neededBy: layer.id,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -52,7 +54,7 @@ export default class DependencyCalculator {
|
|||
neededLayer: dep,
|
||||
reason: "a tagrendering needs this layer",
|
||||
context: tr.id,
|
||||
neededBy: layer.id
|
||||
neededBy: layer.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -62,51 +64,53 @@ export default class DependencyCalculator {
|
|||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [0, 0]
|
||||
coordinates: [0, 0],
|
||||
},
|
||||
properties: {
|
||||
id: "node/1"
|
||||
}
|
||||
id: "node/1",
|
||||
},
|
||||
}
|
||||
let currentKey = undefined
|
||||
let currentLine = undefined
|
||||
const params: ExtraFuncParams = {
|
||||
getFeatureById: _ => undefined,
|
||||
getFeatureById: (_) => undefined,
|
||||
getFeaturesWithin: (layerId, _) => {
|
||||
|
||||
if (layerId === '*') {
|
||||
if (layerId === "*") {
|
||||
// This is a wildcard
|
||||
return []
|
||||
}
|
||||
|
||||
// The important line: steal the dependencies!
|
||||
deps.push({
|
||||
neededLayer: layerId, reason: "a calculated tag loads features from this layer",
|
||||
context: "calculatedTag[" + currentLine + "] which calculates the value for " + currentKey,
|
||||
neededBy: layer.id
|
||||
neededLayer: layerId,
|
||||
reason: "a calculated tag loads features from this layer",
|
||||
context:
|
||||
"calculatedTag[" +
|
||||
currentLine +
|
||||
"] which calculates the value for " +
|
||||
currentKey,
|
||||
neededBy: layer.id,
|
||||
})
|
||||
|
||||
return []
|
||||
},
|
||||
memberships: undefined
|
||||
memberships: undefined,
|
||||
}
|
||||
// Init the extra patched functions...
|
||||
ExtraFunctions.FullPatchFeature(params, obj)
|
||||
// ... Run the calculated tag code, which will trigger the getFeaturesWithin above...
|
||||
for (let i = 0; i < layer.calculatedTags.length; i++) {
|
||||
const [key, code] = layer.calculatedTags[i];
|
||||
currentLine = i; // Leak the state...
|
||||
currentKey = key;
|
||||
const [key, code] = layer.calculatedTags[i]
|
||||
currentLine = i // Leak the state...
|
||||
currentKey = key
|
||||
try {
|
||||
|
||||
const func = new Function("feat", "return " + code + ";");
|
||||
const func = new Function("feat", "return " + code + ";")
|
||||
const result = func(obj)
|
||||
obj.properties[key] = JSON.stringify(result);
|
||||
} catch (e) {
|
||||
}
|
||||
obj.properties[key] = JSON.stringify(result)
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
return deps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,43 @@
|
|||
import ExtraLinkConfigJson from "./Json/ExtraLinkConfigJson";
|
||||
import {Translation} from "../../UI/i18n/Translation";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import ExtraLinkConfigJson from "./Json/ExtraLinkConfigJson"
|
||||
import { Translation } from "../../UI/i18n/Translation"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
|
||||
export default class ExtraLinkConfig {
|
||||
public readonly icon?: string
|
||||
public readonly text?: Translation
|
||||
public readonly href: string
|
||||
public readonly newTab?: false | boolean
|
||||
public readonly requirements?: Set<("iframe" | "no-iframe" | "welcome-message" | "no-welcome-message")>
|
||||
public readonly requirements?: Set<
|
||||
"iframe" | "no-iframe" | "welcome-message" | "no-welcome-message"
|
||||
>
|
||||
|
||||
constructor(configJson: ExtraLinkConfigJson, context) {
|
||||
this.icon = configJson.icon
|
||||
this.text = Translations.T(configJson.text, "themes:"+context+".text")
|
||||
this.text = Translations.T(configJson.text, "themes:" + context + ".text")
|
||||
this.href = configJson.href
|
||||
this.newTab = configJson.newTab
|
||||
this.requirements = new Set(configJson.requirements)
|
||||
|
||||
for (let requirement of configJson.requirements) {
|
||||
|
||||
if (this.requirements.has(<any>("no-" + requirement))) {
|
||||
throw "Conflicting requirements found for " + context + ".extraLink: both '" + requirement + "' and 'no-" + requirement + "' found"
|
||||
throw (
|
||||
"Conflicting requirements found for " +
|
||||
context +
|
||||
".extraLink: both '" +
|
||||
requirement +
|
||||
"' and 'no-" +
|
||||
requirement +
|
||||
"' found"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.icon === undefined && this.text === undefined) {
|
||||
throw "At " + context + ".extraLink: define at least an icon or a text to show. Both are undefined, this is not allowed"
|
||||
throw (
|
||||
"At " +
|
||||
context +
|
||||
".extraLink: define at least an icon or a text to show. Both are undefined, this is not allowed"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import {Translation} from "../../UI/i18n/Translation";
|
||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import FilterConfigJson from "./Json/FilterConfigJson";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
|
||||
import {TagConfigJson} from "./Json/TagConfigJson";
|
||||
import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {FilterState} from "../FilteredLayer";
|
||||
import {QueryParameters} from "../../Logic/Web/QueryParameters";
|
||||
import {Utils} from "../../Utils";
|
||||
import {RegexTag} from "../../Logic/Tags/RegexTag";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import {InputElement} from "../../UI/Input/InputElement";
|
||||
import { Translation } from "../../UI/i18n/Translation"
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||
import FilterConfigJson from "./Json/FilterConfigJson"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import ValidatedTextField from "../../UI/Input/ValidatedTextField"
|
||||
import { TagConfigJson } from "./Json/TagConfigJson"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { FilterState } from "../FilteredLayer"
|
||||
import { QueryParameters } from "../../Logic/Web/QueryParameters"
|
||||
import { Utils } from "../../Utils"
|
||||
import { RegexTag } from "../../Logic/Tags/RegexTag"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import { InputElement } from "../../UI/Input/InputElement"
|
||||
|
||||
export default class FilterConfig {
|
||||
public readonly id: string
|
||||
public readonly options: {
|
||||
question: Translation;
|
||||
osmTags: TagsFilter | undefined;
|
||||
question: Translation
|
||||
osmTags: TagsFilter | undefined
|
||||
originalTagsSpec: TagConfigJson
|
||||
fields: { name: string, type: string }[]
|
||||
}[];
|
||||
public readonly defaultSelection? : number
|
||||
fields: { name: string; type: string }[]
|
||||
}[]
|
||||
public readonly defaultSelection?: number
|
||||
|
||||
constructor(json: FilterConfigJson, context: string) {
|
||||
if (json.options === undefined) {
|
||||
|
@ -37,99 +37,114 @@ export default class FilterConfig {
|
|||
if (json.options.map === undefined) {
|
||||
throw `A filter was given where the options aren't a list at ${context}`
|
||||
}
|
||||
this.id = json.id;
|
||||
let defaultSelection : number = undefined
|
||||
this.id = json.id
|
||||
let defaultSelection: number = undefined
|
||||
this.options = json.options.map((option, i) => {
|
||||
const ctx = `${context}.options.${i}`;
|
||||
const question = Translations.T(
|
||||
option.question,
|
||||
`${ctx}.question`
|
||||
);
|
||||
let osmTags: undefined | TagsFilter = undefined;
|
||||
const ctx = `${context}.options.${i}`
|
||||
const question = Translations.T(option.question, `${ctx}.question`)
|
||||
let osmTags: undefined | TagsFilter = undefined
|
||||
if ((option.fields?.length ?? 0) == 0 && option.osmTags !== undefined) {
|
||||
osmTags = TagUtils.Tag(
|
||||
option.osmTags,
|
||||
`${ctx}.osmTags`
|
||||
);
|
||||
osmTags = TagUtils.Tag(option.osmTags, `${ctx}.osmTags`)
|
||||
FilterConfig.validateSearch(osmTags, ctx)
|
||||
}
|
||||
if (question === undefined) {
|
||||
throw `Invalid filter: no question given at ${ctx}`
|
||||
}
|
||||
|
||||
const fields: { name: string, type: string }[] = ((option.fields) ?? []).map((f, i) => {
|
||||
const fields: { name: string; type: string }[] = (option.fields ?? []).map((f, i) => {
|
||||
const type = f.type ?? "string"
|
||||
if (!ValidatedTextField.ForType(type) === undefined) {
|
||||
throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from(ValidatedTextField.AvailableTypes()).join(",")}`
|
||||
throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from(
|
||||
ValidatedTextField.AvailableTypes()
|
||||
).join(",")}`
|
||||
}
|
||||
if (f.name === undefined || f.name === "" || f.name.match(/[a-z0-9_-]+/) == null) {
|
||||
throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]`
|
||||
}
|
||||
return {
|
||||
name: f.name,
|
||||
type
|
||||
type,
|
||||
}
|
||||
})
|
||||
|
||||
for (const field of fields) {
|
||||
question.OnEveryLanguage((txt, language) => {
|
||||
if(txt.indexOf("{"+field.name+"}")<0){
|
||||
throw "Error in filter with fields at "+context+".question."+language+": The question text should contain every field, but it doesn't contain `{"+field+"}`: "+txt
|
||||
if (txt.indexOf("{" + field.name + "}") < 0) {
|
||||
throw (
|
||||
"Error in filter with fields at " +
|
||||
context +
|
||||
".question." +
|
||||
language +
|
||||
": The question text should contain every field, but it doesn't contain `{" +
|
||||
field +
|
||||
"}`: " +
|
||||
txt
|
||||
)
|
||||
}
|
||||
return txt
|
||||
})
|
||||
}
|
||||
|
||||
if(option.default){
|
||||
if(defaultSelection === undefined){
|
||||
defaultSelection = i;
|
||||
}else{
|
||||
if (option.default) {
|
||||
if (defaultSelection === undefined) {
|
||||
defaultSelection = i
|
||||
} else {
|
||||
throw `Invalid filter: multiple filters are set as default, namely ${i} and ${defaultSelection} at ${context}`
|
||||
}
|
||||
}
|
||||
|
||||
if(option.osmTags !== undefined){
|
||||
|
||||
if (option.osmTags !== undefined) {
|
||||
FilterConfig.validateSearch(TagUtils.Tag(option.osmTags), ctx)
|
||||
}
|
||||
|
||||
return {question: question, osmTags: osmTags, fields, originalTagsSpec: option.osmTags};
|
||||
});
|
||||
|
||||
this.defaultSelection = defaultSelection
|
||||
return {
|
||||
question: question,
|
||||
osmTags: osmTags,
|
||||
fields,
|
||||
originalTagsSpec: option.osmTags,
|
||||
}
|
||||
})
|
||||
|
||||
if (this.options.some(o => o.fields.length > 0) && this.options.length > 1) {
|
||||
this.defaultSelection = defaultSelection
|
||||
|
||||
if (this.options.some((o) => o.fields.length > 0) && this.options.length > 1) {
|
||||
throw `Invalid filter at ${context}: a filter with textfields should only offer a single option.`
|
||||
}
|
||||
|
||||
if (this.options.length > 1 && this.options[0].osmTags !== undefined) {
|
||||
throw "Error in " + context + "." + this.id + ": the first option of a multi-filter should always be the 'reset' option and not have any filters"
|
||||
throw (
|
||||
"Error in " +
|
||||
context +
|
||||
"." +
|
||||
this.id +
|
||||
": the first option of a multi-filter should always be the 'reset' option and not have any filters"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static validateSearch(osmTags: TagsFilter, ctx: string){
|
||||
osmTags.visit(t => {
|
||||
private static validateSearch(osmTags: TagsFilter, ctx: string) {
|
||||
osmTags.visit((t) => {
|
||||
if (!(t instanceof RegexTag)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if(typeof t.value == "string"){
|
||||
return;
|
||||
}
|
||||
|
||||
if(t.value.source == '^..*$' || t.value.source == '^[\\s\\S][\\s\\S]*$' /*Compiled regex with 'm'*/){
|
||||
if (typeof t.value == "string") {
|
||||
return
|
||||
}
|
||||
|
||||
if(!t.value.ignoreCase) {
|
||||
throw `At ${ctx}: The filter for key '${t.key}' uses a regex '${t.value}', but you should use a case invariant regex with ~i~ instead, as search should be case insensitive`
|
||||
if (
|
||||
t.value.source == "^..*$" ||
|
||||
t.value.source == "^[\\s\\S][\\s\\S]*$" /*Compiled regex with 'm'*/
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!t.value.ignoreCase) {
|
||||
throw `At ${ctx}: The filter for key '${t.key}' uses a regex '${t.value}', but you should use a case invariant regex with ~i~ instead, as search should be case insensitive`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public initState(): UIEventSource<FilterState> {
|
||||
|
||||
public initState(): UIEventSource<FilterState> {
|
||||
function reset(state: FilterState): string {
|
||||
if (state === undefined) {
|
||||
return ""
|
||||
|
@ -138,47 +153,54 @@ export default class FilterConfig {
|
|||
}
|
||||
|
||||
let defaultValue = ""
|
||||
if(this.options.length > 1){
|
||||
defaultValue = ""+(this.defaultSelection ?? 0)
|
||||
}else{
|
||||
if (this.options.length > 1) {
|
||||
defaultValue = "" + (this.defaultSelection ?? 0)
|
||||
} else {
|
||||
// Only a single option
|
||||
if(this.defaultSelection === 0){
|
||||
if (this.defaultSelection === 0) {
|
||||
defaultValue = "true"
|
||||
}
|
||||
}
|
||||
const qp = QueryParameters.GetQueryParameter("filter-" + this.id, defaultValue, "State of filter " + this.id)
|
||||
const qp = QueryParameters.GetQueryParameter(
|
||||
"filter-" + this.id,
|
||||
defaultValue,
|
||||
"State of filter " + this.id
|
||||
)
|
||||
|
||||
if (this.options.length > 1) {
|
||||
// This is a multi-option filter; state should be a number which selects the correct entry
|
||||
const possibleStates: FilterState [] = this.options.map((opt, i) => ({
|
||||
const possibleStates: FilterState[] = this.options.map((opt, i) => ({
|
||||
currentFilter: opt.osmTags,
|
||||
state: i
|
||||
state: i,
|
||||
}))
|
||||
|
||||
// We map the query parameter for this case
|
||||
return qp.sync(str => {
|
||||
const parsed = Number(str)
|
||||
if (isNaN(parsed)) {
|
||||
// Nope, not a correct number!
|
||||
return undefined
|
||||
}
|
||||
return possibleStates[parsed]
|
||||
}, [], reset)
|
||||
return qp.sync(
|
||||
(str) => {
|
||||
const parsed = Number(str)
|
||||
if (isNaN(parsed)) {
|
||||
// Nope, not a correct number!
|
||||
return undefined
|
||||
}
|
||||
return possibleStates[parsed]
|
||||
},
|
||||
[],
|
||||
reset
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const option = this.options[0]
|
||||
|
||||
if (option.fields.length > 0) {
|
||||
return qp.sync(str => {
|
||||
// There are variables in play!
|
||||
// str should encode a json-hash
|
||||
try {
|
||||
const props = JSON.parse(str)
|
||||
return qp.sync(
|
||||
(str) => {
|
||||
// There are variables in play!
|
||||
// str should encode a json-hash
|
||||
try {
|
||||
const props = JSON.parse(str)
|
||||
|
||||
const origTags = option.originalTagsSpec
|
||||
const rewrittenTags = Utils.WalkJson(origTags,
|
||||
v => {
|
||||
const origTags = option.originalTagsSpec
|
||||
const rewrittenTags = Utils.WalkJson(origTags, (v) => {
|
||||
if (typeof v !== "string") {
|
||||
return v
|
||||
}
|
||||
|
@ -186,34 +208,36 @@ export default class FilterConfig {
|
|||
v = (<string>v).replace("{" + key + "}", props[key])
|
||||
}
|
||||
return v
|
||||
})
|
||||
const parsed = TagUtils.Tag(rewrittenTags)
|
||||
return <FilterState>{
|
||||
currentFilter: parsed,
|
||||
state: str,
|
||||
}
|
||||
)
|
||||
const parsed = TagUtils.Tag(rewrittenTags)
|
||||
return <FilterState>{
|
||||
currentFilter: parsed,
|
||||
state: str
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
}, [], reset)
|
||||
},
|
||||
[],
|
||||
reset
|
||||
)
|
||||
}
|
||||
|
||||
// The last case is pretty boring: it is checked or it isn't
|
||||
const filterState: FilterState = {
|
||||
currentFilter: option.osmTags,
|
||||
state: "true"
|
||||
state: "true",
|
||||
}
|
||||
return qp.sync(
|
||||
str => {
|
||||
(str) => {
|
||||
// Only a single option exists here
|
||||
if (str === "true") {
|
||||
return filterState
|
||||
}
|
||||
return undefined
|
||||
}, [],
|
||||
},
|
||||
[],
|
||||
reset
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {TagConfigJson} from "./TagConfigJson";
|
||||
import { TagConfigJson } from "./TagConfigJson"
|
||||
|
||||
export interface DeleteConfigJson {
|
||||
|
||||
/***
|
||||
* By default, three reasons to delete a point are shown:
|
||||
*
|
||||
|
@ -21,7 +20,7 @@ export interface DeleteConfigJson {
|
|||
/**
|
||||
* The text that will be shown to the user - translatable
|
||||
*/
|
||||
explanation: string | any,
|
||||
explanation: string | any
|
||||
/**
|
||||
* The text that will be uploaded into the changeset or will be used in the fixme in case of a soft deletion
|
||||
* Should be a few words, in english
|
||||
|
@ -41,12 +40,12 @@ export interface DeleteConfigJson {
|
|||
* The tags that will be given to the object.
|
||||
* This must remove tags so that the 'source/osmTags' won't match anymore
|
||||
*/
|
||||
if: TagConfigJson,
|
||||
if: TagConfigJson
|
||||
/**
|
||||
* The human explanation for the options
|
||||
*/
|
||||
then: string | any,
|
||||
}[],
|
||||
then: string | any
|
||||
}[]
|
||||
|
||||
/**
|
||||
* In some cases, the contributor is not allowed to delete the current feature (e.g. because it isn't a point, the point is referenced by a relation or the user isn't experienced enough).
|
||||
|
@ -67,11 +66,10 @@ export interface DeleteConfigJson {
|
|||
* }
|
||||
* ```
|
||||
*/
|
||||
softDeletionTags?: TagConfigJson,
|
||||
softDeletionTags?: TagConfigJson
|
||||
/***
|
||||
* By default, the contributor needs 20 previous changesets to delete points edited by others.
|
||||
* For some small features (e.g. bicycle racks) this is too much and this requirement can be lowered or dropped, which can be done here.
|
||||
*/
|
||||
neededChangesets?: number
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export default interface ExtraLinkConfigJson {
|
||||
icon?: string,
|
||||
text?: string | any,
|
||||
href: string,
|
||||
newTab?: false | boolean,
|
||||
icon?: string
|
||||
text?: string | any
|
||||
href: string
|
||||
newTab?: false | boolean
|
||||
requirements?: ("iframe" | "no-iframe" | "welcome-message" | "no-welcome-message")[]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {TagConfigJson} from "./TagConfigJson";
|
||||
import { TagConfigJson } from "./TagConfigJson"
|
||||
|
||||
export default interface FilterConfigJson {
|
||||
/**
|
||||
* An id/name for this filter, used to set the URL parameters
|
||||
*/
|
||||
id: string,
|
||||
id: string
|
||||
/**
|
||||
* The options for a filter
|
||||
* If there are multiple options these will be a list of radio buttons
|
||||
|
@ -12,15 +12,15 @@ export default interface FilterConfigJson {
|
|||
* Filtering is done based on the given osmTags that are compared to the objects in that layer.
|
||||
*/
|
||||
options: {
|
||||
question: string | any;
|
||||
osmTags?: TagConfigJson,
|
||||
default?: boolean,
|
||||
question: string | any
|
||||
osmTags?: TagConfigJson
|
||||
default?: boolean
|
||||
fields?: {
|
||||
/**
|
||||
* If name is `search`, use "_first_comment~.*{search}.*" as osmTags
|
||||
*/
|
||||
name: string,
|
||||
name: string
|
||||
type?: string | "string"
|
||||
}[]
|
||||
}[];
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import {TagConfigJson} from "./TagConfigJson";
|
||||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
import FilterConfigJson from "./FilterConfigJson";
|
||||
import {DeleteConfigJson} from "./DeleteConfigJson";
|
||||
import UnitConfigJson from "./UnitConfigJson";
|
||||
import MoveConfigJson from "./MoveConfigJson";
|
||||
import PointRenderingConfigJson from "./PointRenderingConfigJson";
|
||||
import LineRenderingConfigJson from "./LineRenderingConfigJson";
|
||||
import {QuestionableTagRenderingConfigJson} from "./QuestionableTagRenderingConfigJson";
|
||||
import RewritableConfigJson from "./RewritableConfigJson";
|
||||
import { TagConfigJson } from "./TagConfigJson"
|
||||
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
|
||||
import FilterConfigJson from "./FilterConfigJson"
|
||||
import { DeleteConfigJson } from "./DeleteConfigJson"
|
||||
import UnitConfigJson from "./UnitConfigJson"
|
||||
import MoveConfigJson from "./MoveConfigJson"
|
||||
import PointRenderingConfigJson from "./PointRenderingConfigJson"
|
||||
import LineRenderingConfigJson from "./LineRenderingConfigJson"
|
||||
import { QuestionableTagRenderingConfigJson } from "./QuestionableTagRenderingConfigJson"
|
||||
import RewritableConfigJson from "./RewritableConfigJson"
|
||||
|
||||
/**
|
||||
* Configuration for a single layer
|
||||
|
@ -17,7 +17,7 @@ export interface LayerConfigJson {
|
|||
* The id of this layer.
|
||||
* This should be a simple, lowercase, human readable string that is used to identify the layer.
|
||||
*/
|
||||
id: string;
|
||||
id: string
|
||||
|
||||
/**
|
||||
* The name of this layer
|
||||
|
@ -31,8 +31,7 @@ export interface LayerConfigJson {
|
|||
* A description for this layer.
|
||||
* Shown in the layer selections and in the personel theme
|
||||
*/
|
||||
description?: string | any;
|
||||
|
||||
description?: string | any
|
||||
|
||||
/**
|
||||
* This determines where the data for the layer is fetched: from OSM or from an external geojson dataset.
|
||||
|
@ -42,69 +41,69 @@ export interface LayerConfigJson {
|
|||
* Every source _must_ define which tags _must_ be present in order to be picked up.
|
||||
*
|
||||
*/
|
||||
source:
|
||||
({
|
||||
/**
|
||||
* Every source must set which tags have to be present in order to load the given layer.
|
||||
*/
|
||||
osmTags: TagConfigJson
|
||||
/**
|
||||
* The maximum amount of seconds that a tile is allowed to linger in the cache
|
||||
*/
|
||||
maxCacheAge?: number
|
||||
}) &
|
||||
({
|
||||
/**
|
||||
* If set, this custom overpass-script will be used instead of building one by using the OSM-tags.
|
||||
* Specifying OSM-tags is still obligatory and will still hide non-matching items and they will be used for the rest of the pipeline.
|
||||
* _This should be really rare_.
|
||||
*
|
||||
* For example, when you want to fetch all grass-areas in parks and which are marked as publicly accessible:
|
||||
* ```
|
||||
* "source": {
|
||||
* "overpassScript":
|
||||
* "way[\"leisure\"=\"park\"];node(w);is_in;area._[\"leisure\"=\"park\"];(way(area)[\"landuse\"=\"grass\"]; node(w); );",
|
||||
* "osmTags": "access=yes"
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
overpassScript?: string
|
||||
} |
|
||||
{
|
||||
/**
|
||||
* The actual source of the data to load, if loaded via geojson.
|
||||
*
|
||||
* # A single geojson-file
|
||||
* source: {geoJson: "https://my.source.net/some-geo-data.geojson"}
|
||||
* fetches a geojson from a third party source
|
||||
*
|
||||
* # A tiled geojson source
|
||||
* source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14}
|
||||
* to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted by the location; {layer} is substituted with the id of the loaded layer
|
||||
*
|
||||
* Some API's use a BBOX instead of a tile, this can be used by specifying {y_min}, {y_max}, {x_min} and {x_max}
|
||||
*/
|
||||
geoJson: string,
|
||||
/**
|
||||
* To load a tiled geojson layer, set the zoomlevel of the tiles
|
||||
*/
|
||||
geoJsonZoomLevel?: number,
|
||||
/**
|
||||
* Indicates that the upstream geojson data is OSM-derived.
|
||||
* Useful for e.g. merging or for scripts generating this cache
|
||||
*/
|
||||
isOsmCache?: boolean,
|
||||
/**
|
||||
* Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true` in the source for this
|
||||
*/
|
||||
mercatorCrs?: boolean,
|
||||
/**
|
||||
* Some API's have an id-field, but give it a different name.
|
||||
* Setting this key will rename this field into 'id'
|
||||
*/
|
||||
idKey?: string
|
||||
})
|
||||
source: {
|
||||
/**
|
||||
* Every source must set which tags have to be present in order to load the given layer.
|
||||
*/
|
||||
osmTags: TagConfigJson
|
||||
/**
|
||||
* The maximum amount of seconds that a tile is allowed to linger in the cache
|
||||
*/
|
||||
maxCacheAge?: number
|
||||
} & (
|
||||
| {
|
||||
/**
|
||||
* If set, this custom overpass-script will be used instead of building one by using the OSM-tags.
|
||||
* Specifying OSM-tags is still obligatory and will still hide non-matching items and they will be used for the rest of the pipeline.
|
||||
* _This should be really rare_.
|
||||
*
|
||||
* For example, when you want to fetch all grass-areas in parks and which are marked as publicly accessible:
|
||||
* ```
|
||||
* "source": {
|
||||
* "overpassScript":
|
||||
* "way[\"leisure\"=\"park\"];node(w);is_in;area._[\"leisure\"=\"park\"];(way(area)[\"landuse\"=\"grass\"]; node(w); );",
|
||||
* "osmTags": "access=yes"
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
overpassScript?: string
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The actual source of the data to load, if loaded via geojson.
|
||||
*
|
||||
* # A single geojson-file
|
||||
* source: {geoJson: "https://my.source.net/some-geo-data.geojson"}
|
||||
* fetches a geojson from a third party source
|
||||
*
|
||||
* # A tiled geojson source
|
||||
* source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14}
|
||||
* to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted by the location; {layer} is substituted with the id of the loaded layer
|
||||
*
|
||||
* Some API's use a BBOX instead of a tile, this can be used by specifying {y_min}, {y_max}, {x_min} and {x_max}
|
||||
*/
|
||||
geoJson: string
|
||||
/**
|
||||
* To load a tiled geojson layer, set the zoomlevel of the tiles
|
||||
*/
|
||||
geoJsonZoomLevel?: number
|
||||
/**
|
||||
* Indicates that the upstream geojson data is OSM-derived.
|
||||
* Useful for e.g. merging or for scripts generating this cache
|
||||
*/
|
||||
isOsmCache?: boolean
|
||||
/**
|
||||
* Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true` in the source for this
|
||||
*/
|
||||
mercatorCrs?: boolean
|
||||
/**
|
||||
* Some API's have an id-field, but give it a different name.
|
||||
* Setting this key will rename this field into 'id'
|
||||
*/
|
||||
idKey?: string
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -126,13 +125,13 @@ export interface LayerConfigJson {
|
|||
* ]
|
||||
*
|
||||
*/
|
||||
calculatedTags?: string[];
|
||||
calculatedTags?: string[]
|
||||
|
||||
/**
|
||||
* If set, this layer will not query overpass; but it'll still match the tags above which are by chance returned by other layers.
|
||||
* Works well together with 'passAllFeatures', to add decoration
|
||||
*/
|
||||
doNotDownload?: boolean;
|
||||
doNotDownload?: boolean
|
||||
|
||||
/**
|
||||
* If set, only features matching this extra tag will be shown.
|
||||
|
@ -143,7 +142,7 @@ export interface LayerConfigJson {
|
|||
*
|
||||
* The default value is 'yes'
|
||||
*/
|
||||
isShown?: TagConfigJson;
|
||||
isShown?: TagConfigJson
|
||||
|
||||
/**
|
||||
* Advanced option - might be set by the theme compiler
|
||||
|
@ -152,30 +151,28 @@ export interface LayerConfigJson {
|
|||
*/
|
||||
forceLoad?: false | boolean
|
||||
|
||||
|
||||
/**
|
||||
* The minimum needed zoomlevel required before loading of the data start
|
||||
* Default: 0
|
||||
*/
|
||||
minzoom?: number;
|
||||
|
||||
minzoom?: number
|
||||
|
||||
/**
|
||||
* Indicates if this layer is shown by default;
|
||||
* can be used to hide a layer from start, or to load the layer but only to show it where appropriate (e.g. for snapping to it)
|
||||
*/
|
||||
shownByDefault?: true | boolean;
|
||||
shownByDefault?: true | boolean
|
||||
|
||||
/**
|
||||
* The zoom level at which point the data is hidden again
|
||||
* Default: 100 (thus: always visible
|
||||
*/
|
||||
minzoomVisible?: number;
|
||||
minzoomVisible?: number
|
||||
|
||||
/**
|
||||
* The title shown in a popup for elements of this layer.
|
||||
*/
|
||||
title?: string | TagRenderingConfigJson;
|
||||
title?: string | TagRenderingConfigJson
|
||||
|
||||
/**
|
||||
* Small icons shown next to the title.
|
||||
|
@ -185,12 +182,23 @@ export interface LayerConfigJson {
|
|||
*
|
||||
* Type: icon[]
|
||||
*/
|
||||
titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"];
|
||||
titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"]
|
||||
|
||||
/**
|
||||
* Visualisation of the items on the map
|
||||
*/
|
||||
mapRendering: null | (PointRenderingConfigJson | LineRenderingConfigJson | RewritableConfigJson<LineRenderingConfigJson | PointRenderingConfigJson | LineRenderingConfigJson[] | PointRenderingConfigJson[]>)[]
|
||||
mapRendering:
|
||||
| null
|
||||
| (
|
||||
| PointRenderingConfigJson
|
||||
| LineRenderingConfigJson
|
||||
| RewritableConfigJson<
|
||||
| LineRenderingConfigJson
|
||||
| PointRenderingConfigJson
|
||||
| LineRenderingConfigJson[]
|
||||
| PointRenderingConfigJson[]
|
||||
>
|
||||
)[]
|
||||
|
||||
/**
|
||||
* If set, this layer will pass all the features it receives onto the next layer.
|
||||
|
@ -220,18 +228,18 @@ export interface LayerConfigJson {
|
|||
*
|
||||
* Do _not_ indicate 'new': 'add a new shop here' is incorrect, as the shop might have existed forever, it could just be unmapped!
|
||||
*/
|
||||
title: string | any,
|
||||
title: string | any
|
||||
/**
|
||||
* The tags to add. It determines the icon too
|
||||
*/
|
||||
tags: string[],
|
||||
tags: string[]
|
||||
/**
|
||||
* The _first sentence_ of the description is shown on the button of the `add` menu.
|
||||
* The full description is shown in the confirmation dialog.
|
||||
*
|
||||
* (The first sentence is until the first '.'-character in the description)
|
||||
*/
|
||||
description?: string | any,
|
||||
description?: string | any
|
||||
|
||||
/**
|
||||
* Example images, which show real-life pictures of what such a feature might look like
|
||||
|
@ -246,24 +254,32 @@ export interface LayerConfigJson {
|
|||
*
|
||||
* If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category.
|
||||
*/
|
||||
preciseInput?: true | {
|
||||
/**
|
||||
* The type of background picture
|
||||
*/
|
||||
preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string | string[],
|
||||
/**
|
||||
* If specified, these layers will be shown to and the new point will be snapped towards it
|
||||
*/
|
||||
snapToLayer?: string | string[],
|
||||
/**
|
||||
* If specified, a new point will only be snapped if it is within this range.
|
||||
* Distance in meter
|
||||
*
|
||||
* Default: 10
|
||||
*/
|
||||
maxSnapDistance?: number
|
||||
}
|
||||
}[],
|
||||
preciseInput?:
|
||||
| true
|
||||
| {
|
||||
/**
|
||||
* The type of background picture
|
||||
*/
|
||||
preferredBackground:
|
||||
| "osmbasedmap"
|
||||
| "photo"
|
||||
| "historicphoto"
|
||||
| "map"
|
||||
| string
|
||||
| string[]
|
||||
/**
|
||||
* If specified, these layers will be shown to and the new point will be snapped towards it
|
||||
*/
|
||||
snapToLayer?: string | string[]
|
||||
/**
|
||||
* If specified, a new point will only be snapped if it is within this range.
|
||||
* Distance in meter
|
||||
*
|
||||
* Default: 10
|
||||
*/
|
||||
maxSnapDistance?: number
|
||||
}
|
||||
}[]
|
||||
|
||||
/**
|
||||
* All the tag renderings.
|
||||
|
@ -285,19 +301,24 @@ export interface LayerConfigJson {
|
|||
* This is mainly create questions for a 'left' and a 'right' side of the road.
|
||||
* These will be grouped and questions will be asked together
|
||||
*/
|
||||
tagRenderings?:
|
||||
(string
|
||||
| { builtin: string | string[], override: Partial<QuestionableTagRenderingConfigJson> }
|
||||
| { id: string, builtin: string[], override: Partial<QuestionableTagRenderingConfigJson> }
|
||||
| QuestionableTagRenderingConfigJson
|
||||
| (RewritableConfigJson<(string | { builtin: string, override: Partial<QuestionableTagRenderingConfigJson> } | QuestionableTagRenderingConfigJson)[]> & {id: string})
|
||||
) [],
|
||||
|
||||
tagRenderings?: (
|
||||
| string
|
||||
| { builtin: string | string[]; override: Partial<QuestionableTagRenderingConfigJson> }
|
||||
| { id: string; builtin: string[]; override: Partial<QuestionableTagRenderingConfigJson> }
|
||||
| QuestionableTagRenderingConfigJson
|
||||
| (RewritableConfigJson<
|
||||
(
|
||||
| string
|
||||
| { builtin: string; override: Partial<QuestionableTagRenderingConfigJson> }
|
||||
| QuestionableTagRenderingConfigJson
|
||||
)[]
|
||||
> & { id: string })
|
||||
)[]
|
||||
|
||||
/**
|
||||
* All the extra questions for filtering
|
||||
*/
|
||||
filter?: (FilterConfigJson) [] | { sameAs: string },
|
||||
filter?: FilterConfigJson[] | { sameAs: string }
|
||||
|
||||
/**
|
||||
* This block defines under what circumstances the delete dialog is shown for objects of this layer.
|
||||
|
@ -435,4 +456,4 @@ export interface LayerConfigJson {
|
|||
* global: all layers with this ID will be synced accross all themes
|
||||
*/
|
||||
syncSelection?: "no" | "local" | "theme-only" | "global"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {LayerConfigJson} from "./LayerConfigJson";
|
||||
import TilesourceConfigJson from "./TilesourceConfigJson";
|
||||
import ExtraLinkConfigJson from "./ExtraLinkConfigJson";
|
||||
import { LayerConfigJson } from "./LayerConfigJson"
|
||||
import TilesourceConfigJson from "./TilesourceConfigJson"
|
||||
import ExtraLinkConfigJson from "./ExtraLinkConfigJson"
|
||||
|
||||
/**
|
||||
* Defines the entire theme.
|
||||
|
@ -15,7 +15,6 @@ import ExtraLinkConfigJson from "./ExtraLinkConfigJson";
|
|||
* General remark: a type (string | any) indicates either a fixed or a translatable string.
|
||||
*/
|
||||
export interface LayoutConfigJson {
|
||||
|
||||
/**
|
||||
* The id of this layout.
|
||||
*
|
||||
|
@ -25,16 +24,16 @@ export interface LayoutConfigJson {
|
|||
* On official themes, it'll become the name of the page, e.g.
|
||||
* 'cyclestreets' which become 'cyclestreets.html'
|
||||
*/
|
||||
id: string;
|
||||
id: string
|
||||
|
||||
/**
|
||||
* Who helped to create this theme and should be attributed?
|
||||
*/
|
||||
credits?: string;
|
||||
|
||||
credits?: string
|
||||
|
||||
/**
|
||||
* Only used in 'generateLayerOverview': if present, every translation will be checked to make sure it is fully translated.
|
||||
*
|
||||
*
|
||||
* This must be a list of two-letter, lowercase codes which identifies the language, e.g. "en", "nl", ...
|
||||
*/
|
||||
mustHaveLanguage?: string[]
|
||||
|
@ -42,49 +41,49 @@ export interface LayoutConfigJson {
|
|||
/**
|
||||
* The title, as shown in the welcome message and the more-screen.
|
||||
*/
|
||||
title: string | any;
|
||||
title: string | any
|
||||
|
||||
/**
|
||||
* A short description, showed as social description and in the 'more theme'-buttons.
|
||||
* Note that if this one is not defined, the first sentence of 'description' is used
|
||||
*/
|
||||
shortDescription?: string | any;
|
||||
shortDescription?: string | any
|
||||
|
||||
/**
|
||||
* The description, as shown in the welcome message and the more-screen
|
||||
*/
|
||||
description: string | any;
|
||||
description: string | any
|
||||
|
||||
/**
|
||||
* A part of the description, shown under the login-button.
|
||||
*/
|
||||
descriptionTail?: string | any;
|
||||
descriptionTail?: string | any
|
||||
|
||||
/**
|
||||
* The icon representing this theme.
|
||||
* Used as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...
|
||||
* Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)
|
||||
*
|
||||
*
|
||||
* Type: icon
|
||||
*/
|
||||
icon: string;
|
||||
icon: string
|
||||
|
||||
/**
|
||||
* Link to a 'social image' which is included as og:image-tag on official themes.
|
||||
* Useful to share the theme on social media.
|
||||
* See https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information$
|
||||
*
|
||||
*
|
||||
* Type: image
|
||||
*/
|
||||
socialImage?: string;
|
||||
socialImage?: string
|
||||
|
||||
/**
|
||||
* Default location and zoom to start.
|
||||
* Note that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used
|
||||
*/
|
||||
startZoom: number;
|
||||
startLat: number;
|
||||
startLon: number;
|
||||
startZoom: number
|
||||
startLat: number
|
||||
startLon: number
|
||||
|
||||
/**
|
||||
* When a query is run, the data within bounds of the visible map is loaded.
|
||||
|
@ -93,7 +92,7 @@ export interface LayoutConfigJson {
|
|||
*
|
||||
* IF widenfactor is 1, this feature is disabled. A recommended value is between 1 and 3
|
||||
*/
|
||||
widenFactor?: number;
|
||||
widenFactor?: number
|
||||
/**
|
||||
* At low zoom levels, overpass is used to query features.
|
||||
* At high zoom level, the OSM api is used to fetch one or more BBOX aligning with a slippy tile.
|
||||
|
@ -139,12 +138,12 @@ export interface LayoutConfigJson {
|
|||
*
|
||||
* In the above scenario, `sometagrendering` will be added at the beginning of the tagrenderings of every layer
|
||||
*/
|
||||
overrideAll?: Partial<any | LayerConfigJson>;
|
||||
overrideAll?: Partial<any | LayerConfigJson>
|
||||
|
||||
/**
|
||||
* The id of the default background. BY default: vanilla OSM
|
||||
*/
|
||||
defaultBackgroundId?: string;
|
||||
defaultBackgroundId?: string
|
||||
|
||||
/**
|
||||
* Define some (overlay) slippy map tilesources
|
||||
|
@ -174,7 +173,7 @@ export interface LayoutConfigJson {
|
|||
* ```
|
||||
* "layer": {
|
||||
* "builtin": "nature_reserve",
|
||||
* "override": {"source":
|
||||
* "override": {"source":
|
||||
* {"osmTags": {
|
||||
* "+and":["operator=Natuurpunt"]
|
||||
* }
|
||||
|
@ -192,122 +191,129 @@ export interface LayoutConfigJson {
|
|||
* }
|
||||
*```
|
||||
*/
|
||||
layers: (LayerConfigJson | string |
|
||||
{ builtin: string | string[],
|
||||
override: any,
|
||||
/**
|
||||
* TagRenderings with any of these labels will be removed from the layer.
|
||||
* Note that the 'id' and 'group' are considered labels too
|
||||
*/
|
||||
hideTagRenderingsWithLabels?: string[]})[],
|
||||
layers: (
|
||||
| LayerConfigJson
|
||||
| string
|
||||
| {
|
||||
builtin: string | string[]
|
||||
override: any
|
||||
/**
|
||||
* TagRenderings with any of these labels will be removed from the layer.
|
||||
* Note that the 'id' and 'group' are considered labels too
|
||||
*/
|
||||
hideTagRenderingsWithLabels?: string[]
|
||||
}
|
||||
)[]
|
||||
|
||||
/**
|
||||
* If defined, data will be clustered.
|
||||
* Defaults to {maxZoom: 16, minNeeded: 500}
|
||||
*/
|
||||
clustering?: {
|
||||
/**
|
||||
* All zoom levels above 'maxzoom' are not clustered anymore.
|
||||
* Defaults to 18
|
||||
*/
|
||||
maxZoom?: number,
|
||||
/**
|
||||
* The number of elements per tile needed to start clustering
|
||||
* If clustering is defined, defaults to 250
|
||||
*/
|
||||
minNeededElements?: number
|
||||
} | false,
|
||||
clustering?:
|
||||
| {
|
||||
/**
|
||||
* All zoom levels above 'maxzoom' are not clustered anymore.
|
||||
* Defaults to 18
|
||||
*/
|
||||
maxZoom?: number
|
||||
/**
|
||||
* The number of elements per tile needed to start clustering
|
||||
* If clustering is defined, defaults to 250
|
||||
*/
|
||||
minNeededElements?: number
|
||||
}
|
||||
| false
|
||||
|
||||
/**
|
||||
* The URL of a custom CSS stylesheet to modify the layout
|
||||
*/
|
||||
customCss?: string;
|
||||
customCss?: string
|
||||
/**
|
||||
* If set to true, this layout will not be shown in the overview with more themes
|
||||
*/
|
||||
hideFromOverview?: boolean;
|
||||
hideFromOverview?: boolean
|
||||
|
||||
/**
|
||||
* If set to true, the basemap will not scroll outside of the area visible on initial zoom.
|
||||
* If set to [[lon, lat], [lon, lat]], the map will not scroll outside of those bounds.
|
||||
* Off by default, which will enable panning to the entire world
|
||||
*/
|
||||
lockLocation?: [[number, number], [number, number]] | number[][];
|
||||
lockLocation?: [[number, number], [number, number]] | number[][]
|
||||
|
||||
/**
|
||||
* Adds an additional button on the top-left of the application.
|
||||
* This can link to an arbitrary location.
|
||||
*
|
||||
*
|
||||
* Note that {lat},{lon},{zoom}, {language} and {theme} will be replaced
|
||||
*
|
||||
* Default: {icon: "./assets/svg/pop-out.svg", href: 'https://mapcomplete.osm.be/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: ["iframe","no-welcome-message]},
|
||||
*
|
||||
*
|
||||
* Default: {icon: "./assets/svg/pop-out.svg", href: 'https://mapcomplete.osm.be/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: ["iframe","no-welcome-message]},
|
||||
*
|
||||
*/
|
||||
extraLink?: ExtraLinkConfigJson
|
||||
|
||||
|
||||
/**
|
||||
* If set to false, disables logging in.
|
||||
* The userbadge will be hidden, all login-buttons will be hidden and editing will be disabled
|
||||
*/
|
||||
enableUserBadge?: true | boolean;
|
||||
enableUserBadge?: true | boolean
|
||||
/**
|
||||
* If false, hides the tab 'share'-tab in the welcomeMessage
|
||||
*/
|
||||
enableShareScreen?: true | boolean;
|
||||
enableShareScreen?: true | boolean
|
||||
/**
|
||||
* Hides the tab with more themes in the welcomeMessage
|
||||
*/
|
||||
enableMoreQuests?: true | boolean;
|
||||
enableMoreQuests?: true | boolean
|
||||
/**
|
||||
* If false, the layer selection/filter view will be hidden
|
||||
* The corresponding URL-parameter is 'fs-filters' instead of 'fs-layers'
|
||||
*/
|
||||
enableLayers?: true | boolean;
|
||||
enableLayers?: true | boolean
|
||||
/**
|
||||
* If set to false, hides the search bar
|
||||
*/
|
||||
enableSearch?: true | boolean;
|
||||
enableSearch?: true | boolean
|
||||
/**
|
||||
* If set to false, the ability to add new points or nodes will be disabled.
|
||||
* Editing already existing features will still be possible
|
||||
*/
|
||||
enableAddNewPoints?: true | boolean;
|
||||
enableAddNewPoints?: true | boolean
|
||||
/**
|
||||
* If set to false, the 'geolocation'-button will be hidden.
|
||||
*/
|
||||
enableGeolocation?: true | boolean;
|
||||
enableGeolocation?: true | boolean
|
||||
/**
|
||||
* Enable switching the backgroundlayer.
|
||||
* If false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well
|
||||
* If false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well
|
||||
*/
|
||||
enableBackgroundLayerSelection?: true | boolean;
|
||||
enableBackgroundLayerSelection?: true | boolean
|
||||
/**
|
||||
* If set to true, will show _all_ unanswered questions in a popup instead of just the next one
|
||||
*/
|
||||
enableShowAllQuestions?: false | boolean;
|
||||
enableShowAllQuestions?: false | boolean
|
||||
/**
|
||||
* If set to true, download button for the data will be shown (offers downloading as geojson and csv)
|
||||
*/
|
||||
enableDownload?: false | boolean;
|
||||
enableDownload?: false | boolean
|
||||
/**
|
||||
* If set to true, exporting a pdf is enabled
|
||||
*/
|
||||
enablePdfDownload?: false | boolean;
|
||||
enablePdfDownload?: false | boolean
|
||||
|
||||
/**
|
||||
* If true, notes will be loaded and parsed. If a note is an import (as created by the import_helper.html-tool from mapcomplete),
|
||||
* these notes will be shown if a relevant layer is present.
|
||||
*
|
||||
*
|
||||
* Default is true for official layers and false for unofficial (sideloaded) layers
|
||||
*/
|
||||
enableNoteImports?: true | boolean;
|
||||
enableNoteImports?: true | boolean
|
||||
|
||||
/**
|
||||
* Set one or more overpass URLs to use for this theme..
|
||||
*/
|
||||
overpassUrl?: string | string[];
|
||||
overpassUrl?: string | string[]
|
||||
/**
|
||||
* Set a different timeout for overpass queries - in seconds. Default: 30s
|
||||
*/
|
||||
overpassTimeout?: number
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
|
||||
|
||||
/**
|
||||
* The LineRenderingConfig gives all details onto how to render a single line of a feature.
|
||||
|
@ -9,16 +9,15 @@ import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
|||
* - The feature is an area
|
||||
*/
|
||||
export default interface LineRenderingConfigJson {
|
||||
|
||||
/**
|
||||
* The color for way-elements and SVG-elements.
|
||||
* If the value starts with "--", the style of the body element will be queried for the corresponding variable instead
|
||||
*/
|
||||
color?: string | TagRenderingConfigJson;
|
||||
color?: string | TagRenderingConfigJson
|
||||
/**
|
||||
* The stroke-width for way-elements
|
||||
*/
|
||||
width?: string | number | TagRenderingConfigJson;
|
||||
width?: string | number | TagRenderingConfigJson
|
||||
|
||||
/**
|
||||
* A dasharray, e.g. "5 6"
|
||||
|
|
|
@ -9,4 +9,4 @@ export default interface MoveConfigJson {
|
|||
* Set to false to disable this reason
|
||||
*/
|
||||
enableRelocation?: true | boolean
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
import {TagConfigJson} from "./TagConfigJson";
|
||||
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
|
||||
import { TagConfigJson } from "./TagConfigJson"
|
||||
|
||||
/**
|
||||
* The PointRenderingConfig gives all details onto how to render a single point of a feature.
|
||||
|
@ -10,7 +10,6 @@ import {TagConfigJson} from "./TagConfigJson";
|
|||
* - To render something at the centroid of an area, or at the start, end or projected centroid of a way
|
||||
*/
|
||||
export default interface PointRenderingConfigJson {
|
||||
|
||||
/**
|
||||
* All the locations that this point should be rendered at.
|
||||
* Using `location: ["point", "centroid"] will always render centerpoint.
|
||||
|
@ -30,7 +29,7 @@ export default interface PointRenderingConfigJson {
|
|||
|
||||
* Type: icon
|
||||
*/
|
||||
icon?: string | TagRenderingConfigJson;
|
||||
icon?: string | TagRenderingConfigJson
|
||||
|
||||
/**
|
||||
* A list of extra badges to show next to the icon as small badge
|
||||
|
@ -38,26 +37,25 @@ export default interface PointRenderingConfigJson {
|
|||
*
|
||||
* Note: strings are interpreted as icons, so layering and substituting is supported. You can use `circle:white;./my_icon.svg` to add a background circle
|
||||
*/
|
||||
iconBadges?: {
|
||||
if: TagConfigJson,
|
||||
iconBadges?: {
|
||||
if: TagConfigJson
|
||||
/**
|
||||
* Badge to show
|
||||
* Type: icon
|
||||
*/
|
||||
then: string | TagRenderingConfigJson
|
||||
then: string | TagRenderingConfigJson
|
||||
}[]
|
||||
|
||||
|
||||
/**
|
||||
* A string containing "width,height" or "width,height,anchorpoint" where anchorpoint is any of 'center', 'top', 'bottom', 'left', 'right', 'bottomleft','topright', ...
|
||||
* Default is '40,40,center'
|
||||
*/
|
||||
iconSize?: string | TagRenderingConfigJson;
|
||||
iconSize?: string | TagRenderingConfigJson
|
||||
/**
|
||||
* The rotation of an icon, useful for e.g. directions.
|
||||
* Usage: as if it were a css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)``
|
||||
*/
|
||||
rotation?: string | TagRenderingConfigJson;
|
||||
rotation?: string | TagRenderingConfigJson
|
||||
/**
|
||||
* A HTML-fragment that is shown below the icon, for example:
|
||||
* <div style="background: white">{name}</div>
|
||||
|
@ -65,5 +63,5 @@ export default interface PointRenderingConfigJson {
|
|||
* If the icon is undefined, then the label is shown in the center of the feature.
|
||||
* Note that, if the wayhandling hides the icon then no label is shown as well.
|
||||
*/
|
||||
label?: string | TagRenderingConfigJson;
|
||||
}
|
||||
label?: string | TagRenderingConfigJson
|
||||
}
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
import {TagConfigJson} from "./TagConfigJson";
|
||||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
|
||||
import { TagConfigJson } from "./TagConfigJson"
|
||||
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
|
||||
|
||||
export interface MappingConfigJson {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
if: TagConfigJson,
|
||||
if: TagConfigJson
|
||||
/**
|
||||
* Shown if the 'if is fulfilled
|
||||
* Type: rendered
|
||||
*/
|
||||
then: string | any,
|
||||
then: string | any
|
||||
/**
|
||||
* An extra icon supporting the choice
|
||||
* Type: icon
|
||||
*/
|
||||
icon?: string | {
|
||||
/**
|
||||
* The path to the icon
|
||||
* Type: icon
|
||||
*/
|
||||
path: string,
|
||||
/**
|
||||
* Size of the image
|
||||
*/
|
||||
class: "small" | "medium" | "large" | string
|
||||
}
|
||||
icon?:
|
||||
| string
|
||||
| {
|
||||
/**
|
||||
* The path to the icon
|
||||
* Type: icon
|
||||
*/
|
||||
path: string
|
||||
/**
|
||||
* Size of the image
|
||||
*/
|
||||
class: "small" | "medium" | "large" | string
|
||||
}
|
||||
|
||||
/**
|
||||
* In some cases, multiple taggings exist (e.g. a default assumption, or a commonly mapped abbreviation and a fully written variation).
|
||||
|
@ -78,7 +78,7 @@ export interface MappingConfigJson {
|
|||
* {"if":"changing_table:location=female","then":"In the female restroom"},
|
||||
* {"if":"changing_table:location=male","then":"In the male restroom"},
|
||||
* {"if":"changing_table:location=wheelchair","then":"In the wheelchair accessible restroom", "hideInAnswer": "wheelchair=no"},
|
||||
*
|
||||
*
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
|
@ -89,7 +89,7 @@ export interface MappingConfigJson {
|
|||
* hideInAnswer: "_country!=be"
|
||||
* }
|
||||
*/
|
||||
hideInAnswer?: boolean | TagConfigJson,
|
||||
hideInAnswer?: boolean | TagConfigJson
|
||||
/**
|
||||
* Only applicable if 'multiAnswer' is set.
|
||||
* This is for situations such as:
|
||||
|
@ -103,7 +103,7 @@ export interface MappingConfigJson {
|
|||
/**
|
||||
* If chosen as answer, these tags will be applied as well onto the object.
|
||||
* Not compatible with multiAnswer.
|
||||
*
|
||||
*
|
||||
* This can be used e.g. to erase other keys which indicate the 'not' value:
|
||||
*```json
|
||||
* {
|
||||
|
@ -112,13 +112,13 @@ export interface MappingConfigJson {
|
|||
* "addExtraTags": "not:crossing:marking="
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*
|
||||
*/
|
||||
addExtraTags?: string[]
|
||||
|
||||
/**
|
||||
* If there are many options, the mappings-radiobuttons will be replaced by an element with a searchfunction
|
||||
*
|
||||
*
|
||||
* Searchterms (per language) allow to easily find an option if there are many options
|
||||
*/
|
||||
searchTerms?: Record<string, string[]>
|
||||
|
@ -128,7 +128,6 @@ export interface MappingConfigJson {
|
|||
* Use this sparingly
|
||||
*/
|
||||
priorityIf?: TagConfigJson
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,19 +135,16 @@ export interface MappingConfigJson {
|
|||
* If the desired tags are missing and a question is defined, a question will be shown instead.
|
||||
*/
|
||||
export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJson {
|
||||
|
||||
/**
|
||||
* If it turns out that this tagRendering doesn't match _any_ value, then we show this question.
|
||||
* If undefined, the question is never asked and this tagrendering is read-only
|
||||
*/
|
||||
question?: string | any,
|
||||
|
||||
question?: string | any
|
||||
|
||||
/**
|
||||
* Allow freeform text input from the user
|
||||
*/
|
||||
freeform?: {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
@ -158,7 +154,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
* The type of the text-field, e.g. 'string', 'nat', 'float', 'date',...
|
||||
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values
|
||||
*/
|
||||
type?: string,
|
||||
type?: string
|
||||
/**
|
||||
* A (translated) text that is shown (as gray text) within the textfield
|
||||
*/
|
||||
|
@ -168,12 +164,12 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
* Extra parameters to initialize the input helper arguments.
|
||||
* For semantics, see the 'SpecialInputElements.md'
|
||||
*/
|
||||
helperArgs?: (string | number | boolean | any)[];
|
||||
helperArgs?: (string | number | boolean | any)[]
|
||||
/**
|
||||
* If a value is added with the textfield, these extra tag is addded.
|
||||
* Useful to add a 'fixme=freeform textfield used - to be checked'
|
||||
**/
|
||||
addExtraTags?: string[];
|
||||
addExtraTags?: string[]
|
||||
|
||||
/**
|
||||
* When set, influences the way a question is asked.
|
||||
|
@ -188,15 +184,15 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
* Normally undefined (aka do not enter anything)
|
||||
*/
|
||||
default?: string
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, use checkboxes instead of radio buttons when asking the question
|
||||
*/
|
||||
multiAnswer?: boolean,
|
||||
multiAnswer?: boolean
|
||||
|
||||
/**
|
||||
* Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
|
||||
*/
|
||||
mappings?: MappingConfigJson[]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
* Rewrites and multiplies the given renderings of type T.
|
||||
*
|
||||
*
|
||||
* This can be used for introducing many similar questions automatically,
|
||||
* which also makes translations easier.
|
||||
*
|
||||
* (Note that the key does _not_ need to be wrapped in {}.
|
||||
*
|
||||
* (Note that the key does _not_ need to be wrapped in {}.
|
||||
* However, we recommend to use them if the key is used in a translation, as missing keys will be picked up and warned for by the translation scripts)
|
||||
*
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* ```
|
||||
|
@ -25,7 +25,7 @@
|
|||
* }
|
||||
* ```
|
||||
* will result in _three_ copies (as the values to rewrite into have three values, namely:
|
||||
*
|
||||
*
|
||||
* [
|
||||
* {
|
||||
* # The first pair: key --> X, a|b|c --> 0
|
||||
|
@ -37,15 +37,15 @@
|
|||
* {
|
||||
* "Z": 2
|
||||
* }
|
||||
*
|
||||
*
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* @see ExpandRewrite
|
||||
*/
|
||||
export default interface RewritableConfigJson<T> {
|
||||
rewrite: {
|
||||
sourceString: string[],
|
||||
sourceString: string[]
|
||||
into: (string | any)[][]
|
||||
},
|
||||
}
|
||||
renderings: T
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
*/
|
||||
export type TagConfigJson = string | AndTagConfigJson | OrTagConfigJson
|
||||
|
||||
|
||||
/**
|
||||
* Chain many tags, to match, all of these should be true
|
||||
* See https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for documentation
|
||||
|
@ -14,7 +13,7 @@ export type OrTagConfigJson = {
|
|||
}
|
||||
/**
|
||||
* Chain many tags, to match, a single of these should be true
|
||||
* See https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for documentation
|
||||
* See https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for documentation
|
||||
*/
|
||||
export type AndTagConfigJson = {
|
||||
and: TagConfigJson[]
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import {TagConfigJson} from "./TagConfigJson";
|
||||
import { TagConfigJson } from "./TagConfigJson"
|
||||
|
||||
/**
|
||||
* A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.
|
||||
* For an _editable_ tagRendering, use 'QuestionableTagRenderingConfigJson' instead, which extends this one
|
||||
*/
|
||||
export interface TagRenderingConfigJson {
|
||||
|
||||
/**
|
||||
* The id of the tagrendering, should be an unique string.
|
||||
* Used to keep the translations in sync. Only used in the tagRenderings-array of a layerConfig, not requered otherwise.
|
||||
*
|
||||
* Use 'questions' to trigger the question box of this group (if a group is defined)
|
||||
*/
|
||||
id?: string,
|
||||
id?: string
|
||||
|
||||
/**
|
||||
* If 'group' is defined on many tagRenderings, these are grouped together when shown. The questions are grouped together as well.
|
||||
|
@ -37,15 +36,15 @@ export interface TagRenderingConfigJson {
|
|||
* Note that this is a HTML-interpreted value, so you can add links as e.g. '<a href='{website}'>{website}</a>' or include images such as `This is of type A <br><img src='typeA-icon.svg' />`
|
||||
* type: rendered
|
||||
*/
|
||||
render?: string | any,
|
||||
|
||||
render?: string | any
|
||||
|
||||
/**
|
||||
* Only show this tagrendering (or ask the question) if the selected object also matches the tags specified as `condition`.
|
||||
*
|
||||
* This is useful to ask a follow-up question.
|
||||
* For example, within toilets, asking _where_ the diaper changing table is is only useful _if_ there is one.
|
||||
* This can be done by adding `"condition": "changing_table=yes"`
|
||||
*
|
||||
*
|
||||
* A full example would be:
|
||||
* ```json
|
||||
* {
|
||||
|
@ -78,25 +77,23 @@ export interface TagRenderingConfigJson {
|
|||
* },
|
||||
* ```
|
||||
* */
|
||||
condition?: TagConfigJson;
|
||||
condition?: TagConfigJson
|
||||
|
||||
/**
|
||||
* Allow freeform text input from the user
|
||||
*/
|
||||
freeform?: {
|
||||
|
||||
/**
|
||||
* If this key is present, then 'render' is used to display the value.
|
||||
* If this is undefined, the rendering is _always_ shown
|
||||
*/
|
||||
key: string,
|
||||
},
|
||||
key: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
|
||||
*/
|
||||
mappings?: {
|
||||
|
||||
/**
|
||||
* If this condition is met, then the text under `then` will be shown.
|
||||
* If no value matches, and the user selects this mapping as an option, then these tags will be uploaded to OSM.
|
||||
|
@ -105,29 +102,30 @@ export interface TagRenderingConfigJson {
|
|||
*
|
||||
* This can be an substituting-tag as well, e.g. {'if': 'addr:street:={_calculated_nearby_streetname}', 'then': '{_calculated_nearby_streetname}'}
|
||||
*/
|
||||
if: TagConfigJson,
|
||||
if: TagConfigJson
|
||||
/**
|
||||
* If the condition `if` is met, the text `then` will be rendered.
|
||||
* If not known yet, the user will be presented with `then` as an option
|
||||
* Type: rendered
|
||||
*/
|
||||
then: string | any,
|
||||
then: string | any
|
||||
/**
|
||||
* An icon supporting this mapping; typically shown pretty small
|
||||
* Type: icon
|
||||
*/
|
||||
icon?: string | {
|
||||
/**
|
||||
* The path to the icon
|
||||
* Type: icon
|
||||
*/
|
||||
path: string,
|
||||
/**
|
||||
* A hint to mapcomplete on how to render this icon within the mapping.
|
||||
* This is translated to 'mapping-icon-<classtype>', so defining your own in combination with a custom CSS is possible (but discouraged)
|
||||
*/
|
||||
class: "small" | "medium" | "large" | string
|
||||
}
|
||||
|
||||
icon?:
|
||||
| string
|
||||
| {
|
||||
/**
|
||||
* The path to the icon
|
||||
* Type: icon
|
||||
*/
|
||||
path: string
|
||||
/**
|
||||
* A hint to mapcomplete on how to render this icon within the mapping.
|
||||
* This is translated to 'mapping-icon-<classtype>', so defining your own in combination with a custom CSS is possible (but discouraged)
|
||||
*/
|
||||
class: "small" | "medium" | "large" | string
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
|
|
@ -2,19 +2,18 @@
|
|||
* Configuration for a tilesource config
|
||||
*/
|
||||
export default interface TilesourceConfigJson {
|
||||
|
||||
/**
|
||||
* Id of this overlay, used in the URL-parameters to set the state
|
||||
*/
|
||||
id: string,
|
||||
id: string
|
||||
/**
|
||||
* The path, where {x}, {y} and {z} will be substituted
|
||||
*/
|
||||
source: string,
|
||||
source: string
|
||||
/**
|
||||
* Wether or not this is an overlay. Default: true
|
||||
*/
|
||||
isOverlay?: boolean,
|
||||
isOverlay?: boolean
|
||||
|
||||
/**
|
||||
* How this will be shown in the selection menu.
|
||||
|
@ -32,10 +31,8 @@ export default interface TilesourceConfigJson {
|
|||
*/
|
||||
maxZoom?: number
|
||||
|
||||
|
||||
/**
|
||||
* The default state, set to false to hide by default
|
||||
*/
|
||||
defaultState: boolean;
|
||||
|
||||
}
|
||||
defaultState: boolean
|
||||
}
|
||||
|
|
|
@ -1,33 +1,29 @@
|
|||
export default interface UnitConfigJson {
|
||||
|
||||
/**
|
||||
* Every key from this list will be normalized.
|
||||
*
|
||||
* To render a united value properly, use
|
||||
*
|
||||
* To render a united value properly, use
|
||||
*/
|
||||
appliesToKey: string[],
|
||||
appliesToKey: string[]
|
||||
/**
|
||||
* If set, invalid values will be erased in the MC application (but not in OSM of course!)
|
||||
* Be careful with setting this
|
||||
*/
|
||||
eraseInvalidValues?: boolean;
|
||||
eraseInvalidValues?: boolean
|
||||
/**
|
||||
* The possible denominations
|
||||
*/
|
||||
applicableUnits: DenominationConfigJson[]
|
||||
|
||||
}
|
||||
|
||||
export interface DenominationConfigJson {
|
||||
|
||||
|
||||
/**
|
||||
* If this evaluates to true and the value to interpret has _no_ unit given, assumes that this unit is meant.
|
||||
* Alternatively, a list of country codes can be given where this acts as the default interpretation
|
||||
*
|
||||
*
|
||||
* E.g., a denomination using "meter" would probably set this flag to "true";
|
||||
* a denomination for "mp/h" will use the condition "_country=gb" to indicate that it is the default in the UK.
|
||||
*
|
||||
*
|
||||
* If none of the units indicate that they are the default, the first denomination will be used instead
|
||||
*/
|
||||
useIfNoUnitGiven?: boolean | string[]
|
||||
|
@ -42,24 +38,22 @@ export interface DenominationConfigJson {
|
|||
* The canonical value for this denomination which will be added to the value in OSM.
|
||||
* e.g. "m" for meters
|
||||
* If the user inputs '42', the canonical value will be added and it'll become '42m'.
|
||||
*
|
||||
*
|
||||
* Important: often, _no_ canonical values are expected, e.g. in the case of 'maxspeed' where 'km/h' is the default.
|
||||
* In this case, an empty string should be used
|
||||
*/
|
||||
canonicalDenomination: string,
|
||||
canonicalDenomination: string
|
||||
|
||||
|
||||
/**
|
||||
* The canonical denomination in the case that the unit is precisely '1'.
|
||||
* Used for display purposes
|
||||
*/
|
||||
canonicalDenominationSingular?: string,
|
||||
|
||||
canonicalDenominationSingular?: string
|
||||
|
||||
/**
|
||||
* A list of alternative values which can occur in the OSM database - used for parsing.
|
||||
*/
|
||||
alternativeDenomination?: string[],
|
||||
alternativeDenomination?: string[]
|
||||
|
||||
/**
|
||||
* The value for humans in the dropdown. This should not use abbreviations and should be translated, e.g.
|
||||
|
@ -84,6 +78,4 @@ export interface DenominationConfigJson {
|
|||
* Note that if all values use 'prefix', the dropdown might move to before the text field
|
||||
*/
|
||||
prefix?: boolean
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,86 +1,80 @@
|
|||
import {Translation} from "../../UI/i18n/Translation";
|
||||
import SourceConfig from "./SourceConfig";
|
||||
import TagRenderingConfig from "./TagRenderingConfig";
|
||||
import PresetConfig, {PreciseInput} from "./PresetConfig";
|
||||
import {LayerConfigJson} from "./Json/LayerConfigJson";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import FilterConfig from "./FilterConfig";
|
||||
import {Unit} from "../Unit";
|
||||
import DeleteConfig from "./DeleteConfig";
|
||||
import MoveConfig from "./MoveConfig";
|
||||
import PointRenderingConfig from "./PointRenderingConfig";
|
||||
import WithContextLoader from "./WithContextLoader";
|
||||
import LineRenderingConfig from "./LineRenderingConfig";
|
||||
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
|
||||
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";
|
||||
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import Combine from "../../UI/Base/Combine";
|
||||
import Title from "../../UI/Base/Title";
|
||||
import List from "../../UI/Base/List";
|
||||
import Link from "../../UI/Base/Link";
|
||||
import {Utils} from "../../Utils";
|
||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import Table from "../../UI/Base/Table";
|
||||
import FilterConfigJson from "./Json/FilterConfigJson";
|
||||
import {And} from "../../Logic/Tags/And";
|
||||
import {Overpass} from "../../Logic/Osm/Overpass";
|
||||
import Constants from "../Constants";
|
||||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||
import Svg from "../../Svg";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {OsmTags} from "../OsmFeature";
|
||||
import { Translation } from "../../UI/i18n/Translation"
|
||||
import SourceConfig from "./SourceConfig"
|
||||
import TagRenderingConfig from "./TagRenderingConfig"
|
||||
import PresetConfig, { PreciseInput } from "./PresetConfig"
|
||||
import { LayerConfigJson } from "./Json/LayerConfigJson"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import FilterConfig from "./FilterConfig"
|
||||
import { Unit } from "../Unit"
|
||||
import DeleteConfig from "./DeleteConfig"
|
||||
import MoveConfig from "./MoveConfig"
|
||||
import PointRenderingConfig from "./PointRenderingConfig"
|
||||
import WithContextLoader from "./WithContextLoader"
|
||||
import LineRenderingConfig from "./LineRenderingConfig"
|
||||
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"
|
||||
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson"
|
||||
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import Combine from "../../UI/Base/Combine"
|
||||
import Title from "../../UI/Base/Title"
|
||||
import List from "../../UI/Base/List"
|
||||
import Link from "../../UI/Base/Link"
|
||||
import { Utils } from "../../Utils"
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||
import Table from "../../UI/Base/Table"
|
||||
import FilterConfigJson from "./Json/FilterConfigJson"
|
||||
import { And } from "../../Logic/Tags/And"
|
||||
import { Overpass } from "../../Logic/Osm/Overpass"
|
||||
import Constants from "../Constants"
|
||||
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
|
||||
import Svg from "../../Svg"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { OsmTags } from "../OsmFeature"
|
||||
|
||||
export default class LayerConfig extends WithContextLoader {
|
||||
|
||||
public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const;
|
||||
public readonly id: string;
|
||||
public readonly name: Translation;
|
||||
public readonly description: Translation;
|
||||
public readonly source: SourceConfig;
|
||||
public readonly calculatedTags: [string, string, boolean][];
|
||||
public readonly doNotDownload: boolean;
|
||||
public readonly passAllFeatures: boolean;
|
||||
public readonly isShown: TagsFilter;
|
||||
public minzoom: number;
|
||||
public minzoomVisible: number;
|
||||
public readonly maxzoom: number;
|
||||
public readonly title?: TagRenderingConfig;
|
||||
public readonly titleIcons: TagRenderingConfig[];
|
||||
public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const
|
||||
public readonly id: string
|
||||
public readonly name: Translation
|
||||
public readonly description: Translation
|
||||
public readonly source: SourceConfig
|
||||
public readonly calculatedTags: [string, string, boolean][]
|
||||
public readonly doNotDownload: boolean
|
||||
public readonly passAllFeatures: boolean
|
||||
public readonly isShown: TagsFilter
|
||||
public minzoom: number
|
||||
public minzoomVisible: number
|
||||
public readonly maxzoom: number
|
||||
public readonly title?: TagRenderingConfig
|
||||
public readonly titleIcons: TagRenderingConfig[]
|
||||
public readonly mapRendering: PointRenderingConfig[]
|
||||
public readonly lineRendering: LineRenderingConfig[]
|
||||
public readonly units: Unit[];
|
||||
public readonly deletion: DeleteConfig | null;
|
||||
public readonly units: Unit[]
|
||||
public readonly deletion: DeleteConfig | null
|
||||
public readonly allowMove: MoveConfig | null
|
||||
public readonly allowSplit: boolean
|
||||
public readonly shownByDefault: boolean;
|
||||
public readonly shownByDefault: boolean
|
||||
/**
|
||||
* In seconds
|
||||
*/
|
||||
public readonly maxAgeOfCache: number
|
||||
public readonly presets: PresetConfig[];
|
||||
public readonly tagRenderings: TagRenderingConfig[];
|
||||
public readonly filters: FilterConfig[];
|
||||
public readonly filterIsSameAs: string;
|
||||
public readonly forceLoad: boolean;
|
||||
public readonly syncSelection: (typeof LayerConfig.syncSelectionAllowed)[number] // this is a trick to conver a constant array of strings into a type union of these values
|
||||
public readonly presets: PresetConfig[]
|
||||
public readonly tagRenderings: TagRenderingConfig[]
|
||||
public readonly filters: FilterConfig[]
|
||||
public readonly filterIsSameAs: string
|
||||
public readonly forceLoad: boolean
|
||||
public readonly syncSelection: typeof LayerConfig.syncSelectionAllowed[number] // this is a trick to conver a constant array of strings into a type union of these values
|
||||
|
||||
constructor(
|
||||
json: LayerConfigJson,
|
||||
context?: string,
|
||||
official: boolean = true
|
||||
) {
|
||||
context = context + "." + json.id;
|
||||
constructor(json: LayerConfigJson, context?: string, official: boolean = true) {
|
||||
context = context + "." + json.id
|
||||
const translationContext = "layers:" + json.id
|
||||
super(json, context)
|
||||
this.id = json.id;
|
||||
this.id = json.id
|
||||
|
||||
if (typeof json === "string") {
|
||||
throw `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed (at ${context})`
|
||||
}
|
||||
|
||||
|
||||
if (json.id === undefined) {
|
||||
throw `Not a valid layer: id is undefined: ${JSON.stringify(json)} (At ${context})`
|
||||
}
|
||||
|
@ -89,9 +83,14 @@ export default class LayerConfig extends WithContextLoader {
|
|||
throw "Layer " + this.id + " does not define a source section (" + context + ")"
|
||||
}
|
||||
|
||||
|
||||
if (json.source.osmTags === undefined) {
|
||||
throw "Layer " + this.id + " does not define a osmTags in the source section - these should always be present, even for geojson layers (" + context + ")"
|
||||
throw (
|
||||
"Layer " +
|
||||
this.id +
|
||||
" does not define a osmTags in the source section - these should always be present, even for geojson layers (" +
|
||||
context +
|
||||
")"
|
||||
)
|
||||
}
|
||||
|
||||
if (json.id.toLowerCase() !== json.id) {
|
||||
|
@ -102,28 +101,38 @@ export default class LayerConfig extends WithContextLoader {
|
|||
}
|
||||
|
||||
this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30
|
||||
if (json.syncSelection !== undefined && LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0) {
|
||||
throw context + " Invalid sync-selection: must be one of " + LayerConfig.syncSelectionAllowed.map(v => `'${v}'`).join(", ") + " but got '" + json.syncSelection + "'"
|
||||
if (
|
||||
json.syncSelection !== undefined &&
|
||||
LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
|
||||
) {
|
||||
throw (
|
||||
context +
|
||||
" Invalid sync-selection: must be one of " +
|
||||
LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
|
||||
" but got '" +
|
||||
json.syncSelection +
|
||||
"'"
|
||||
)
|
||||
}
|
||||
this.syncSelection = json.syncSelection ?? "no";
|
||||
const osmTags = TagUtils.Tag(
|
||||
json.source.osmTags,
|
||||
context + "source.osmTags"
|
||||
);
|
||||
this.syncSelection = json.syncSelection ?? "no"
|
||||
const osmTags = TagUtils.Tag(json.source.osmTags, context + "source.osmTags")
|
||||
|
||||
if (Constants.priviliged_layers.indexOf(this.id) < 0 && osmTags.isNegative()) {
|
||||
throw context + "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" + osmTags.asHumanString(false, false, {});
|
||||
throw (
|
||||
context +
|
||||
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
|
||||
osmTags.asHumanString(false, false, {})
|
||||
)
|
||||
}
|
||||
|
||||
if (json.source["geoJsonSource"] !== undefined) {
|
||||
throw context + "Use 'geoJson' instead of 'geoJsonSource'";
|
||||
throw context + "Use 'geoJson' instead of 'geoJsonSource'"
|
||||
}
|
||||
|
||||
if (json.source["geojson"] !== undefined) {
|
||||
throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)";
|
||||
throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)"
|
||||
}
|
||||
|
||||
|
||||
this.source = new SourceConfig(
|
||||
{
|
||||
osmTags: osmTags,
|
||||
|
@ -132,74 +141,80 @@ export default class LayerConfig extends WithContextLoader {
|
|||
overpassScript: json.source["overpassScript"],
|
||||
isOsmCache: json.source["isOsmCache"],
|
||||
mercatorCrs: json.source["mercatorCrs"],
|
||||
idKey: json.source["idKey"]
|
||||
|
||||
idKey: json.source["idKey"],
|
||||
},
|
||||
Constants.priviliged_layers.indexOf(this.id) > 0,
|
||||
json.id
|
||||
);
|
||||
)
|
||||
|
||||
|
||||
this.allowSplit = json.allowSplit ?? false;
|
||||
this.name = Translations.T(json.name, translationContext + ".name");
|
||||
this.allowSplit = json.allowSplit ?? false
|
||||
this.name = Translations.T(json.name, translationContext + ".name")
|
||||
if (json.units !== undefined && !Array.isArray(json.units)) {
|
||||
throw "At " + context + ".units: the 'units'-section should be a list; you probably have an object there"
|
||||
throw (
|
||||
"At " +
|
||||
context +
|
||||
".units: the 'units'-section should be a list; you probably have an object there"
|
||||
)
|
||||
}
|
||||
this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`)))
|
||||
this.units = (json.units ?? []).map((unitJson, i) =>
|
||||
Unit.fromJson(unitJson, `${context}.unit[${i}]`)
|
||||
)
|
||||
|
||||
if (json.description !== undefined) {
|
||||
if (Object.keys(json.description).length === 0) {
|
||||
json.description = undefined;
|
||||
json.description = undefined
|
||||
}
|
||||
}
|
||||
|
||||
this.description = Translations.T(
|
||||
json.description,
|
||||
translationContext + ".description"
|
||||
);
|
||||
this.description = Translations.T(json.description, translationContext + ".description")
|
||||
|
||||
|
||||
this.calculatedTags = undefined;
|
||||
this.calculatedTags = undefined
|
||||
if (json.calculatedTags !== undefined) {
|
||||
if (!official) {
|
||||
console.warn(
|
||||
`Unofficial theme ${this.id} with custom javascript! This is a security risk`
|
||||
);
|
||||
)
|
||||
}
|
||||
this.calculatedTags = [];
|
||||
this.calculatedTags = []
|
||||
for (const kv of json.calculatedTags) {
|
||||
const index = kv.indexOf("=");
|
||||
let key = kv.substring(0, index).trim();
|
||||
const index = kv.indexOf("=")
|
||||
let key = kv.substring(0, index).trim()
|
||||
const r = "[a-z_][a-z0-9:]*"
|
||||
if (key.match(r) === null) {
|
||||
throw "At " + context + " invalid key for calculated tag: " + key + "; it should match " + r
|
||||
throw (
|
||||
"At " +
|
||||
context +
|
||||
" invalid key for calculated tag: " +
|
||||
key +
|
||||
"; it should match " +
|
||||
r
|
||||
)
|
||||
}
|
||||
const isStrict = key.endsWith(':')
|
||||
const isStrict = key.endsWith(":")
|
||||
if (isStrict) {
|
||||
key = key.substr(0, key.length - 1)
|
||||
}
|
||||
const code = kv.substring(index + 1);
|
||||
const code = kv.substring(index + 1)
|
||||
|
||||
try {
|
||||
new Function("feat", "return " + code + ";");
|
||||
new Function("feat", "return " + code + ";")
|
||||
} catch (e) {
|
||||
throw `Invalid function definition: the custom javascript is invalid:${e} (at ${context}). The offending javascript code is:\n ${code}`
|
||||
}
|
||||
|
||||
|
||||
this.calculatedTags.push([key, code, isStrict]);
|
||||
this.calculatedTags.push([key, code, isStrict])
|
||||
}
|
||||
}
|
||||
|
||||
this.doNotDownload = json.doNotDownload ?? false;
|
||||
this.passAllFeatures = json.passAllFeatures ?? false;
|
||||
this.minzoom = json.minzoom ?? 0;
|
||||
this.doNotDownload = json.doNotDownload ?? false
|
||||
this.passAllFeatures = json.passAllFeatures ?? false
|
||||
this.minzoom = json.minzoom ?? 0
|
||||
if (json["minZoom"] !== undefined) {
|
||||
throw "At " + context + ": minzoom is written all lowercase"
|
||||
}
|
||||
this.minzoomVisible = json.minzoomVisible ?? this.minzoom;
|
||||
this.shownByDefault = json.shownByDefault ?? true;
|
||||
this.forceLoad = json.forceLoad ?? false;
|
||||
this.minzoomVisible = json.minzoomVisible ?? this.minzoom
|
||||
this.shownByDefault = json.shownByDefault ?? true
|
||||
this.forceLoad = json.forceLoad ?? false
|
||||
if (json.presets !== undefined && json.presets?.map === undefined) {
|
||||
throw "Presets should be a list of items (at " + context + ")"
|
||||
}
|
||||
|
@ -207,23 +222,29 @@ export default class LayerConfig extends WithContextLoader {
|
|||
let preciseInput: PreciseInput = {
|
||||
preferredBackground: ["photo"],
|
||||
snapToLayers: undefined,
|
||||
maxSnapDistance: undefined
|
||||
};
|
||||
maxSnapDistance: undefined,
|
||||
}
|
||||
if (pr.preciseInput !== undefined) {
|
||||
if (pr.preciseInput === true) {
|
||||
pr.preciseInput = {
|
||||
preferredBackground: undefined
|
||||
preferredBackground: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
let snapToLayers: string[];
|
||||
let snapToLayers: string[]
|
||||
if (typeof pr.preciseInput.snapToLayer === "string") {
|
||||
snapToLayers = [pr.preciseInput.snapToLayer]
|
||||
} else {
|
||||
snapToLayers = pr.preciseInput.snapToLayer
|
||||
}
|
||||
|
||||
let preferredBackground: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[]
|
||||
let preferredBackground: (
|
||||
| "map"
|
||||
| "photo"
|
||||
| "osmbasedmap"
|
||||
| "historicphoto"
|
||||
| string
|
||||
)[]
|
||||
if (typeof pr.preciseInput.preferredBackground === "string") {
|
||||
preferredBackground = [pr.preciseInput.preferredBackground]
|
||||
} else {
|
||||
|
@ -232,19 +253,22 @@ export default class LayerConfig extends WithContextLoader {
|
|||
preciseInput = {
|
||||
preferredBackground,
|
||||
snapToLayers,
|
||||
maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10
|
||||
maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10,
|
||||
}
|
||||
}
|
||||
|
||||
const config: PresetConfig = {
|
||||
title: Translations.T(pr.title, `${translationContext}.presets.${i}.title`),
|
||||
tags: pr.tags.map((t) => TagUtils.SimpleTag(t)),
|
||||
description: Translations.T(pr.description, `${translationContext}.presets.${i}.description`),
|
||||
description: Translations.T(
|
||||
pr.description,
|
||||
`${translationContext}.presets.${i}.description`
|
||||
),
|
||||
preciseInput: preciseInput,
|
||||
exampleImages: pr.exampleImages
|
||||
exampleImages: pr.exampleImages,
|
||||
}
|
||||
return config;
|
||||
});
|
||||
return config
|
||||
})
|
||||
|
||||
if (json.mapRendering === undefined) {
|
||||
throw "MapRendering is undefined in " + context
|
||||
|
@ -255,41 +279,89 @@ export default class LayerConfig extends WithContextLoader {
|
|||
this.lineRendering = []
|
||||
} else {
|
||||
this.mapRendering = Utils.NoNull(json.mapRendering)
|
||||
.filter(r => r["location"] !== undefined)
|
||||
.map((r, i) => new PointRenderingConfig(<PointRenderingConfigJson>r, context + ".mapRendering[" + i + "]"))
|
||||
.filter((r) => r["location"] !== undefined)
|
||||
.map(
|
||||
(r, i) =>
|
||||
new PointRenderingConfig(
|
||||
<PointRenderingConfigJson>r,
|
||||
context + ".mapRendering[" + i + "]"
|
||||
)
|
||||
)
|
||||
|
||||
this.lineRendering = Utils.NoNull(json.mapRendering)
|
||||
.filter(r => r["location"] === undefined)
|
||||
.map((r, i) => new LineRenderingConfig(<LineRenderingConfigJson>r, context + ".mapRendering[" + i + "]"))
|
||||
.filter((r) => r["location"] === undefined)
|
||||
.map(
|
||||
(r, i) =>
|
||||
new LineRenderingConfig(
|
||||
<LineRenderingConfigJson>r,
|
||||
context + ".mapRendering[" + i + "]"
|
||||
)
|
||||
)
|
||||
|
||||
const hasCenterRendering = this.mapRendering.some(r => r.location.has("centroid") || r.location.has("start") || r.location.has("end"))
|
||||
const hasCenterRendering = this.mapRendering.some(
|
||||
(r) =>
|
||||
r.location.has("centroid") || r.location.has("start") || r.location.has("end")
|
||||
)
|
||||
|
||||
if (this.lineRendering.length === 0 && this.mapRendering.length === 0) {
|
||||
throw("The layer " + this.id + " does not have any maprenderings defined and will thus not show up on the map at all. If this is intentional, set maprenderings to 'null' instead of '[]'")
|
||||
} else if (!hasCenterRendering && this.lineRendering.length === 0 && !this.source.geojsonSource?.startsWith("https://api.openstreetmap.org/api/0.6/notes.json")) {
|
||||
throw "The layer " + this.id + " might not render ways. This might result in dropped information (at " + context + ")"
|
||||
throw (
|
||||
"The layer " +
|
||||
this.id +
|
||||
" does not have any maprenderings defined and will thus not show up on the map at all. If this is intentional, set maprenderings to 'null' instead of '[]'"
|
||||
)
|
||||
} else if (
|
||||
!hasCenterRendering &&
|
||||
this.lineRendering.length === 0 &&
|
||||
!this.source.geojsonSource?.startsWith(
|
||||
"https://api.openstreetmap.org/api/0.6/notes.json"
|
||||
)
|
||||
) {
|
||||
throw (
|
||||
"The layer " +
|
||||
this.id +
|
||||
" might not render ways. This might result in dropped information (at " +
|
||||
context +
|
||||
")"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const missingIds = Utils.NoNull(json.tagRenderings)?.filter(tr => typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined && tr["rewrite"] === undefined) ?? [];
|
||||
const missingIds =
|
||||
Utils.NoNull(json.tagRenderings)?.filter(
|
||||
(tr) =>
|
||||
typeof tr !== "string" &&
|
||||
tr["builtin"] === undefined &&
|
||||
tr["id"] === undefined &&
|
||||
tr["rewrite"] === undefined
|
||||
) ?? []
|
||||
if (missingIds?.length > 0 && official) {
|
||||
console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds)
|
||||
throw "Missing ids in tagrenderings"
|
||||
}
|
||||
|
||||
this.tagRenderings = (Utils.NoNull(json.tagRenderings) ?? []).map((tr, i) => new TagRenderingConfig(<TagRenderingConfigJson>tr, this.id + ".tagRenderings[" + i + "]"))
|
||||
this.tagRenderings = (Utils.NoNull(json.tagRenderings) ?? []).map(
|
||||
(tr, i) =>
|
||||
new TagRenderingConfig(
|
||||
<TagRenderingConfigJson>tr,
|
||||
this.id + ".tagRenderings[" + i + "]"
|
||||
)
|
||||
)
|
||||
|
||||
if (json.filter !== undefined && json.filter !== null && json.filter["sameAs"] !== undefined) {
|
||||
if (
|
||||
json.filter !== undefined &&
|
||||
json.filter !== null &&
|
||||
json.filter["sameAs"] !== undefined
|
||||
) {
|
||||
this.filterIsSameAs = json.filter["sameAs"]
|
||||
this.filters = []
|
||||
} else {
|
||||
this.filters = (<FilterConfigJson[]>json.filter ?? []).map((option, i) => {
|
||||
return new FilterConfig(option, `layers:${this.id}.filter.${i}`)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const duplicateIds = Utils.Dupiclates(this.filters.map(f => f.id))
|
||||
const duplicateIds = Utils.Dupiclates(this.filters.map((f) => f.id))
|
||||
if (duplicateIds.length > 0) {
|
||||
throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)`
|
||||
}
|
||||
|
@ -299,46 +371,44 @@ export default class LayerConfig extends WithContextLoader {
|
|||
throw "Error in " + context + ": use 'filter' instead of 'filters'"
|
||||
}
|
||||
|
||||
this.titleIcons = this.ParseTagRenderings(<TagRenderingConfigJson[]>json.titleIcons, {
|
||||
readOnlyMode: true,
|
||||
})
|
||||
|
||||
this.titleIcons = this.ParseTagRenderings((<TagRenderingConfigJson[]>json.titleIcons), {
|
||||
readOnlyMode: true
|
||||
});
|
||||
this.title = this.tr("title", undefined)
|
||||
this.isShown = TagUtils.TagD(json.isShown, context + ".isShown")
|
||||
|
||||
this.title = this.tr("title", undefined);
|
||||
this.isShown = TagUtils.TagD(json.isShown, context+".isShown")
|
||||
|
||||
this.deletion = null;
|
||||
this.deletion = null
|
||||
if (json.deletion === true) {
|
||||
json.deletion = {};
|
||||
json.deletion = {}
|
||||
}
|
||||
if (json.deletion !== undefined && json.deletion !== false) {
|
||||
this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`);
|
||||
this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`)
|
||||
}
|
||||
|
||||
this.allowMove = null
|
||||
if (json.allowMove === false) {
|
||||
this.allowMove = null;
|
||||
this.allowMove = null
|
||||
} else if (json.allowMove === true) {
|
||||
this.allowMove = new MoveConfig({}, context + ".allowMove")
|
||||
} else if (json.allowMove !== undefined && json.allowMove !== false) {
|
||||
this.allowMove = new MoveConfig(json.allowMove, context + ".allowMove")
|
||||
}
|
||||
|
||||
|
||||
if (json["showIf"] !== undefined) {
|
||||
throw (
|
||||
"Invalid key on layerconfig " +
|
||||
this.id +
|
||||
": showIf. Did you mean 'isShown' instead?"
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public defaultIcon(): BaseUIElement | undefined {
|
||||
if (this.mapRendering === undefined || this.mapRendering === null) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const mapRendering = this.mapRendering.filter(r => r.location.has("point"))[0]
|
||||
const mapRendering = this.mapRendering.filter((r) => r.location.has("point"))[0]
|
||||
if (mapRendering === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -346,64 +416,104 @@ export default class LayerConfig extends WithContextLoader {
|
|||
}
|
||||
|
||||
public GetBaseTags(): any {
|
||||
return TagUtils.changeAsProperties(this.source.osmTags.asChange({id: "node/-1"}))
|
||||
return TagUtils.changeAsProperties(this.source.osmTags.asChange({ id: "node/-1" }))
|
||||
}
|
||||
|
||||
public GenerateDocumentation(usedInThemes: string[], layerIsNeededBy?: Map<string, string[]>, dependencies: {
|
||||
context?: string;
|
||||
reason: string;
|
||||
neededLayer: string;
|
||||
}[] = []
|
||||
, addedByDefault = false, canBeIncluded = true): BaseUIElement {
|
||||
const extraProps : (string | BaseUIElement)[] = []
|
||||
public GenerateDocumentation(
|
||||
usedInThemes: string[],
|
||||
layerIsNeededBy?: Map<string, string[]>,
|
||||
dependencies: {
|
||||
context?: string
|
||||
reason: string
|
||||
neededLayer: string
|
||||
}[] = [],
|
||||
addedByDefault = false,
|
||||
canBeIncluded = true
|
||||
): BaseUIElement {
|
||||
const extraProps: (string | BaseUIElement)[] = []
|
||||
|
||||
extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher")
|
||||
|
||||
if (canBeIncluded) {
|
||||
if (addedByDefault) {
|
||||
extraProps.push("**This layer is included automatically in every theme. This layer might contain no points**")
|
||||
extraProps.push(
|
||||
"**This layer is included automatically in every theme. This layer might contain no points**"
|
||||
)
|
||||
}
|
||||
if (this.shownByDefault === false) {
|
||||
extraProps.push('This layer is not visible by default and must be enabled in the filter by the user. ')
|
||||
extraProps.push(
|
||||
"This layer is not visible by default and must be enabled in the filter by the user. "
|
||||
)
|
||||
}
|
||||
if (this.title === undefined) {
|
||||
extraProps.push("Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable.")
|
||||
extraProps.push(
|
||||
"Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable."
|
||||
)
|
||||
}
|
||||
if (this.name === undefined && this.shownByDefault === false) {
|
||||
extraProps.push("This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true")
|
||||
extraProps.push(
|
||||
"This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true"
|
||||
)
|
||||
}
|
||||
if (this.name === undefined) {
|
||||
extraProps.push("Not visible in the layer selection by default. If you want to make this layer toggable, override `name`")
|
||||
extraProps.push(
|
||||
"Not visible in the layer selection by default. If you want to make this layer toggable, override `name`"
|
||||
)
|
||||
}
|
||||
if (this.mapRendering.length === 0) {
|
||||
extraProps.push("Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`")
|
||||
extraProps.push(
|
||||
"Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`"
|
||||
)
|
||||
}
|
||||
|
||||
if (this.source.geojsonSource !== undefined) {
|
||||
extraProps.push(
|
||||
new Combine([
|
||||
Utils.runningFromConsole ? "<img src='../warning.svg' height='1rem'/>" : undefined,
|
||||
"This layer is loaded from an external source, namely ",
|
||||
new FixedUiElement( this.source.geojsonSource).SetClass("code")]));
|
||||
Utils.runningFromConsole
|
||||
? "<img src='../warning.svg' height='1rem'/>"
|
||||
: undefined,
|
||||
"This layer is loaded from an external source, namely ",
|
||||
new FixedUiElement(this.source.geojsonSource).SetClass("code"),
|
||||
])
|
||||
)
|
||||
}
|
||||
} else {
|
||||
extraProps.push("This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.")
|
||||
extraProps.push(
|
||||
"This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
let usingLayer: BaseUIElement[] = []
|
||||
if (usedInThemes?.length > 0 && !addedByDefault) {
|
||||
usingLayer = [new Title("Themes using this layer", 4),
|
||||
new List((usedInThemes ?? []).map(id => new Link(id, "https://mapcomplete.osm.be/" + id)))
|
||||
usingLayer = [
|
||||
new Title("Themes using this layer", 4),
|
||||
new List(
|
||||
(usedInThemes ?? []).map(
|
||||
(id) => new Link(id, "https://mapcomplete.osm.be/" + id)
|
||||
)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
for (const dep of dependencies) {
|
||||
extraProps.push(new Combine(["This layer will automatically load ", new Link(dep.neededLayer, "./" + dep.neededLayer + ".md"), " into the layout as it depends on it: ", dep.reason, "(" + dep.context + ")"]))
|
||||
extraProps.push(
|
||||
new Combine([
|
||||
"This layer will automatically load ",
|
||||
new Link(dep.neededLayer, "./" + dep.neededLayer + ".md"),
|
||||
" into the layout as it depends on it: ",
|
||||
dep.reason,
|
||||
"(" + dep.context + ")",
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
for (const revDep of Utils.Dedup(layerIsNeededBy?.get(this.id) ?? [])) {
|
||||
extraProps.push(new Combine(["This layer is needed as dependency for layer", new Link(revDep, "#" + revDep)]))
|
||||
extraProps.push(
|
||||
new Combine([
|
||||
"This layer is needed as dependency for layer",
|
||||
new Link(revDep, "#" + revDep),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
let neededTags: TagsFilter[] = [this.source.osmTags]
|
||||
|
@ -411,86 +521,110 @@ export default class LayerConfig extends WithContextLoader {
|
|||
neededTags = this.source.osmTags["and"]
|
||||
}
|
||||
|
||||
let tableRows = Utils.NoNull(this.tagRenderings.map(tr => tr.FreeformValues())
|
||||
.map(values => {
|
||||
if (values == undefined) {
|
||||
return undefined
|
||||
}
|
||||
const embedded: (Link | string)[] = values.values?.map(v => Link.OsmWiki(values.key, v, true).SetClass("mr-2")) ?? ["_no preset options defined, or no values in them_"]
|
||||
return [
|
||||
new Combine([
|
||||
new Link(
|
||||
Utils.runningFromConsole ? "<img src='https://mapcomplete.osm.be/assets/svg/statistics.svg' height='18px'>" : Svg.statistics_svg().SetClass("w-4 h-4 mr-2"),
|
||||
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values", true
|
||||
), Link.OsmWiki(values.key)
|
||||
]).SetClass("flex"),
|
||||
values.type === undefined ? "Multiple choice" : new Link(values.type, "../SpecialInputElements.md#" + values.type),
|
||||
new Combine(embedded).SetClass("flex")
|
||||
];
|
||||
}))
|
||||
let tableRows = Utils.NoNull(
|
||||
this.tagRenderings
|
||||
.map((tr) => tr.FreeformValues())
|
||||
.map((values) => {
|
||||
if (values == undefined) {
|
||||
return undefined
|
||||
}
|
||||
const embedded: (Link | string)[] = values.values?.map((v) =>
|
||||
Link.OsmWiki(values.key, v, true).SetClass("mr-2")
|
||||
) ?? ["_no preset options defined, or no values in them_"]
|
||||
return [
|
||||
new Combine([
|
||||
new Link(
|
||||
Utils.runningFromConsole
|
||||
? "<img src='https://mapcomplete.osm.be/assets/svg/statistics.svg' height='18px'>"
|
||||
: Svg.statistics_svg().SetClass("w-4 h-4 mr-2"),
|
||||
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values",
|
||||
true
|
||||
),
|
||||
Link.OsmWiki(values.key),
|
||||
]).SetClass("flex"),
|
||||
values.type === undefined
|
||||
? "Multiple choice"
|
||||
: new Link(values.type, "../SpecialInputElements.md#" + values.type),
|
||||
new Combine(embedded).SetClass("flex"),
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
let quickOverview: BaseUIElement = undefined;
|
||||
let quickOverview: BaseUIElement = undefined
|
||||
if (tableRows.length > 0) {
|
||||
quickOverview = new Combine([
|
||||
new FixedUiElement("Warning: ").SetClass("bold"),
|
||||
"this quick overview is incomplete",
|
||||
new Table(["attribute", "type", "values which are supported by this layer"], tableRows).SetClass("zebra-table")
|
||||
new Table(
|
||||
["attribute", "type", "values which are supported by this layer"],
|
||||
tableRows
|
||||
).SetClass("zebra-table"),
|
||||
]).SetClass("flex-col flex")
|
||||
}
|
||||
|
||||
|
||||
let iconImg: BaseUIElement = new FixedUiElement("")
|
||||
|
||||
if (Utils.runningFromConsole) {
|
||||
const icon = this.mapRendering
|
||||
.filter(mr => mr.location.has("point"))
|
||||
.map(mr => mr.icon?.render?.txt)
|
||||
.find(i => i !== undefined)
|
||||
.filter((mr) => mr.location.has("point"))
|
||||
.map((mr) => mr.icon?.render?.txt)
|
||||
.find((i) => i !== undefined)
|
||||
// This is for the documentation in a markdown-file, so we have to use raw HTML
|
||||
if (icon !== undefined) {
|
||||
iconImg = new FixedUiElement(`<img src='https://mapcomplete.osm.be/${icon}' height="100px"> `)
|
||||
iconImg = new FixedUiElement(
|
||||
`<img src='https://mapcomplete.osm.be/${icon}' height="100px"> `
|
||||
)
|
||||
}
|
||||
} else {
|
||||
iconImg = this.mapRendering
|
||||
.filter(mr => mr.location.has("point"))
|
||||
.map(mr => mr.GenerateLeafletStyle(new UIEventSource<OsmTags>({id:"node/-1"}), false, {includeBadges: false}).html)
|
||||
.find(i => i !== undefined)
|
||||
.filter((mr) => mr.location.has("point"))
|
||||
.map(
|
||||
(mr) =>
|
||||
mr.GenerateLeafletStyle(
|
||||
new UIEventSource<OsmTags>({ id: "node/-1" }),
|
||||
false,
|
||||
{ includeBadges: false }
|
||||
).html
|
||||
)
|
||||
.find((i) => i !== undefined)
|
||||
}
|
||||
|
||||
let overpassLink: BaseUIElement = undefined;
|
||||
let overpassLink: BaseUIElement = undefined
|
||||
if (Constants.priviliged_layers.indexOf(this.id) < 0) {
|
||||
try {
|
||||
overpassLink = new Link("Execute on overpass", Overpass.AsOverpassTurboLink(<TagsFilter>new And(neededTags).optimize()))
|
||||
overpassLink = new Link(
|
||||
"Execute on overpass",
|
||||
Overpass.AsOverpassTurboLink(<TagsFilter>new And(neededTags).optimize())
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Could not generate overpasslink for " + this.id)
|
||||
}
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
new Combine([
|
||||
new Title(this.id, 1),
|
||||
iconImg,
|
||||
this.description,
|
||||
"\n"
|
||||
]).SetClass("flex flex-col"),
|
||||
new Combine([new Title(this.id, 1), iconImg, this.description, "\n"]).SetClass(
|
||||
"flex flex-col"
|
||||
),
|
||||
new List(extraProps),
|
||||
...usingLayer,
|
||||
|
||||
new Title("Basic tags for this layer", 2),
|
||||
"Elements must have the all of following tags to be shown on this layer:",
|
||||
new List(neededTags.map(t => t.asHumanString(true, false, {}))),
|
||||
new List(neededTags.map((t) => t.asHumanString(true, false, {}))),
|
||||
overpassLink,
|
||||
new Title("Supported attributes", 2),
|
||||
quickOverview,
|
||||
...this.tagRenderings.map(tr => tr.GenerateDocumentation())
|
||||
]).SetClass("flex-col").SetClass("link-underline")
|
||||
...this.tagRenderings.map((tr) => tr.GenerateDocumentation()),
|
||||
])
|
||||
.SetClass("flex-col")
|
||||
.SetClass("link-underline")
|
||||
}
|
||||
|
||||
public CustomCodeSnippets(): string[] {
|
||||
if (this.calculatedTags === undefined) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
return this.calculatedTags.map((code) => code[1]);
|
||||
return this.calculatedTags.map((code) => code[1])
|
||||
}
|
||||
|
||||
AllTagRenderings(): TagRenderingConfig[] {
|
||||
|
@ -498,6 +632,6 @@ export default class LayerConfig extends WithContextLoader {
|
|||
}
|
||||
|
||||
public isLeftRightSensitive(): boolean {
|
||||
return this.lineRendering.some(lr => lr.leftRightSensitive)
|
||||
return this.lineRendering.some((lr) => lr.leftRightSensitive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,68 +1,72 @@
|
|||
import {Translation} from "../../UI/i18n/Translation";
|
||||
import {LayoutConfigJson} from "./Json/LayoutConfigJson";
|
||||
import LayerConfig from "./LayerConfig";
|
||||
import {LayerConfigJson} from "./Json/LayerConfigJson";
|
||||
import Constants from "../Constants";
|
||||
import TilesourceConfig from "./TilesourceConfig";
|
||||
import {ExtractImages} from "./Conversion/FixImages";
|
||||
import ExtraLinkConfig from "./ExtraLinkConfig";
|
||||
import { Translation } from "../../UI/i18n/Translation"
|
||||
import { LayoutConfigJson } from "./Json/LayoutConfigJson"
|
||||
import LayerConfig from "./LayerConfig"
|
||||
import { LayerConfigJson } from "./Json/LayerConfigJson"
|
||||
import Constants from "../Constants"
|
||||
import TilesourceConfig from "./TilesourceConfig"
|
||||
import { ExtractImages } from "./Conversion/FixImages"
|
||||
import ExtraLinkConfig from "./ExtraLinkConfig"
|
||||
|
||||
export default class LayoutConfig {
|
||||
public static readonly defaultSocialImage = "assets/SocialImage.png"
|
||||
public readonly id: string;
|
||||
public readonly credits?: string;
|
||||
public readonly language: string[];
|
||||
public readonly title: Translation;
|
||||
public readonly shortDescription: Translation;
|
||||
public readonly description: Translation;
|
||||
public readonly descriptionTail?: Translation;
|
||||
public readonly icon: string;
|
||||
public readonly socialImage?: string;
|
||||
public readonly startZoom: number;
|
||||
public readonly startLat: number;
|
||||
public readonly startLon: number;
|
||||
public readonly widenFactor: number;
|
||||
public readonly defaultBackgroundId?: string;
|
||||
public layers: LayerConfig[];
|
||||
public readonly id: string
|
||||
public readonly credits?: string
|
||||
public readonly language: string[]
|
||||
public readonly title: Translation
|
||||
public readonly shortDescription: Translation
|
||||
public readonly description: Translation
|
||||
public readonly descriptionTail?: Translation
|
||||
public readonly icon: string
|
||||
public readonly socialImage?: string
|
||||
public readonly startZoom: number
|
||||
public readonly startLat: number
|
||||
public readonly startLon: number
|
||||
public readonly widenFactor: number
|
||||
public readonly defaultBackgroundId?: string
|
||||
public layers: LayerConfig[]
|
||||
public tileLayerSources: TilesourceConfig[]
|
||||
public readonly clustering?: {
|
||||
maxZoom: number,
|
||||
minNeededElements: number,
|
||||
};
|
||||
public readonly hideFromOverview: boolean;
|
||||
public lockLocation: boolean | [[number, number], [number, number]];
|
||||
public readonly enableUserBadge: boolean;
|
||||
public readonly enableShareScreen: boolean;
|
||||
public readonly enableMoreQuests: boolean;
|
||||
public readonly enableAddNewPoints: boolean;
|
||||
public readonly enableLayers: boolean;
|
||||
public readonly enableSearch: boolean;
|
||||
public readonly enableGeolocation: boolean;
|
||||
public readonly enableBackgroundLayerSelection: boolean;
|
||||
public readonly enableShowAllQuestions: boolean;
|
||||
public readonly enableExportButton: boolean;
|
||||
public readonly enablePdfDownload: boolean;
|
||||
maxZoom: number
|
||||
minNeededElements: number
|
||||
}
|
||||
public readonly hideFromOverview: boolean
|
||||
public lockLocation: boolean | [[number, number], [number, number]]
|
||||
public readonly enableUserBadge: boolean
|
||||
public readonly enableShareScreen: boolean
|
||||
public readonly enableMoreQuests: boolean
|
||||
public readonly enableAddNewPoints: boolean
|
||||
public readonly enableLayers: boolean
|
||||
public readonly enableSearch: boolean
|
||||
public readonly enableGeolocation: boolean
|
||||
public readonly enableBackgroundLayerSelection: boolean
|
||||
public readonly enableShowAllQuestions: boolean
|
||||
public readonly enableExportButton: boolean
|
||||
public readonly enablePdfDownload: boolean
|
||||
|
||||
public readonly customCss?: string;
|
||||
public readonly customCss?: string
|
||||
|
||||
public readonly overpassUrl: string[];
|
||||
public readonly overpassTimeout: number;
|
||||
public readonly overpassUrl: string[]
|
||||
public readonly overpassTimeout: number
|
||||
public readonly overpassMaxZoom: number
|
||||
public readonly osmApiTileSize: number
|
||||
public readonly official: boolean;
|
||||
public readonly official: boolean
|
||||
|
||||
public readonly usedImages: string[]
|
||||
public readonly extraLink?: ExtraLinkConfig
|
||||
|
||||
public readonly definedAtUrl?: string;
|
||||
public readonly definitionRaw?: string;
|
||||
public readonly definedAtUrl?: string
|
||||
public readonly definitionRaw?: string
|
||||
|
||||
constructor(json: LayoutConfigJson, official = true, options?: {
|
||||
definedAtUrl?: string,
|
||||
definitionRaw?: string
|
||||
}) {
|
||||
this.official = official;
|
||||
this.id = json.id;
|
||||
constructor(
|
||||
json: LayoutConfigJson,
|
||||
official = true,
|
||||
options?: {
|
||||
definedAtUrl?: string
|
||||
definitionRaw?: string
|
||||
}
|
||||
) {
|
||||
this.official = official
|
||||
this.id = json.id
|
||||
this.definedAtUrl = options?.definedAtUrl
|
||||
this.definitionRaw = options?.definitionRaw
|
||||
if (official) {
|
||||
|
@ -74,73 +78,108 @@ export default class LayoutConfig {
|
|||
}
|
||||
}
|
||||
const context = this.id
|
||||
this.credits = json.credits;
|
||||
this.language = json.mustHaveLanguage ?? Array.from(Object.keys(json.title));
|
||||
this.usedImages = Array.from(new ExtractImages(official, undefined).convertStrict(json, "while extracting the images of " + json.id + " " + context ?? "")).sort()
|
||||
this.credits = json.credits
|
||||
this.language = json.mustHaveLanguage ?? Array.from(Object.keys(json.title))
|
||||
this.usedImages = Array.from(
|
||||
new ExtractImages(official, undefined).convertStrict(
|
||||
json,
|
||||
"while extracting the images of " + json.id + " " + context ?? ""
|
||||
)
|
||||
).sort()
|
||||
{
|
||||
if (typeof json.title === "string") {
|
||||
throw `The title of a theme should always be a translation, as it sets the corresponding languages (${context}.title). The themenID is ${this.id}; the offending object is ${JSON.stringify(json.title)} which is a ${typeof json.title})`
|
||||
throw `The title of a theme should always be a translation, as it sets the corresponding languages (${context}.title). The themenID is ${
|
||||
this.id
|
||||
}; the offending object is ${JSON.stringify(
|
||||
json.title
|
||||
)} which is a ${typeof json.title})`
|
||||
}
|
||||
if (this.language.length == 0) {
|
||||
throw `No languages defined. Define at least one language. (${context}.languages)`
|
||||
}
|
||||
if (json.title === undefined) {
|
||||
throw "Title not defined in " + this.id;
|
||||
throw "Title not defined in " + this.id
|
||||
}
|
||||
if (json.description === undefined) {
|
||||
throw "Description not defined in " + this.id;
|
||||
throw "Description not defined in " + this.id
|
||||
}
|
||||
if (json.widenFactor <= 0) {
|
||||
throw "Widenfactor too small, shoud be > 0"
|
||||
}
|
||||
if (json.widenFactor > 20) {
|
||||
throw "Widenfactor is very big, use a value between 1 and 5 (current value is " + json.widenFactor + ") at " + context
|
||||
throw (
|
||||
"Widenfactor is very big, use a value between 1 and 5 (current value is " +
|
||||
json.widenFactor +
|
||||
") at " +
|
||||
context
|
||||
)
|
||||
}
|
||||
if (json["hideInOverview"]) {
|
||||
throw "The json for " + this.id + " contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
|
||||
throw (
|
||||
"The json for " +
|
||||
this.id +
|
||||
" contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
|
||||
)
|
||||
}
|
||||
if (json.layers === undefined) {
|
||||
throw "Got undefined layers for " + json.id + " at " + context
|
||||
}
|
||||
}
|
||||
this.title = new Translation(json.title, "themes:" + context + ".title");
|
||||
this.description = new Translation(json.description, "themes:" + context + ".description");
|
||||
this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, "themes:" + context + ".shortdescription");
|
||||
this.descriptionTail = json.descriptionTail === undefined ? undefined : new Translation(json.descriptionTail, "themes:" + context + ".descriptionTail");
|
||||
this.icon = json.icon;
|
||||
this.socialImage = json.socialImage ?? LayoutConfig.defaultSocialImage;
|
||||
this.title = new Translation(json.title, "themes:" + context + ".title")
|
||||
this.description = new Translation(json.description, "themes:" + context + ".description")
|
||||
this.shortDescription =
|
||||
json.shortDescription === undefined
|
||||
? this.description.FirstSentence()
|
||||
: new Translation(json.shortDescription, "themes:" + context + ".shortdescription")
|
||||
this.descriptionTail =
|
||||
json.descriptionTail === undefined
|
||||
? undefined
|
||||
: new Translation(json.descriptionTail, "themes:" + context + ".descriptionTail")
|
||||
this.icon = json.icon
|
||||
this.socialImage = json.socialImage ?? LayoutConfig.defaultSocialImage
|
||||
if (this.socialImage === "") {
|
||||
if (official) {
|
||||
throw "Theme " + json.id + " has empty string as social image"
|
||||
}
|
||||
}
|
||||
this.startZoom = json.startZoom;
|
||||
this.startLat = json.startLat;
|
||||
this.startLon = json.startLon;
|
||||
this.widenFactor = json.widenFactor ?? 1.5;
|
||||
this.startZoom = json.startZoom
|
||||
this.startLat = json.startLat
|
||||
this.startLon = json.startLon
|
||||
this.widenFactor = json.widenFactor ?? 1.5
|
||||
|
||||
this.defaultBackgroundId = json.defaultBackgroundId;
|
||||
this.tileLayerSources = (json.tileLayerSources ?? []).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`))
|
||||
this.defaultBackgroundId = json.defaultBackgroundId
|
||||
this.tileLayerSources = (json.tileLayerSources ?? []).map(
|
||||
(config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`)
|
||||
)
|
||||
// At this point, layers should be expanded and validated either by the generateScript or the LegacyJsonConvert
|
||||
this.layers = json.layers.map(lyrJson => new LayerConfig(<LayerConfigJson>lyrJson, json.id + ".layers." + lyrJson["id"], official));
|
||||
|
||||
this.extraLink = new ExtraLinkConfig(json.extraLink ?? {
|
||||
icon: "./assets/svg/pop-out.svg",
|
||||
href: "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}",
|
||||
newTab: true,
|
||||
requirements: ["iframe", "no-welcome-message"]
|
||||
}, context + ".extraLink")
|
||||
this.layers = json.layers.map(
|
||||
(lyrJson) =>
|
||||
new LayerConfig(
|
||||
<LayerConfigJson>lyrJson,
|
||||
json.id + ".layers." + lyrJson["id"],
|
||||
official
|
||||
)
|
||||
)
|
||||
|
||||
this.extraLink = new ExtraLinkConfig(
|
||||
json.extraLink ?? {
|
||||
icon: "./assets/svg/pop-out.svg",
|
||||
href: "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}",
|
||||
newTab: true,
|
||||
requirements: ["iframe", "no-welcome-message"],
|
||||
},
|
||||
context + ".extraLink"
|
||||
)
|
||||
|
||||
this.clustering = {
|
||||
maxZoom: 16,
|
||||
minNeededElements: 250,
|
||||
};
|
||||
}
|
||||
if (json.clustering === false) {
|
||||
this.clustering = {
|
||||
maxZoom: 0,
|
||||
minNeededElements: 100000,
|
||||
};
|
||||
}
|
||||
} else if (json.clustering) {
|
||||
this.clustering = {
|
||||
maxZoom: json.clustering.maxZoom ?? 18,
|
||||
|
@ -148,20 +187,20 @@ export default class LayoutConfig {
|
|||
}
|
||||
}
|
||||
|
||||
this.hideFromOverview = json.hideFromOverview ?? false;
|
||||
this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined;
|
||||
this.enableUserBadge = json.enableUserBadge ?? true;
|
||||
this.enableShareScreen = json.enableShareScreen ?? true;
|
||||
this.enableMoreQuests = json.enableMoreQuests ?? true;
|
||||
this.enableLayers = json.enableLayers ?? true;
|
||||
this.enableSearch = json.enableSearch ?? true;
|
||||
this.enableGeolocation = json.enableGeolocation ?? true;
|
||||
this.enableAddNewPoints = json.enableAddNewPoints ?? true;
|
||||
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true;
|
||||
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false;
|
||||
this.enableExportButton = json.enableDownload ?? false;
|
||||
this.enablePdfDownload = json.enablePdfDownload ?? false;
|
||||
this.customCss = json.customCss;
|
||||
this.hideFromOverview = json.hideFromOverview ?? false
|
||||
this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined
|
||||
this.enableUserBadge = json.enableUserBadge ?? true
|
||||
this.enableShareScreen = json.enableShareScreen ?? true
|
||||
this.enableMoreQuests = json.enableMoreQuests ?? true
|
||||
this.enableLayers = json.enableLayers ?? true
|
||||
this.enableSearch = json.enableSearch ?? true
|
||||
this.enableGeolocation = json.enableGeolocation ?? true
|
||||
this.enableAddNewPoints = json.enableAddNewPoints ?? true
|
||||
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true
|
||||
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false
|
||||
this.enableExportButton = json.enableDownload ?? false
|
||||
this.enablePdfDownload = json.enablePdfDownload ?? false
|
||||
this.customCss = json.customCss
|
||||
this.overpassUrl = Constants.defaultOverpassUrls
|
||||
if (json.overpassUrl !== undefined) {
|
||||
if (typeof json.overpassUrl === "string") {
|
||||
|
@ -173,27 +212,27 @@ export default class LayoutConfig {
|
|||
this.overpassTimeout = json.overpassTimeout ?? 30
|
||||
this.overpassMaxZoom = json.overpassMaxZoom ?? 16
|
||||
this.osmApiTileSize = json.osmApiTileSize ?? this.overpassMaxZoom + 1
|
||||
|
||||
}
|
||||
|
||||
public CustomCodeSnippets(): string[] {
|
||||
if (this.official) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
const msg = "<br/><b>This layout uses <span class='alert'>custom javascript</span>, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:</b><br/>"
|
||||
const custom = [];
|
||||
const msg =
|
||||
"<br/><b>This layout uses <span class='alert'>custom javascript</span>, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:</b><br/>"
|
||||
const custom = []
|
||||
for (const layer of this.layers) {
|
||||
custom.push(...layer.CustomCodeSnippets().map(code => code + "<br />"))
|
||||
custom.push(...layer.CustomCodeSnippets().map((code) => code + "<br />"))
|
||||
}
|
||||
if (custom.length === 0) {
|
||||
return custom;
|
||||
return custom
|
||||
}
|
||||
custom.splice(0, 0, msg);
|
||||
return custom;
|
||||
custom.splice(0, 0, msg)
|
||||
return custom
|
||||
}
|
||||
|
||||
public isLeftRightSensitive() {
|
||||
return this.layers.some(l => l.isLeftRightSensitive())
|
||||
return this.layers.some((l) => l.isLeftRightSensitive())
|
||||
}
|
||||
|
||||
public getMatchingLayer(tags: any): LayerConfig | undefined {
|
||||
|
@ -207,5 +246,4 @@ export default class LayoutConfig {
|
|||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,43 +1,49 @@
|
|||
import WithContextLoader from "./WithContextLoader";
|
||||
import TagRenderingConfig from "./TagRenderingConfig";
|
||||
import {Utils} from "../../Utils";
|
||||
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";
|
||||
import WithContextLoader from "./WithContextLoader"
|
||||
import TagRenderingConfig from "./TagRenderingConfig"
|
||||
import { Utils } from "../../Utils"
|
||||
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson"
|
||||
|
||||
export default class LineRenderingConfig extends WithContextLoader {
|
||||
|
||||
|
||||
public readonly color: TagRenderingConfig;
|
||||
public readonly width: TagRenderingConfig;
|
||||
public readonly dashArray: TagRenderingConfig;
|
||||
public readonly lineCap: TagRenderingConfig;
|
||||
public readonly offset: TagRenderingConfig;
|
||||
public readonly fill: TagRenderingConfig;
|
||||
public readonly fillColor: TagRenderingConfig;
|
||||
public readonly color: TagRenderingConfig
|
||||
public readonly width: TagRenderingConfig
|
||||
public readonly dashArray: TagRenderingConfig
|
||||
public readonly lineCap: TagRenderingConfig
|
||||
public readonly offset: TagRenderingConfig
|
||||
public readonly fill: TagRenderingConfig
|
||||
public readonly fillColor: TagRenderingConfig
|
||||
public readonly leftRightSensitive: boolean
|
||||
|
||||
constructor(json: LineRenderingConfigJson, context: string) {
|
||||
super(json, context)
|
||||
this.color = this.tr("color", "#0000ff");
|
||||
this.width = this.tr("width", "7");
|
||||
this.dashArray = this.tr("dashArray", "");
|
||||
this.lineCap = this.tr("lineCap", "round");
|
||||
this.fill = this.tr("fill", undefined);
|
||||
this.fillColor = this.tr("fillColor", undefined);
|
||||
this.color = this.tr("color", "#0000ff")
|
||||
this.width = this.tr("width", "7")
|
||||
this.dashArray = this.tr("dashArray", "")
|
||||
this.lineCap = this.tr("lineCap", "round")
|
||||
this.fill = this.tr("fill", undefined)
|
||||
this.fillColor = this.tr("fillColor", undefined)
|
||||
|
||||
this.leftRightSensitive = json.offset !== undefined && json.offset !== 0 && json.offset !== "0"
|
||||
this.leftRightSensitive =
|
||||
json.offset !== undefined && json.offset !== 0 && json.offset !== "0"
|
||||
|
||||
this.offset = this.tr("offset", "0");
|
||||
this.offset = this.tr("offset", "0")
|
||||
}
|
||||
|
||||
public GenerateLeafletStyle(tags: {}):
|
||||
{ fillColor?: string; color: string; lineCap: string; offset: number; weight: number; dashArray: string; fill?: boolean } {
|
||||
public GenerateLeafletStyle(tags: {}): {
|
||||
fillColor?: string
|
||||
color: string
|
||||
lineCap: string
|
||||
offset: number
|
||||
weight: number
|
||||
dashArray: string
|
||||
fill?: boolean
|
||||
} {
|
||||
function rendernum(tr: TagRenderingConfig, deflt: number) {
|
||||
const str = Number(render(tr, "" + deflt));
|
||||
const n = Number(str);
|
||||
const str = Number(render(tr, "" + deflt))
|
||||
const n = Number(str)
|
||||
if (isNaN(n)) {
|
||||
return deflt;
|
||||
return deflt
|
||||
}
|
||||
return n;
|
||||
return n
|
||||
}
|
||||
|
||||
function render(tr: TagRenderingConfig, deflt?: string) {
|
||||
|
@ -47,19 +53,17 @@ export default class LineRenderingConfig extends WithContextLoader {
|
|||
if (tr === undefined) {
|
||||
return deflt
|
||||
}
|
||||
const str = tr?.GetRenderValue(tags)?.txt ?? deflt;
|
||||
const str = tr?.GetRenderValue(tags)?.txt ?? deflt
|
||||
if (str === "") {
|
||||
return deflt
|
||||
}
|
||||
return Utils.SubstituteKeys(str, tags)?.replace(/{.*}/g, "");
|
||||
return Utils.SubstituteKeys(str, tags)?.replace(/{.*}/g, "")
|
||||
}
|
||||
|
||||
const dashArray = render(this.dashArray);
|
||||
let color = render(this.color, "#00f");
|
||||
const dashArray = render(this.dashArray)
|
||||
let color = render(this.color, "#00f")
|
||||
if (color.startsWith("--")) {
|
||||
color = getComputedStyle(document.body).getPropertyValue(
|
||||
"--catch-detail-color"
|
||||
);
|
||||
color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color")
|
||||
}
|
||||
|
||||
const style = {
|
||||
|
@ -67,7 +71,7 @@ export default class LineRenderingConfig extends WithContextLoader {
|
|||
dashArray,
|
||||
weight: rendernum(this.width, 5),
|
||||
lineCap: render(this.lineCap),
|
||||
offset: rendernum(this.offset, 0)
|
||||
offset: rendernum(this.offset, 0),
|
||||
}
|
||||
|
||||
const fillStr = render(this.fill, undefined)
|
||||
|
@ -80,7 +84,5 @@ export default class LineRenderingConfig extends WithContextLoader {
|
|||
style["fillColor"] = fillColorStr
|
||||
}
|
||||
return style
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import MoveConfigJson from "./Json/MoveConfigJson";
|
||||
import MoveConfigJson from "./Json/MoveConfigJson"
|
||||
|
||||
export default class MoveConfig {
|
||||
|
||||
public readonly enableImproveAccuracy: boolean
|
||||
public readonly enableRelocation: boolean
|
||||
|
||||
|
@ -12,6 +11,4 @@ export default class MoveConfig {
|
|||
throw "At least one default move reason should be allowed (at " + context + ")"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,35 @@
|
|||
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
|
||||
import TagRenderingConfig from "./TagRenderingConfig";
|
||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import {Utils} from "../../Utils";
|
||||
import Svg from "../../Svg";
|
||||
import WithContextLoader from "./WithContextLoader";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||
import Img from "../../UI/Base/Img";
|
||||
import Combine from "../../UI/Base/Combine";
|
||||
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
||||
|
||||
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"
|
||||
import TagRenderingConfig from "./TagRenderingConfig"
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||
import SharedTagRenderings from "../../Customizations/SharedTagRenderings"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import { Utils } from "../../Utils"
|
||||
import Svg from "../../Svg"
|
||||
import WithContextLoader from "./WithContextLoader"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
|
||||
import Img from "../../UI/Base/Img"
|
||||
import Combine from "../../UI/Base/Combine"
|
||||
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
|
||||
|
||||
export default class PointRenderingConfig extends WithContextLoader {
|
||||
private static readonly allowed_location_codes = new Set<string>([
|
||||
"point",
|
||||
"centroid",
|
||||
"start",
|
||||
"end",
|
||||
"projected_centerpoint",
|
||||
])
|
||||
public readonly location: Set<
|
||||
"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string
|
||||
>
|
||||
|
||||
private static readonly allowed_location_codes = new Set<string>(["point", "centroid", "start", "end","projected_centerpoint"])
|
||||
public readonly location: Set<"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string>
|
||||
|
||||
public readonly icon: TagRenderingConfig;
|
||||
public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[];
|
||||
public readonly iconSize: TagRenderingConfig;
|
||||
public readonly label: TagRenderingConfig;
|
||||
public readonly rotation: TagRenderingConfig;
|
||||
public readonly icon: TagRenderingConfig
|
||||
public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[]
|
||||
public readonly iconSize: TagRenderingConfig
|
||||
public readonly label: TagRenderingConfig
|
||||
public readonly rotation: TagRenderingConfig
|
||||
|
||||
constructor(json: PointRenderingConfigJson, context: string) {
|
||||
super(json, context)
|
||||
|
@ -34,10 +40,12 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
|
||||
this.location = new Set(json.location)
|
||||
|
||||
this.location.forEach(l => {
|
||||
this.location.forEach((l) => {
|
||||
const allowed = PointRenderingConfig.allowed_location_codes
|
||||
if (!allowed.has(l)) {
|
||||
throw `A point rendering has an invalid location: '${l}' is not one of ${Array.from(allowed).join(", ")} (at ${context}.location)`
|
||||
throw `A point rendering has an invalid location: '${l}' is not one of ${Array.from(
|
||||
allowed
|
||||
).join(", ")} (at ${context}.location)`
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -46,36 +54,39 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
}
|
||||
|
||||
if (this.location.size == 0) {
|
||||
throw "A pointRendering should have at least one 'location' to defined where it should be rendered. (At " + context + ".location)"
|
||||
throw (
|
||||
"A pointRendering should have at least one 'location' to defined where it should be rendered. (At " +
|
||||
context +
|
||||
".location)"
|
||||
)
|
||||
}
|
||||
this.icon = this.tr("icon", undefined);
|
||||
this.icon = this.tr("icon", undefined)
|
||||
this.iconBadges = (json.iconBadges ?? []).map((overlay, i) => {
|
||||
let tr: TagRenderingConfig;
|
||||
if (typeof overlay.then === "string" &&
|
||||
SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined) {
|
||||
tr = SharedTagRenderings.SharedIcons.get(overlay.then);
|
||||
let tr: TagRenderingConfig
|
||||
if (
|
||||
typeof overlay.then === "string" &&
|
||||
SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined
|
||||
) {
|
||||
tr = SharedTagRenderings.SharedIcons.get(overlay.then)
|
||||
} else {
|
||||
tr = new TagRenderingConfig(
|
||||
overlay.then,
|
||||
`iconBadges.${i}`
|
||||
);
|
||||
tr = new TagRenderingConfig(overlay.then, `iconBadges.${i}`)
|
||||
}
|
||||
return {
|
||||
if: TagUtils.Tag(overlay.if),
|
||||
then: tr
|
||||
};
|
||||
});
|
||||
then: tr,
|
||||
}
|
||||
})
|
||||
|
||||
const iconPath = this.icon?.GetRenderValue({id: "node/-1"})?.txt;
|
||||
const iconPath = this.icon?.GetRenderValue({ id: "node/-1" })?.txt
|
||||
if (iconPath !== undefined && iconPath.startsWith(Utils.assets_path)) {
|
||||
const iconKey = iconPath.substr(Utils.assets_path.length);
|
||||
const iconKey = iconPath.substr(Utils.assets_path.length)
|
||||
if (Svg.All[iconKey] === undefined) {
|
||||
throw context + ": builtin SVG asset not found: " + iconPath;
|
||||
throw context + ": builtin SVG asset not found: " + iconPath
|
||||
}
|
||||
}
|
||||
this.iconSize = this.tr("iconSize", "40,40,center");
|
||||
this.label = this.tr("label", undefined);
|
||||
this.rotation = this.tr("rotation", "0");
|
||||
this.iconSize = this.tr("iconSize", "40,40,center")
|
||||
this.label = this.tr("label", undefined)
|
||||
this.rotation = this.tr("rotation", "0")
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,40 +95,47 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
*/
|
||||
private static FromHtmlSpec(htmlSpec: string, style: string, isBadge = false): BaseUIElement {
|
||||
if (htmlSpec === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const match = htmlSpec.match(/([a-zA-Z0-9_]*):([^;]*)/);
|
||||
const match = htmlSpec.match(/([a-zA-Z0-9_]*):([^;]*)/)
|
||||
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
||||
const svg = (Svg.All[match[1] + ".svg"] as string)
|
||||
const svg = Svg.All[match[1] + ".svg"] as string
|
||||
const targetColor = match[2]
|
||||
const img = new Img(svg
|
||||
.replace(/(rgb\(0%,0%,0%\)|#000000|#000)/g, targetColor), true)
|
||||
.SetStyle(style)
|
||||
const img = new Img(
|
||||
svg.replace(/(rgb\(0%,0%,0%\)|#000000|#000)/g, targetColor),
|
||||
true
|
||||
).SetStyle(style)
|
||||
if (isBadge) {
|
||||
img.SetClass("badge")
|
||||
}
|
||||
return img
|
||||
} else if (Svg.All[htmlSpec + ".svg"] !== undefined) {
|
||||
const svg = (Svg.All[htmlSpec + ".svg"] as string)
|
||||
const img = new Img(svg, true)
|
||||
.SetStyle(style)
|
||||
const svg = Svg.All[htmlSpec + ".svg"] as string
|
||||
const img = new Img(svg, true).SetStyle(style)
|
||||
if (isBadge) {
|
||||
img.SetClass("badge")
|
||||
}
|
||||
return img
|
||||
} else {
|
||||
return new FixedUiElement(`<img src="${htmlSpec}" style="${style}" />`);
|
||||
return new FixedUiElement(`<img src="${htmlSpec}" style="${style}" />`)
|
||||
}
|
||||
}
|
||||
|
||||
private static FromHtmlMulti(multiSpec: string, rotation: string, isBadge: boolean, defaultElement: BaseUIElement = undefined) {
|
||||
private static FromHtmlMulti(
|
||||
multiSpec: string,
|
||||
rotation: string,
|
||||
isBadge: boolean,
|
||||
defaultElement: BaseUIElement = undefined
|
||||
) {
|
||||
if (multiSpec === undefined) {
|
||||
return defaultElement
|
||||
}
|
||||
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
|
||||
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`
|
||||
|
||||
const htmlDefs = multiSpec.trim()?.split(";") ?? []
|
||||
const elements = Utils.NoEmpty(htmlDefs).map(def => PointRenderingConfig.FromHtmlSpec(def, style, isBadge))
|
||||
const elements = Utils.NoEmpty(htmlDefs).map((def) =>
|
||||
PointRenderingConfig.FromHtmlSpec(def, style, isBadge)
|
||||
)
|
||||
if (elements.length === 0) {
|
||||
return defaultElement
|
||||
} else {
|
||||
|
@ -126,93 +144,95 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
}
|
||||
|
||||
public GetBaseIcon(tags?: any): BaseUIElement {
|
||||
tags = tags ?? {id: "node/-1"}
|
||||
tags = tags ?? { id: "node/-1" }
|
||||
let defaultPin: BaseUIElement = undefined
|
||||
if (this.label === undefined) {
|
||||
defaultPin = Svg.teardrop_with_hole_green_svg()
|
||||
}
|
||||
if(this.icon === undefined){
|
||||
return defaultPin;
|
||||
if (this.icon === undefined) {
|
||||
return defaultPin
|
||||
}
|
||||
const rotation = Utils.SubstituteKeys(this.rotation?.GetRenderValue(tags)?.txt ?? "0deg", tags)
|
||||
const rotation = Utils.SubstituteKeys(
|
||||
this.rotation?.GetRenderValue(tags)?.txt ?? "0deg",
|
||||
tags
|
||||
)
|
||||
const htmlDefs = Utils.SubstituteKeys(this.icon?.GetRenderValue(tags)?.txt, tags)
|
||||
if(htmlDefs === undefined){
|
||||
if (htmlDefs === undefined) {
|
||||
// This layer doesn't want to show an icon right now
|
||||
return undefined
|
||||
}
|
||||
return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin)
|
||||
return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin)
|
||||
}
|
||||
|
||||
public GetSimpleIcon(tags: UIEventSource<any>): BaseUIElement {
|
||||
const self = this;
|
||||
const self = this
|
||||
if (this.icon === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return new VariableUiElement(tags.map(tags => self.GetBaseIcon(tags))).SetClass("w-full h-full block")
|
||||
return new VariableUiElement(tags.map((tags) => self.GetBaseIcon(tags))).SetClass(
|
||||
"w-full h-full block"
|
||||
)
|
||||
}
|
||||
|
||||
public GenerateLeafletStyle(
|
||||
tags: UIEventSource<any>,
|
||||
clickable: boolean,
|
||||
options?: {
|
||||
noSize?: false | boolean,
|
||||
noSize?: false | boolean
|
||||
includeBadges?: true | boolean
|
||||
}
|
||||
):
|
||||
{
|
||||
html: BaseUIElement;
|
||||
iconSize: [number, number];
|
||||
iconAnchor: [number, number];
|
||||
popupAnchor: [number, number];
|
||||
iconUrl: string;
|
||||
className: string;
|
||||
} {
|
||||
): {
|
||||
html: BaseUIElement
|
||||
iconSize: [number, number]
|
||||
iconAnchor: [number, number]
|
||||
popupAnchor: [number, number]
|
||||
iconUrl: string
|
||||
className: string
|
||||
} {
|
||||
function num(str, deflt = 40) {
|
||||
const n = Number(str);
|
||||
const n = Number(str)
|
||||
if (isNaN(n)) {
|
||||
return deflt;
|
||||
return deflt
|
||||
}
|
||||
return n;
|
||||
return n
|
||||
}
|
||||
|
||||
function render(tr: TagRenderingConfig, deflt?: string) {
|
||||
if (tags === undefined) {
|
||||
return deflt
|
||||
}
|
||||
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt;
|
||||
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
|
||||
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt
|
||||
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "")
|
||||
}
|
||||
|
||||
const iconSize = render(this.iconSize, "40,40,center").split(",");
|
||||
const iconSize = render(this.iconSize, "40,40,center").split(",")
|
||||
|
||||
const iconW = num(iconSize[0]);
|
||||
let iconH = num(iconSize[1]);
|
||||
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center";
|
||||
const iconW = num(iconSize[0])
|
||||
let iconH = num(iconSize[1])
|
||||
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center"
|
||||
|
||||
let anchorW = iconW / 2;
|
||||
let anchorH = iconH / 2;
|
||||
let anchorW = iconW / 2
|
||||
let anchorH = iconH / 2
|
||||
if (mode === "left") {
|
||||
anchorW = 0;
|
||||
anchorW = 0
|
||||
}
|
||||
if (mode === "right") {
|
||||
anchorW = iconW;
|
||||
anchorW = iconW
|
||||
}
|
||||
|
||||
if (mode === "top") {
|
||||
anchorH = 0;
|
||||
anchorH = 0
|
||||
}
|
||||
if (mode === "bottom") {
|
||||
anchorH = iconH;
|
||||
anchorH = iconH
|
||||
}
|
||||
|
||||
|
||||
const icon = this.GetSimpleIcon(tags)
|
||||
let badges = undefined;
|
||||
let badges = undefined
|
||||
if (options?.includeBadges ?? true) {
|
||||
badges = this.GetBadges(tags)
|
||||
}
|
||||
const iconAndBadges = new Combine([icon, badges])
|
||||
.SetClass("block relative")
|
||||
const iconAndBadges = new Combine([icon, badges]).SetClass("block relative")
|
||||
|
||||
if (!options?.noSize) {
|
||||
iconAndBadges.SetStyle(`width: ${iconW}px; height: ${iconH}px`)
|
||||
|
@ -221,7 +241,7 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
}
|
||||
|
||||
let label = this.GetLabel(tags)
|
||||
let htmlEl: BaseUIElement;
|
||||
let htmlEl: BaseUIElement
|
||||
if (icon === undefined && label === undefined) {
|
||||
htmlEl = undefined
|
||||
} else if (icon === undefined) {
|
||||
|
@ -238,10 +258,8 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
iconAnchor: [anchorW, anchorH],
|
||||
popupAnchor: [0, 3 - anchorH],
|
||||
iconUrl: undefined,
|
||||
className: clickable
|
||||
? "leaflet-div-icon"
|
||||
: "leaflet-div-icon unclickable",
|
||||
};
|
||||
className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable",
|
||||
}
|
||||
}
|
||||
|
||||
private GetBadges(tags: UIEventSource<any>): BaseUIElement {
|
||||
|
@ -249,41 +267,46 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
return undefined
|
||||
}
|
||||
return new VariableUiElement(
|
||||
tags.map(tags => {
|
||||
|
||||
const badgeElements = this.iconBadges.map(badge => {
|
||||
|
||||
tags.map((tags) => {
|
||||
const badgeElements = this.iconBadges.map((badge) => {
|
||||
if (!badge.if.matchesProperties(tags)) {
|
||||
// Doesn't match...
|
||||
return undefined
|
||||
}
|
||||
|
||||
const htmlDefs = Utils.SubstituteKeys(badge.then.GetRenderValue(tags)?.txt, tags)
|
||||
const badgeElement = PointRenderingConfig.FromHtmlMulti(htmlDefs, "0", true)?.SetClass("block relative")
|
||||
const htmlDefs = Utils.SubstituteKeys(
|
||||
badge.then.GetRenderValue(tags)?.txt,
|
||||
tags
|
||||
)
|
||||
const badgeElement = PointRenderingConfig.FromHtmlMulti(
|
||||
htmlDefs,
|
||||
"0",
|
||||
true
|
||||
)?.SetClass("block relative")
|
||||
if (badgeElement === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return new Combine([badgeElement]).SetStyle("width: 1.5rem").SetClass("block")
|
||||
|
||||
})
|
||||
|
||||
return new Combine(badgeElements).SetClass("inline-flex h-full")
|
||||
})).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0")
|
||||
})
|
||||
).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0")
|
||||
}
|
||||
|
||||
private GetLabel(tags: UIEventSource<any>): BaseUIElement {
|
||||
if (this.label === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const self = this;
|
||||
return new VariableUiElement(tags.map(tags => {
|
||||
const label = self.label
|
||||
?.GetRenderValue(tags)
|
||||
?.Subs(tags)
|
||||
?.SetClass("block text-center")
|
||||
return new Combine([label]).SetClass("flex flex-col items-center mt-1")
|
||||
}))
|
||||
|
||||
const self = this
|
||||
return new VariableUiElement(
|
||||
tags.map((tags) => {
|
||||
const label = self.label
|
||||
?.GetRenderValue(tags)
|
||||
?.Subs(tags)
|
||||
?.SetClass("block text-center")
|
||||
return new Combine([label]).SetClass("flex flex-col items-center mt-1")
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import {Translation} from "../../UI/i18n/Translation";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import { Translation } from "../../UI/i18n/Translation"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
|
||||
export interface PreciseInput {
|
||||
preferredBackground?: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[],
|
||||
snapToLayers?: string[],
|
||||
preferredBackground?: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[]
|
||||
snapToLayers?: string[]
|
||||
maxSnapDistance?: number
|
||||
}
|
||||
|
||||
export default interface PresetConfig {
|
||||
title: Translation,
|
||||
tags: Tag[],
|
||||
description?: Translation,
|
||||
exampleImages?: string[],
|
||||
title: Translation
|
||||
tags: Tag[]
|
||||
description?: Translation
|
||||
exampleImages?: string[]
|
||||
/**
|
||||
* If precise input is set, then an extra map is shown in which the user can drag the map to the precise location
|
||||
*/
|
||||
preciseInput?: PreciseInput
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +1,79 @@
|
|||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import {RegexTag} from "../../Logic/Tags/RegexTag";
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||
import { RegexTag } from "../../Logic/Tags/RegexTag"
|
||||
|
||||
export default class SourceConfig {
|
||||
public readonly osmTags?: TagsFilter
|
||||
public readonly overpassScript?: string
|
||||
public geojsonSource?: string
|
||||
public geojsonZoomLevel?: number
|
||||
public isOsmCacheLayer: boolean
|
||||
public readonly mercatorCrs: boolean
|
||||
public readonly idKey: string
|
||||
|
||||
public readonly osmTags?: TagsFilter;
|
||||
public readonly overpassScript?: string;
|
||||
public geojsonSource?: string;
|
||||
public geojsonZoomLevel?: number;
|
||||
public isOsmCacheLayer: boolean;
|
||||
public readonly mercatorCrs: boolean;
|
||||
public readonly idKey : string
|
||||
|
||||
constructor(params: {
|
||||
mercatorCrs?: boolean;
|
||||
osmTags?: TagsFilter,
|
||||
overpassScript?: string,
|
||||
geojsonSource?: string,
|
||||
isOsmCache?: boolean,
|
||||
geojsonSourceLevel?: number,
|
||||
idKey?: string
|
||||
}, isSpecialLayer: boolean, context?: string) {
|
||||
|
||||
let defined = 0;
|
||||
constructor(
|
||||
params: {
|
||||
mercatorCrs?: boolean
|
||||
osmTags?: TagsFilter
|
||||
overpassScript?: string
|
||||
geojsonSource?: string
|
||||
isOsmCache?: boolean
|
||||
geojsonSourceLevel?: number
|
||||
idKey?: string
|
||||
},
|
||||
isSpecialLayer: boolean,
|
||||
context?: string
|
||||
) {
|
||||
let defined = 0
|
||||
if (params.osmTags) {
|
||||
defined++;
|
||||
defined++
|
||||
}
|
||||
if (params.overpassScript) {
|
||||
defined++;
|
||||
defined++
|
||||
}
|
||||
if (params.geojsonSource) {
|
||||
defined++;
|
||||
defined++
|
||||
}
|
||||
if (defined == 0) {
|
||||
throw `Source: nothing correct defined in the source (in ${context}) (the params are ${JSON.stringify(params)})`
|
||||
throw `Source: nothing correct defined in the source (in ${context}) (the params are ${JSON.stringify(
|
||||
params
|
||||
)})`
|
||||
}
|
||||
if (params.isOsmCache && params.geojsonSource == undefined) {
|
||||
console.error(params)
|
||||
throw `Source said it is a OSM-cached layer, but didn't define the actual source of the cache (in context ${context})`
|
||||
}
|
||||
if (params.geojsonSource !== undefined && params.geojsonSourceLevel !== undefined) {
|
||||
if (!["x", "y", "x_min", "x_max", "y_min", "Y_max"].some(toSearch => params.geojsonSource.indexOf(toSearch) > 0)) {
|
||||
if (
|
||||
!["x", "y", "x_min", "x_max", "y_min", "Y_max"].some(
|
||||
(toSearch) => params.geojsonSource.indexOf(toSearch) > 0
|
||||
)
|
||||
) {
|
||||
throw `Source defines a geojson-zoomLevel, but does not specify {x} nor {y} (or equivalent), this is probably a bug (in context ${context})`
|
||||
}
|
||||
}
|
||||
if(params.osmTags !== undefined && !isSpecialLayer){
|
||||
if (params.osmTags !== undefined && !isSpecialLayer) {
|
||||
const optimized = params.osmTags.optimize()
|
||||
if(optimized === false){
|
||||
throw "Error at "+context+": the specified tags are conflicting with each other: they will never match anything at all"
|
||||
if (optimized === false) {
|
||||
throw (
|
||||
"Error at " +
|
||||
context +
|
||||
": the specified tags are conflicting with each other: they will never match anything at all"
|
||||
)
|
||||
}
|
||||
if(optimized === true){
|
||||
throw "Error at "+context+": the specified tags are very wide: they will always match everything"
|
||||
if (optimized === true) {
|
||||
throw (
|
||||
"Error at " +
|
||||
context +
|
||||
": the specified tags are very wide: they will always match everything"
|
||||
)
|
||||
}
|
||||
}
|
||||
this.osmTags = params.osmTags ?? new RegexTag("id", /.*/);
|
||||
this.overpassScript = params.overpassScript;
|
||||
this.geojsonSource = params.geojsonSource;
|
||||
this.geojsonZoomLevel = params.geojsonSourceLevel;
|
||||
this.isOsmCacheLayer = params.isOsmCache ?? false;
|
||||
this.mercatorCrs = params.mercatorCrs ?? false;
|
||||
this.idKey= params.idKey
|
||||
this.osmTags = params.osmTags ?? new RegexTag("id", /.*/)
|
||||
this.overpassScript = params.overpassScript
|
||||
this.geojsonSource = params.geojsonSource
|
||||
this.geojsonZoomLevel = params.geojsonSourceLevel
|
||||
this.isOsmCacheLayer = params.isOsmCache ?? false
|
||||
this.mercatorCrs = params.mercatorCrs ?? false
|
||||
this.idKey = params.idKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,39 @@
|
|||
import {Translation, TypedTranslation} from "../../UI/i18n/Translation";
|
||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import {TagUtils, UploadableTag} from "../../Logic/Tags/TagUtils";
|
||||
import {And} from "../../Logic/Tags/And";
|
||||
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
|
||||
import {Utils} from "../../Utils";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import Combine from "../../UI/Base/Combine";
|
||||
import Title from "../../UI/Base/Title";
|
||||
import Link from "../../UI/Base/Link";
|
||||
import List from "../../UI/Base/List";
|
||||
import {MappingConfigJson, QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson";
|
||||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||
import {Paragraph} from "../../UI/Base/Paragraph";
|
||||
import { Translation, TypedTranslation } from "../../UI/i18n/Translation"
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils"
|
||||
import { And } from "../../Logic/Tags/And"
|
||||
import ValidatedTextField from "../../UI/Input/ValidatedTextField"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import Combine from "../../UI/Base/Combine"
|
||||
import Title from "../../UI/Base/Title"
|
||||
import Link from "../../UI/Base/Link"
|
||||
import List from "../../UI/Base/List"
|
||||
import {
|
||||
MappingConfigJson,
|
||||
QuestionableTagRenderingConfigJson,
|
||||
} from "./Json/QuestionableTagRenderingConfigJson"
|
||||
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
|
||||
import { Paragraph } from "../../UI/Base/Paragraph"
|
||||
|
||||
export interface Mapping {
|
||||
readonly if: UploadableTag,
|
||||
readonly ifnot?: UploadableTag,
|
||||
readonly then: TypedTranslation<object>,
|
||||
readonly icon: string,
|
||||
readonly iconClass: string | "small" | "medium" | "large" | "small-height" | "medium-height" | "large-height",
|
||||
readonly if: UploadableTag
|
||||
readonly ifnot?: UploadableTag
|
||||
readonly then: TypedTranslation<object>
|
||||
readonly icon: string
|
||||
readonly iconClass:
|
||||
| string
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "small-height"
|
||||
| "medium-height"
|
||||
| "large-height"
|
||||
readonly hideInAnswer: boolean | TagsFilter
|
||||
readonly addExtraTags: Tag[],
|
||||
readonly searchTerms?: Record<string, string[]>,
|
||||
readonly addExtraTags: Tag[]
|
||||
readonly searchTerms?: Record<string, string[]>
|
||||
readonly priorityIf?: TagsFilter
|
||||
}
|
||||
|
||||
|
@ -32,52 +42,49 @@ export interface Mapping {
|
|||
* Identical data, but with some methods and validation
|
||||
*/
|
||||
export default class TagRenderingConfig {
|
||||
|
||||
public readonly id: string;
|
||||
public readonly group: string;
|
||||
public readonly render?: TypedTranslation<object>;
|
||||
public readonly question?: TypedTranslation<object>;
|
||||
public readonly condition?: TagsFilter;
|
||||
public readonly description?: Translation;
|
||||
public readonly id: string
|
||||
public readonly group: string
|
||||
public readonly render?: TypedTranslation<object>
|
||||
public readonly question?: TypedTranslation<object>
|
||||
public readonly condition?: TagsFilter
|
||||
public readonly description?: Translation
|
||||
|
||||
public readonly configuration_warnings: string[] = []
|
||||
|
||||
public readonly freeform?: {
|
||||
readonly key: string,
|
||||
readonly type: string,
|
||||
readonly placeholder: Translation,
|
||||
readonly addExtraTags: UploadableTag[];
|
||||
readonly inline: boolean,
|
||||
readonly default?: string,
|
||||
readonly key: string
|
||||
readonly type: string
|
||||
readonly placeholder: Translation
|
||||
readonly addExtraTags: UploadableTag[]
|
||||
readonly inline: boolean
|
||||
readonly default?: string
|
||||
readonly helperArgs?: (string | number | boolean)[]
|
||||
};
|
||||
}
|
||||
|
||||
public readonly multiAnswer: boolean;
|
||||
public readonly multiAnswer: boolean
|
||||
|
||||
public readonly mappings?: Mapping[]
|
||||
public readonly labels: string[]
|
||||
|
||||
|
||||
constructor(json: string | QuestionableTagRenderingConfigJson, context?: string) {
|
||||
if (json === undefined) {
|
||||
throw "Initing a TagRenderingConfig with undefined in " + context;
|
||||
throw "Initing a TagRenderingConfig with undefined in " + context
|
||||
}
|
||||
if (json === "questions") {
|
||||
// Very special value
|
||||
this.render = null;
|
||||
this.question = null;
|
||||
this.condition = null;
|
||||
this.render = null
|
||||
this.question = null
|
||||
this.condition = null
|
||||
this.id = "questions"
|
||||
this.group = ""
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (typeof json === "number") {
|
||||
json = "" + json
|
||||
}
|
||||
|
||||
let translationKey = context;
|
||||
let translationKey = context
|
||||
if (json["id"] !== undefined) {
|
||||
const layerId = context.split(".")[0]
|
||||
if (json["source"]) {
|
||||
|
@ -91,43 +98,54 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
if (typeof json === "string") {
|
||||
this.render = Translations.T(json, translationKey + ".render");
|
||||
this.multiAnswer = false;
|
||||
return;
|
||||
this.render = Translations.T(json, translationKey + ".render")
|
||||
this.multiAnswer = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
this.id = json.id ?? ""; // Some tagrenderings - especially for the map rendering - don't need an ID
|
||||
this.id = json.id ?? "" // Some tagrenderings - especially for the map rendering - don't need an ID
|
||||
if (this.id.match(/^[a-zA-Z0-9 ()?\/=:;,_-]*$/) === null) {
|
||||
throw "Invalid ID in " + context + ": an id can only contain [a-zA-Z0-0_-] as characters. The offending id is: " + this.id
|
||||
throw (
|
||||
"Invalid ID in " +
|
||||
context +
|
||||
": an id can only contain [a-zA-Z0-0_-] as characters. The offending id is: " +
|
||||
this.id
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
this.group = json.group ?? "";
|
||||
this.group = json.group ?? ""
|
||||
this.labels = json.labels ?? []
|
||||
this.render = Translations.T(json.render, translationKey + ".render");
|
||||
this.question = Translations.T(json.question, translationKey + ".question");
|
||||
this.description = Translations.T(json.description, translationKey + ".description");
|
||||
this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`);
|
||||
this.render = Translations.T(json.render, translationKey + ".render")
|
||||
this.question = Translations.T(json.question, translationKey + ".question")
|
||||
this.description = Translations.T(json.description, translationKey + ".description")
|
||||
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
|
||||
if (json.freeform) {
|
||||
|
||||
if (json.freeform.addExtraTags !== undefined && json.freeform.addExtraTags.map === undefined) {
|
||||
if (
|
||||
json.freeform.addExtraTags !== undefined &&
|
||||
json.freeform.addExtraTags.map === undefined
|
||||
) {
|
||||
throw `Freeform.addExtraTags should be a list of strings - not a single string (at ${context})`
|
||||
}
|
||||
const type = json.freeform.type ?? "string"
|
||||
|
||||
if (ValidatedTextField.AvailableTypes().indexOf(type) < 0) {
|
||||
throw "At " + context + ".freeform.type is an unknown type: " + type + "; try one of " + ValidatedTextField.AvailableTypes().join(", ")
|
||||
throw (
|
||||
"At " +
|
||||
context +
|
||||
".freeform.type is an unknown type: " +
|
||||
type +
|
||||
"; try one of " +
|
||||
ValidatedTextField.AvailableTypes().join(", ")
|
||||
)
|
||||
}
|
||||
|
||||
let placeholder: Translation = Translations.T(json.freeform.placeholder)
|
||||
if (placeholder === undefined) {
|
||||
const typeDescription = <Translation>Translations.t.validation[type]?.description
|
||||
const key = json.freeform.key;
|
||||
const key = json.freeform.key
|
||||
if (typeDescription !== undefined) {
|
||||
placeholder = typeDescription.OnEveryLanguage(l => key + " (" + l + ")")
|
||||
placeholder = typeDescription.OnEveryLanguage((l) => key + " (" + l + ")")
|
||||
} else {
|
||||
placeholder = Translations.T(key + " (" + type + ")")
|
||||
}
|
||||
|
@ -137,12 +155,13 @@ export default class TagRenderingConfig {
|
|||
key: json.freeform.key,
|
||||
type,
|
||||
placeholder,
|
||||
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
|
||||
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)) ?? [],
|
||||
addExtraTags:
|
||||
json.freeform.addExtraTags?.map((tg, i) =>
|
||||
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)
|
||||
) ?? [],
|
||||
inline: json.freeform.inline ?? false,
|
||||
default: json.freeform.default,
|
||||
helperArgs: json.freeform.helperArgs
|
||||
|
||||
helperArgs: json.freeform.helperArgs,
|
||||
}
|
||||
if (json.freeform["extraTags"] !== undefined) {
|
||||
throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})`
|
||||
|
@ -152,7 +171,6 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
if (json.freeform["args"] !== undefined) {
|
||||
throw `Freeform.args is defined. This should probably be 'freeform.helperArgs' (at ${context})`
|
||||
|
||||
}
|
||||
|
||||
if (json.freeform.key === "questions") {
|
||||
|
@ -161,28 +179,42 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.freeform.type !== undefined && ValidatedTextField.AvailableTypes().indexOf(this.freeform.type) < 0) {
|
||||
const knownKeys = ValidatedTextField.AvailableTypes().join(", ");
|
||||
if (
|
||||
this.freeform.type !== undefined &&
|
||||
ValidatedTextField.AvailableTypes().indexOf(this.freeform.type) < 0
|
||||
) {
|
||||
const knownKeys = ValidatedTextField.AvailableTypes().join(", ")
|
||||
throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}`
|
||||
}
|
||||
if (this.freeform.addExtraTags) {
|
||||
const usedKeys = new And(this.freeform.addExtraTags).usedKeys();
|
||||
const usedKeys = new And(this.freeform.addExtraTags).usedKeys()
|
||||
if (usedKeys.indexOf(this.freeform.key) >= 0) {
|
||||
throw `The freeform key ${this.freeform.key} will be overwritten by one of the extra tags, as they use the same key too. This is in ${context}`;
|
||||
throw `The freeform key ${this.freeform.key} will be overwritten by one of the extra tags, as they use the same key too. This is in ${context}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.multiAnswer = json.multiAnswer ?? false
|
||||
if (json.mappings) {
|
||||
|
||||
if (!Array.isArray(json.mappings)) {
|
||||
throw "Tagrendering has a 'mappings'-object, but expected a list (" + context + ")"
|
||||
}
|
||||
|
||||
const commonIconSize = Utils.NoNull(json.mappings.map(m => m.icon !== undefined ? m.icon["class"] : undefined))[0] ?? "small"
|
||||
this.mappings = json.mappings.map((m, i) => TagRenderingConfig.ExtractMapping(m, i, translationKey, context, this.multiAnswer, this.question !== undefined, commonIconSize));
|
||||
const commonIconSize =
|
||||
Utils.NoNull(
|
||||
json.mappings.map((m) => (m.icon !== undefined ? m.icon["class"] : undefined))
|
||||
)[0] ?? "small"
|
||||
this.mappings = json.mappings.map((m, i) =>
|
||||
TagRenderingConfig.ExtractMapping(
|
||||
m,
|
||||
i,
|
||||
translationKey,
|
||||
context,
|
||||
this.multiAnswer,
|
||||
this.question !== undefined,
|
||||
commonIconSize
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (this.question && this.freeform?.key === undefined && this.mappings === undefined) {
|
||||
|
@ -196,14 +228,12 @@ export default class TagRenderingConfig {
|
|||
continue
|
||||
}
|
||||
throw `${context}: The rendering for language ${ln} does not contain {questions}. This is a bug, as this rendering should include exactly this to trigger those questions to be shown!`
|
||||
|
||||
}
|
||||
if (this.freeform?.key !== undefined && this.freeform?.key !== "questions") {
|
||||
throw `${context}: If the ID is questions to trigger a question box, the only valid freeform value is 'questions' as well. Set freeform to questions or remove the freeform all together`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.freeform) {
|
||||
if (this.render === undefined) {
|
||||
throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}`
|
||||
|
@ -222,21 +252,25 @@ export default class TagRenderingConfig {
|
|||
if (txt.indexOf("{canonical(" + this.freeform.key + ")") >= 0) {
|
||||
continue
|
||||
}
|
||||
if (this.freeform.type === "opening_hours" && txt.indexOf("{opening_hours_table(") >= 0) {
|
||||
if (
|
||||
this.freeform.type === "opening_hours" &&
|
||||
txt.indexOf("{opening_hours_table(") >= 0
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (this.freeform.type === "wikidata" && txt.indexOf("{wikipedia(" + this.freeform.key) >= 0) {
|
||||
if (
|
||||
this.freeform.type === "wikidata" &&
|
||||
txt.indexOf("{wikipedia(" + this.freeform.key) >= 0
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (this.freeform.key === "wikidata" && txt.indexOf("{wikipedia()") >= 0) {
|
||||
continue
|
||||
}
|
||||
throw `${context}: The rendering for language ${ln} does not contain the freeform key {${this.freeform.key}}. This is a bug, as this rendering should show exactly this freeform key!\nThe rendering is ${txt} `
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.render && this.question && this.freeform === undefined) {
|
||||
throw `${context}: Detected a tagrendering which takes input without freeform key in ${context}; the question is ${this.question.txt}`
|
||||
}
|
||||
|
@ -244,7 +278,7 @@ export default class TagRenderingConfig {
|
|||
if (!json.multiAnswer && this.mappings !== undefined && this.question !== undefined) {
|
||||
let keys = []
|
||||
for (let i = 0; i < this.mappings.length; i++) {
|
||||
const mapping = this.mappings[i];
|
||||
const mapping = this.mappings[i]
|
||||
if (mapping.if === undefined) {
|
||||
throw `${context}.mappings[${i}].if is undefined`
|
||||
}
|
||||
|
@ -252,15 +286,17 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
keys = Utils.Dedup(keys)
|
||||
for (let i = 0; i < this.mappings.length; i++) {
|
||||
const mapping = this.mappings[i];
|
||||
const mapping = this.mappings[i]
|
||||
if (mapping.hideInAnswer) {
|
||||
continue
|
||||
}
|
||||
|
||||
const usedKeys = mapping.if.usedKeys();
|
||||
const usedKeys = mapping.if.usedKeys()
|
||||
for (const expectedKey of keys) {
|
||||
if (usedKeys.indexOf(expectedKey) < 0) {
|
||||
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(', ')}, but it should also give a value for ${expectedKey}`
|
||||
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(
|
||||
", "
|
||||
)}, but it should also give a value for ${expectedKey}`
|
||||
this.configuration_warnings.push(msg)
|
||||
}
|
||||
}
|
||||
|
@ -272,22 +308,21 @@ export default class TagRenderingConfig {
|
|||
throw `${context} MultiAnswer is set, but no mappings are defined`
|
||||
}
|
||||
|
||||
let allKeys = [];
|
||||
let allHaveIfNot = true;
|
||||
let allKeys = []
|
||||
let allHaveIfNot = true
|
||||
for (const mapping of this.mappings) {
|
||||
if (mapping.hideInAnswer) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (mapping.ifnot === undefined) {
|
||||
allHaveIfNot = false;
|
||||
allHaveIfNot = false
|
||||
}
|
||||
allKeys = allKeys.concat(mapping.if.usedKeys());
|
||||
allKeys = allKeys.concat(mapping.if.usedKeys())
|
||||
}
|
||||
allKeys = Utils.Dedup(allKeys);
|
||||
allKeys = Utils.Dedup(allKeys)
|
||||
if (allKeys.length > 1 && !allHaveIfNot) {
|
||||
throw `${context}: A multi-answer is defined, which generates values over multiple keys. Please define ifnot-tags too on every mapping`
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -296,17 +331,24 @@ export default class TagRenderingConfig {
|
|||
* tr.if // => new Tag("a","b")
|
||||
* tr.priorityIf // => new Tag("_country","be")
|
||||
*/
|
||||
public static ExtractMapping(mapping: MappingConfigJson, i: number, translationKey: string,
|
||||
context: string,
|
||||
multiAnswer?: boolean, isQuestionable?: boolean, commonSize: string = "small") {
|
||||
|
||||
public static ExtractMapping(
|
||||
mapping: MappingConfigJson,
|
||||
i: number,
|
||||
translationKey: string,
|
||||
context: string,
|
||||
multiAnswer?: boolean,
|
||||
isQuestionable?: boolean,
|
||||
commonSize: string = "small"
|
||||
) {
|
||||
const ctx = `${translationKey}.mappings.${i}`
|
||||
if (mapping.if === undefined) {
|
||||
throw `${ctx}: Invalid mapping: "if" is not defined in ${JSON.stringify(mapping)}`
|
||||
}
|
||||
if (mapping.then === undefined) {
|
||||
if (mapping["render"] !== undefined) {
|
||||
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(mapping)}`
|
||||
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(
|
||||
mapping
|
||||
)}`
|
||||
}
|
||||
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
|
||||
}
|
||||
|
@ -315,7 +357,9 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
|
||||
if (mapping["render"] !== undefined) {
|
||||
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(mapping)}`
|
||||
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(
|
||||
mapping
|
||||
)}`
|
||||
}
|
||||
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
|
||||
throw `${ctx}: Invalid mapping: "if" is defined as an array. Use {"and": <your conditions>} or {"or": <your conditions>} instead`
|
||||
|
@ -325,18 +369,23 @@ export default class TagRenderingConfig {
|
|||
throw `${ctx}: Invalid mapping: got a multi-Answer with addExtraTags; this is not allowed`
|
||||
}
|
||||
|
||||
let hideInAnswer: boolean | TagsFilter = false;
|
||||
let hideInAnswer: boolean | TagsFilter = false
|
||||
if (typeof mapping.hideInAnswer === "boolean") {
|
||||
hideInAnswer = mapping.hideInAnswer;
|
||||
hideInAnswer = mapping.hideInAnswer
|
||||
} else if (mapping.hideInAnswer !== undefined) {
|
||||
hideInAnswer = TagUtils.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`);
|
||||
hideInAnswer = TagUtils.Tag(
|
||||
mapping.hideInAnswer,
|
||||
`${context}.mapping[${i}].hideInAnswer`
|
||||
)
|
||||
}
|
||||
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) => TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`));
|
||||
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) =>
|
||||
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`)
|
||||
)
|
||||
if (hideInAnswer === true && addExtraTags.length > 0) {
|
||||
throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.`
|
||||
}
|
||||
|
||||
let icon = undefined;
|
||||
let icon = undefined
|
||||
let iconClass = commonSize
|
||||
if (mapping.icon !== undefined) {
|
||||
if (typeof mapping.icon === "string" && mapping.icon !== "") {
|
||||
|
@ -346,18 +395,22 @@ export default class TagRenderingConfig {
|
|||
iconClass = mapping.icon["class"] ?? iconClass
|
||||
}
|
||||
}
|
||||
const prioritySearch = mapping.priorityIf !== undefined ? TagUtils.Tag(mapping.priorityIf) : undefined;
|
||||
const prioritySearch =
|
||||
mapping.priorityIf !== undefined ? TagUtils.Tag(mapping.priorityIf) : undefined
|
||||
const mp = <Mapping>{
|
||||
if: TagUtils.Tag(mapping.if, `${ctx}.if`),
|
||||
ifnot: (mapping.ifnot !== undefined ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) : undefined),
|
||||
ifnot:
|
||||
mapping.ifnot !== undefined
|
||||
? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`)
|
||||
: undefined,
|
||||
then: Translations.T(mapping.then, `${ctx}.then`),
|
||||
hideInAnswer,
|
||||
icon,
|
||||
iconClass,
|
||||
addExtraTags,
|
||||
searchTerms: mapping.searchTerms,
|
||||
priorityIf: prioritySearch
|
||||
};
|
||||
priorityIf: prioritySearch,
|
||||
}
|
||||
if (isQuestionable) {
|
||||
if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
|
||||
throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
|
||||
|
@ -368,7 +421,7 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
return mp;
|
||||
return mp
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -376,15 +429,14 @@ export default class TagRenderingConfig {
|
|||
* @constructor
|
||||
*/
|
||||
public IsKnown(tags: Record<string, string>): boolean {
|
||||
if (this.condition &&
|
||||
!this.condition.matchesProperties(tags)) {
|
||||
if (this.condition && !this.condition.matchesProperties(tags)) {
|
||||
// Filtered away by the condition, so it is kindof known
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
if (this.multiAnswer) {
|
||||
for (const m of this.mappings ?? []) {
|
||||
if (TagUtils.MatchesMultiAnswer(m.if, tags)) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -394,15 +446,14 @@ export default class TagRenderingConfig {
|
|||
return value !== undefined && value !== ""
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
if (this.GetRenderValue(tags) !== undefined) {
|
||||
// This value is known and can be rendered
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -411,39 +462,49 @@ export default class TagRenderingConfig {
|
|||
* @param tags
|
||||
* @constructor
|
||||
*/
|
||||
public GetRenderValues(tags: Record<string, string>): { then: Translation, icon?: string, iconClass?: string }[] {
|
||||
public GetRenderValues(
|
||||
tags: Record<string, string>
|
||||
): { then: Translation; icon?: string; iconClass?: string }[] {
|
||||
if (!this.multiAnswer) {
|
||||
return [this.GetRenderValueWithImage(tags)]
|
||||
}
|
||||
|
||||
// A flag to check that the freeform key isn't matched multiple times
|
||||
// A flag to check that the freeform key isn't matched multiple times
|
||||
// If it is undefined, it is "used" already, or at least we don't have to check for it anymore
|
||||
let freeformKeyDefined = this.freeform?.key !== undefined;
|
||||
let freeformKeyDefined = this.freeform?.key !== undefined
|
||||
let usedFreeformValues = new Set<string>()
|
||||
// We run over all the mappings first, to check if the mapping matches
|
||||
const applicableMappings: { then: TypedTranslation<Record<string, string>>, img?: string }[] = Utils.NoNull((this.mappings ?? [])?.map(mapping => {
|
||||
if (mapping.if === undefined) {
|
||||
return mapping;
|
||||
}
|
||||
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
|
||||
if (freeformKeyDefined && mapping.if.isUsableAsAnswer()) {
|
||||
// THe freeform key is defined: what value does it use though?
|
||||
// We mark the value to see if we have any leftovers
|
||||
const value = mapping.if.asChange({}).find(kv => kv.k === this.freeform.key).v
|
||||
usedFreeformValues.add(value)
|
||||
const applicableMappings: {
|
||||
then: TypedTranslation<Record<string, string>>
|
||||
img?: string
|
||||
}[] = Utils.NoNull(
|
||||
(this.mappings ?? [])?.map((mapping) => {
|
||||
if (mapping.if === undefined) {
|
||||
return mapping
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
return undefined;
|
||||
}))
|
||||
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
|
||||
if (freeformKeyDefined && mapping.if.isUsableAsAnswer()) {
|
||||
// THe freeform key is defined: what value does it use though?
|
||||
// We mark the value to see if we have any leftovers
|
||||
const value = mapping.if
|
||||
.asChange({})
|
||||
.find((kv) => kv.k === this.freeform.key).v
|
||||
usedFreeformValues.add(value)
|
||||
}
|
||||
return mapping
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
)
|
||||
|
||||
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
|
||||
const freeformValues = tags[this.freeform.key].split(";")
|
||||
const leftovers = freeformValues.filter(v => !usedFreeformValues.has(v))
|
||||
const leftovers = freeformValues.filter((v) => !usedFreeformValues.has(v))
|
||||
for (const leftover of leftovers) {
|
||||
applicableMappings.push({
|
||||
then:
|
||||
new TypedTranslation<object>(this.render.replace("{" + this.freeform.key + "}", leftover).translations)
|
||||
then: new TypedTranslation<object>(
|
||||
this.render.replace("{" + this.freeform.key + "}", leftover).translations
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -451,7 +512,10 @@ export default class TagRenderingConfig {
|
|||
return applicableMappings
|
||||
}
|
||||
|
||||
public GetRenderValue(tags: any, defltValue: any = undefined): TypedTranslation<any> | undefined {
|
||||
public GetRenderValue(
|
||||
tags: any,
|
||||
defltValue: any = undefined
|
||||
): TypedTranslation<any> | undefined {
|
||||
return this.GetRenderValueWithImage(tags, defltValue)?.then
|
||||
}
|
||||
|
||||
|
@ -460,7 +524,10 @@ export default class TagRenderingConfig {
|
|||
* Not compatible with multiAnswer - use GetRenderValueS instead in that case
|
||||
* @constructor
|
||||
*/
|
||||
public GetRenderValueWithImage(tags: any, defltValue: any = undefined): { then: TypedTranslation<any>, icon?: string } | undefined {
|
||||
public GetRenderValueWithImage(
|
||||
tags: any,
|
||||
defltValue: any = undefined
|
||||
): { then: TypedTranslation<any>; icon?: string } | undefined {
|
||||
if (this.condition !== undefined) {
|
||||
if (!this.condition.matchesProperties(tags)) {
|
||||
return undefined
|
||||
|
@ -470,22 +537,23 @@ export default class TagRenderingConfig {
|
|||
if (this.mappings !== undefined && !this.multiAnswer) {
|
||||
for (const mapping of this.mappings) {
|
||||
if (mapping.if === undefined) {
|
||||
return mapping;
|
||||
return mapping
|
||||
}
|
||||
if (mapping.if.matchesProperties(tags)) {
|
||||
return mapping;
|
||||
return mapping
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.id === "questions" ||
|
||||
if (
|
||||
this.id === "questions" ||
|
||||
this.freeform?.key === undefined ||
|
||||
tags[this.freeform.key] !== undefined
|
||||
) {
|
||||
return {then: this.render}
|
||||
return { then: this.render }
|
||||
}
|
||||
|
||||
return {then: defltValue};
|
||||
return { then: defltValue }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -498,52 +566,57 @@ export default class TagRenderingConfig {
|
|||
const translations: Translation[] = []
|
||||
for (const key in this) {
|
||||
if (!this.hasOwnProperty(key)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const o = this[key]
|
||||
if (o instanceof Translation) {
|
||||
translations.push(o)
|
||||
}
|
||||
}
|
||||
return translations;
|
||||
return translations
|
||||
}
|
||||
|
||||
FreeformValues(): { key: string, type?: string, values?: string [] } {
|
||||
FreeformValues(): { key: string; type?: string; values?: string[] } {
|
||||
try {
|
||||
|
||||
const key = this.freeform?.key
|
||||
const answerMappings = this.mappings?.filter(m => m.hideInAnswer !== true)
|
||||
const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true)
|
||||
if (key === undefined) {
|
||||
|
||||
let values: { k: string, v: string }[][] = Utils.NoNull(answerMappings?.map(m => m.if.asChange({})) ?? [])
|
||||
let values: { k: string; v: string }[][] = Utils.NoNull(
|
||||
answerMappings?.map((m) => m.if.asChange({})) ?? []
|
||||
)
|
||||
if (values.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const allKeys = values.map(arr => arr.map(o => o.k))
|
||||
let common = allKeys[0];
|
||||
const allKeys = values.map((arr) => arr.map((o) => o.k))
|
||||
let common = allKeys[0]
|
||||
for (const keyset of allKeys) {
|
||||
common = common.filter(item => keyset.indexOf(item) >= 0)
|
||||
common = common.filter((item) => keyset.indexOf(item) >= 0)
|
||||
}
|
||||
const commonKey = common[0]
|
||||
if (commonKey === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
key: commonKey,
|
||||
values: Utils.NoNull(values.map(arr => arr.filter(item => item.k === commonKey)[0]?.v))
|
||||
values: Utils.NoNull(
|
||||
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
|
||||
),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let values = Utils.NoNull(answerMappings?.map(m => m.if.asChange({}).filter(item => item.k === key)[0]?.v) ?? [])
|
||||
let values = Utils.NoNull(
|
||||
answerMappings?.map(
|
||||
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v
|
||||
) ?? []
|
||||
)
|
||||
if (values.length === undefined) {
|
||||
values = undefined
|
||||
}
|
||||
return {
|
||||
key,
|
||||
type: this.freeform.type,
|
||||
values
|
||||
values,
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not create FreeformValues for tagrendering", this.id)
|
||||
|
@ -552,80 +625,93 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
|
||||
GenerateDocumentation(): BaseUIElement {
|
||||
|
||||
let withRender: (BaseUIElement | string)[] = [];
|
||||
let withRender: (BaseUIElement | string)[] = []
|
||||
if (this.freeform?.key !== undefined) {
|
||||
withRender = [
|
||||
`This rendering asks information about the property `,
|
||||
Link.OsmWiki(this.freeform.key),
|
||||
new Paragraph(new Combine([
|
||||
"This is rendered with ",
|
||||
new FixedUiElement(this.render.txt).SetClass("literalcode bold")
|
||||
]))
|
||||
|
||||
new Paragraph(
|
||||
new Combine([
|
||||
"This is rendered with ",
|
||||
new FixedUiElement(this.render.txt).SetClass("literalcode bold"),
|
||||
])
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
let mappings: BaseUIElement = undefined;
|
||||
let mappings: BaseUIElement = undefined
|
||||
if (this.mappings !== undefined) {
|
||||
mappings = new List(
|
||||
[].concat(...this.mappings.map(m => {
|
||||
[].concat(
|
||||
...this.mappings.map((m) => {
|
||||
const msgs: (string | BaseUIElement)[] = [
|
||||
new Combine(
|
||||
[
|
||||
new FixedUiElement(m.then.txt).SetClass("bold"),
|
||||
" corresponds with ",
|
||||
new FixedUiElement( m.if.asHumanString(true, false, {})).SetClass("code")
|
||||
]
|
||||
)
|
||||
new Combine([
|
||||
new FixedUiElement(m.then.txt).SetClass("bold"),
|
||||
" corresponds with ",
|
||||
new FixedUiElement(m.if.asHumanString(true, false, {})).SetClass(
|
||||
"code"
|
||||
),
|
||||
]),
|
||||
]
|
||||
if (m.hideInAnswer === true) {
|
||||
msgs.push(new FixedUiElement("This option cannot be chosen as answer").SetClass("italic"))
|
||||
msgs.push(
|
||||
new FixedUiElement(
|
||||
"This option cannot be chosen as answer"
|
||||
).SetClass("italic")
|
||||
)
|
||||
}
|
||||
if (m.ifnot !== undefined) {
|
||||
msgs.push("Unselecting this answer will add " + m.ifnot.asHumanString(true, false, {}))
|
||||
msgs.push(
|
||||
"Unselecting this answer will add " +
|
||||
m.ifnot.asHumanString(true, false, {})
|
||||
)
|
||||
}
|
||||
return msgs;
|
||||
}
|
||||
))
|
||||
return msgs
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let condition: BaseUIElement = undefined
|
||||
if (this.condition !== undefined && !this.condition?.matchesProperties({})) {
|
||||
condition = new Combine(["Only visible if ",
|
||||
new FixedUiElement(this.condition.asHumanString(false, false, {})
|
||||
).SetClass("code")
|
||||
, " is shown"])
|
||||
condition = new Combine([
|
||||
"Only visible if ",
|
||||
new FixedUiElement(this.condition.asHumanString(false, false, {})).SetClass("code"),
|
||||
" is shown",
|
||||
])
|
||||
}
|
||||
|
||||
let group: BaseUIElement = undefined
|
||||
if (this.group !== undefined && this.group !== "") {
|
||||
group = new Combine([
|
||||
"This tagrendering is part of group ", new FixedUiElement(this.group).SetClass("code")
|
||||
"This tagrendering is part of group ",
|
||||
new FixedUiElement(this.group).SetClass("code"),
|
||||
])
|
||||
}
|
||||
let labels: BaseUIElement = undefined
|
||||
if (this.labels?.length > 0) {
|
||||
labels = new Combine([
|
||||
"This tagrendering has labels ",
|
||||
...this.labels.map(label => new FixedUiElement(label).SetClass("code"))
|
||||
...this.labels.map((label) => new FixedUiElement(label).SetClass("code")),
|
||||
]).SetClass("flex")
|
||||
}
|
||||
|
||||
|
||||
return new Combine([
|
||||
new Title(this.id, 3),
|
||||
this.description,
|
||||
this.question !== undefined ?
|
||||
new Combine(["The question is ", new FixedUiElement(this.question.txt).SetClass("font-bold bold")]) :
|
||||
new FixedUiElement(
|
||||
"This tagrendering has no question and is thus read-only"
|
||||
).SetClass("italic"),
|
||||
this.question !== undefined
|
||||
? new Combine([
|
||||
"The question is ",
|
||||
new FixedUiElement(this.question.txt).SetClass("font-bold bold"),
|
||||
])
|
||||
: new FixedUiElement(
|
||||
"This tagrendering has no question and is thus read-only"
|
||||
).SetClass("italic"),
|
||||
new Combine(withRender),
|
||||
mappings,
|
||||
condition,
|
||||
group,
|
||||
labels
|
||||
]).SetClass("flex flex-col");
|
||||
labels,
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import TilesourceConfigJson from "./Json/TilesourceConfigJson";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import {Translation} from "../../UI/i18n/Translation";
|
||||
import TilesourceConfigJson from "./Json/TilesourceConfigJson"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import { Translation } from "../../UI/i18n/Translation"
|
||||
|
||||
export default class TilesourceConfig {
|
||||
public readonly source: string
|
||||
|
@ -9,21 +9,23 @@ export default class TilesourceConfig {
|
|||
public readonly name: Translation
|
||||
public readonly minzoom: number
|
||||
public readonly maxzoom: number
|
||||
public readonly defaultState: boolean;
|
||||
public readonly defaultState: boolean
|
||||
|
||||
constructor(config: TilesourceConfigJson, ctx: string = "") {
|
||||
this.id = config.id
|
||||
this.source = config.source;
|
||||
this.isOverlay = config.isOverlay ?? false;
|
||||
this.source = config.source
|
||||
this.isOverlay = config.isOverlay ?? false
|
||||
this.name = Translations.T(config.name)
|
||||
this.minzoom = config.minZoom ?? 0
|
||||
this.maxzoom = config.maxZoom ?? 999
|
||||
this.defaultState = config.defaultState ?? true;
|
||||
this.defaultState = config.defaultState ?? true
|
||||
if (this.id === undefined) {
|
||||
throw "An id is obligated"
|
||||
}
|
||||
if (this.minzoom > this.maxzoom) {
|
||||
throw "Invalid tilesourceConfig: minzoom should be smaller then maxzoom (at " + ctx + ")"
|
||||
throw (
|
||||
"Invalid tilesourceConfig: minzoom should be smaller then maxzoom (at " + ctx + ")"
|
||||
)
|
||||
}
|
||||
if (this.minzoom < 0) {
|
||||
throw "minzoom should be > 0 (at " + ctx + ")"
|
||||
|
@ -38,5 +40,4 @@ export default class TilesourceConfig {
|
|||
throw "Disabling an overlay without a name is not possible"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import TagRenderingConfig from "./TagRenderingConfig";
|
||||
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
|
||||
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
|
||||
import TagRenderingConfig from "./TagRenderingConfig"
|
||||
import SharedTagRenderings from "../../Customizations/SharedTagRenderings"
|
||||
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
|
||||
|
||||
export default class WithContextLoader {
|
||||
protected readonly _context: string;
|
||||
private readonly _json: any;
|
||||
protected readonly _context: string
|
||||
private readonly _json: any
|
||||
|
||||
constructor(json: any, context: string) {
|
||||
this._json = json;
|
||||
this._context = context;
|
||||
this._json = json
|
||||
this._context = context
|
||||
}
|
||||
|
||||
/** Given a key, gets the corresponding property from the json (or the default if not found
|
||||
|
@ -16,26 +16,20 @@ export default class WithContextLoader {
|
|||
* The found value is interpreted as a tagrendering and fetched/parsed
|
||||
* */
|
||||
public tr(key: string, deflt) {
|
||||
const v = this._json[key];
|
||||
const v = this._json[key]
|
||||
if (v === undefined || v === null) {
|
||||
if (deflt === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return new TagRenderingConfig(
|
||||
deflt,
|
||||
`${this._context}.${key}.default value`
|
||||
);
|
||||
return new TagRenderingConfig(deflt, `${this._context}.${key}.default value`)
|
||||
}
|
||||
if (typeof v === "string") {
|
||||
const shared = SharedTagRenderings.SharedTagRendering.get(v);
|
||||
const shared = SharedTagRenderings.SharedTagRendering.get(v)
|
||||
if (shared) {
|
||||
return shared;
|
||||
return shared
|
||||
}
|
||||
}
|
||||
return new TagRenderingConfig(
|
||||
v,
|
||||
`${this._context}.${key}`
|
||||
);
|
||||
return new TagRenderingConfig(v, `${this._context}.${key}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,27 +42,29 @@ export default class WithContextLoader {
|
|||
/**
|
||||
* Throw an error if 'question' is defined
|
||||
*/
|
||||
readOnlyMode?: boolean,
|
||||
readOnlyMode?: boolean
|
||||
requiresId?: boolean
|
||||
prepConfig?: ((config: TagRenderingConfigJson) => TagRenderingConfigJson)
|
||||
|
||||
prepConfig?: (config: TagRenderingConfigJson) => TagRenderingConfigJson
|
||||
}
|
||||
): TagRenderingConfig[] {
|
||||
if (tagRenderings === undefined) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
|
||||
const context = this._context
|
||||
options = options ?? {}
|
||||
if (options.prepConfig === undefined) {
|
||||
options.prepConfig = c => c
|
||||
options.prepConfig = (c) => c
|
||||
}
|
||||
const renderings: TagRenderingConfig[] = []
|
||||
for (let i = 0; i < tagRenderings.length; i++) {
|
||||
const preparedConfig = tagRenderings[i];
|
||||
const tr = new TagRenderingConfig(preparedConfig, `${context}.tagrendering[${i}]`);
|
||||
const preparedConfig = tagRenderings[i]
|
||||
const tr = new TagRenderingConfig(preparedConfig, `${context}.tagrendering[${i}]`)
|
||||
if (options.readOnlyMode && tr.question !== undefined) {
|
||||
throw "A question is defined for " + `${context}.tagrendering[${i}], but this is not allowed at this position - probably because this rendering is an icon, badge or label`
|
||||
throw (
|
||||
"A question is defined for " +
|
||||
`${context}.tagrendering[${i}], but this is not allowed at this position - probably because this rendering is an icon, badge or label`
|
||||
)
|
||||
}
|
||||
if (options.requiresId && tr.id === "") {
|
||||
throw `${context}.tagrendering[${i}] has an invalid ID - make sure it is defined and not empty`
|
||||
|
@ -77,6 +73,6 @@ export default class WithContextLoader {
|
|||
renderings.push(tr)
|
||||
}
|
||||
|
||||
return renderings;
|
||||
return renderings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
export interface TileRange {
|
||||
xstart: number,
|
||||
ystart: number,
|
||||
xend: number,
|
||||
yend: number,
|
||||
total: number,
|
||||
xstart: number
|
||||
ystart: number
|
||||
xend: number
|
||||
yend: number
|
||||
total: number
|
||||
zoomlevel: number
|
||||
}
|
||||
|
||||
export class Tiles {
|
||||
|
||||
public static MapRange<T>(tileRange: TileRange, f: (x: number, y: number) => T): T[] {
|
||||
const result: T[] = []
|
||||
const total = tileRange.total
|
||||
|
@ -17,11 +16,11 @@ export class Tiles {
|
|||
}
|
||||
for (let x = tileRange.xstart; x <= tileRange.xend; x++) {
|
||||
for (let y = tileRange.ystart; y <= tileRange.yend; y++) {
|
||||
const t = f(x, y);
|
||||
const t = f(x, y)
|
||||
result.push(t)
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,11 +31,21 @@ export class Tiles {
|
|||
* @returns [[maxlat, minlon], [minlat, maxlon]]
|
||||
*/
|
||||
static tile_bounds(z: number, x: number, y: number): [[number, number], [number, number]] {
|
||||
return [[Tiles.tile2lat(y, z), Tiles.tile2long(x, z)], [Tiles.tile2lat(y + 1, z), Tiles.tile2long(x + 1, z)]]
|
||||
return [
|
||||
[Tiles.tile2lat(y, z), Tiles.tile2long(x, z)],
|
||||
[Tiles.tile2lat(y + 1, z), Tiles.tile2long(x + 1, z)],
|
||||
]
|
||||
}
|
||||
|
||||
static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] {
|
||||
return [[Tiles.tile2long(x, z), Tiles.tile2lat(y, z)], [Tiles.tile2long(x + 1, z), Tiles.tile2lat(y + 1, z)]]
|
||||
static tile_bounds_lon_lat(
|
||||
z: number,
|
||||
x: number,
|
||||
y: number
|
||||
): [[number, number], [number, number]] {
|
||||
return [
|
||||
[Tiles.tile2long(x, z), Tiles.tile2lat(y, z)],
|
||||
[Tiles.tile2long(x + 1, z), Tiles.tile2lat(y + 1, z)],
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,11 +55,14 @@ export class Tiles {
|
|||
* @param y
|
||||
*/
|
||||
static centerPointOf(z: number, x: number, y: number): [number, number] {
|
||||
return [(Tiles.tile2long(x, z) + Tiles.tile2long(x + 1, z)) / 2, (Tiles.tile2lat(y, z) + Tiles.tile2lat(y + 1, z)) / 2]
|
||||
return [
|
||||
(Tiles.tile2long(x, z) + Tiles.tile2long(x + 1, z)) / 2,
|
||||
(Tiles.tile2lat(y, z) + Tiles.tile2lat(y + 1, z)) / 2,
|
||||
]
|
||||
}
|
||||
|
||||
static tile_index(z: number, x: number, y: number): number {
|
||||
return ((x * (2 << z)) + y) * 100 + z
|
||||
return (x * (2 << z) + y) * 100 + z
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,7 +71,7 @@ export class Tiles {
|
|||
* @returns 'zxy'
|
||||
*/
|
||||
static tile_from_index(index: number): [number, number, number] {
|
||||
const z = index % 100;
|
||||
const z = index % 100
|
||||
const factor = 2 << z
|
||||
index = Math.floor(index / 100)
|
||||
const x = Math.floor(index / factor)
|
||||
|
@ -69,11 +81,17 @@ export class Tiles {
|
|||
/**
|
||||
* Return x, y of the tile containing (lat, lon) on the given zoom level
|
||||
*/
|
||||
static embedded_tile(lat: number, lon: number, z: number): { x: number, y: number, z: number } {
|
||||
return {x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z: z}
|
||||
static embedded_tile(lat: number, lon: number, z: number): { x: number; y: number; z: number } {
|
||||
return { x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z: z }
|
||||
}
|
||||
|
||||
static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1: number, lon1: number): TileRange {
|
||||
static TileRangeBetween(
|
||||
zoomlevel: number,
|
||||
lat0: number,
|
||||
lon0: number,
|
||||
lat1: number,
|
||||
lon1: number
|
||||
): TileRange {
|
||||
const t0 = Tiles.embedded_tile(lat0, lon0, zoomlevel)
|
||||
const t1 = Tiles.embedded_tile(lat1, lon1, zoomlevel)
|
||||
|
||||
|
@ -89,26 +107,30 @@ export class Tiles {
|
|||
ystart: ystart,
|
||||
yend: yend,
|
||||
total: total,
|
||||
zoomlevel: zoomlevel
|
||||
zoomlevel: zoomlevel,
|
||||
}
|
||||
}
|
||||
|
||||
private static tile2long(x, z) {
|
||||
return (x / Math.pow(2, z) * 360 - 180);
|
||||
return (x / Math.pow(2, z)) * 360 - 180
|
||||
}
|
||||
|
||||
private static tile2lat(y, z) {
|
||||
const n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
|
||||
return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))));
|
||||
const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z)
|
||||
return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))
|
||||
}
|
||||
|
||||
private static lon2tile(lon, zoom) {
|
||||
return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom)));
|
||||
return Math.floor(((lon + 180) / 360) * Math.pow(2, zoom))
|
||||
}
|
||||
|
||||
private static lat2tile(lat, zoom) {
|
||||
return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)));
|
||||
return Math.floor(
|
||||
((1 -
|
||||
Math.log(Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)) /
|
||||
Math.PI) /
|
||||
2) *
|
||||
Math.pow(2, zoom)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,41 @@
|
|||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import {FixedUiElement} from "../UI/Base/FixedUiElement";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import {Denomination} from "./Denomination";
|
||||
import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson";
|
||||
import BaseUIElement from "../UI/BaseUIElement"
|
||||
import { FixedUiElement } from "../UI/Base/FixedUiElement"
|
||||
import Combine from "../UI/Base/Combine"
|
||||
import { Denomination } from "./Denomination"
|
||||
import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"
|
||||
|
||||
export class Unit {
|
||||
public readonly appliesToKeys: Set<string>;
|
||||
public readonly denominations: Denomination[];
|
||||
public readonly denominationsSorted: Denomination[];
|
||||
public readonly eraseInvalid: boolean;
|
||||
public readonly appliesToKeys: Set<string>
|
||||
public readonly denominations: Denomination[]
|
||||
public readonly denominationsSorted: Denomination[]
|
||||
public readonly eraseInvalid: boolean
|
||||
|
||||
constructor(appliesToKeys: string[], applicableDenominations: Denomination[], eraseInvalid: boolean) {
|
||||
this.appliesToKeys = new Set(appliesToKeys);
|
||||
this.denominations = applicableDenominations;
|
||||
constructor(
|
||||
appliesToKeys: string[],
|
||||
applicableDenominations: Denomination[],
|
||||
eraseInvalid: boolean
|
||||
) {
|
||||
this.appliesToKeys = new Set(appliesToKeys)
|
||||
this.denominations = applicableDenominations
|
||||
this.eraseInvalid = eraseInvalid
|
||||
|
||||
const seenUnitExtensions = new Set<string>();
|
||||
const seenUnitExtensions = new Set<string>()
|
||||
for (const denomination of this.denominations) {
|
||||
if (seenUnitExtensions.has(denomination.canonical)) {
|
||||
throw "This canonical unit is already defined in another denomination: " + denomination.canonical
|
||||
throw (
|
||||
"This canonical unit is already defined in another denomination: " +
|
||||
denomination.canonical
|
||||
)
|
||||
}
|
||||
const duplicate = denomination.alternativeDenominations.filter(denom => seenUnitExtensions.has(denom))
|
||||
const duplicate = denomination.alternativeDenominations.filter((denom) =>
|
||||
seenUnitExtensions.has(denom)
|
||||
)
|
||||
if (duplicate.length > 0) {
|
||||
throw "A denomination is used multiple times: " + duplicate.join(", ")
|
||||
}
|
||||
|
||||
seenUnitExtensions.add(denomination.canonical)
|
||||
denomination.alternativeDenominations.forEach(d => seenUnitExtensions.add(d))
|
||||
denomination.alternativeDenominations.forEach((d) => seenUnitExtensions.add(d))
|
||||
}
|
||||
this.denominationsSorted = [...this.denominations]
|
||||
this.denominationsSorted.sort((a, b) => b.canonical.length - a.canonical.length)
|
||||
|
@ -51,37 +60,38 @@ export class Unit {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
static fromJson(json: UnitConfigJson, ctx: string) {
|
||||
const appliesTo = json.appliesToKey
|
||||
for (let i = 0; i < appliesTo.length; i++) {
|
||||
let key = appliesTo[i];
|
||||
let key = appliesTo[i]
|
||||
if (key.trim() !== key) {
|
||||
throw `${ctx}.appliesToKey[${i}] is invalid: it starts or ends with whitespace`
|
||||
}
|
||||
}
|
||||
|
||||
if ((json.applicableUnits ?? []).length === 0) {
|
||||
throw `${ctx}: define at least one applicable unit`
|
||||
throw `${ctx}: define at least one applicable unit`
|
||||
}
|
||||
// Some keys do have unit handling
|
||||
|
||||
if(json.applicableUnits.some(denom => denom.useAsDefaultInput !== undefined)){
|
||||
json.applicableUnits.forEach(denom => {
|
||||
if (json.applicableUnits.some((denom) => denom.useAsDefaultInput !== undefined)) {
|
||||
json.applicableUnits.forEach((denom) => {
|
||||
denom.useAsDefaultInput = denom.useAsDefaultInput ?? false
|
||||
})
|
||||
}
|
||||
|
||||
const applicable = json.applicableUnits.map((u, i) => new Denomination(u, `${ctx}.units[${i}]`))
|
||||
|
||||
const applicable = json.applicableUnits.map(
|
||||
(u, i) => new Denomination(u, `${ctx}.units[${i}]`)
|
||||
)
|
||||
return new Unit(appliesTo, applicable, json.eraseInvalidValues ?? false)
|
||||
}
|
||||
|
||||
isApplicableToKey(key: string | undefined): boolean {
|
||||
if (key === undefined) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
return this.appliesToKeys.has(key);
|
||||
return this.appliesToKeys.has(key)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,7 +99,7 @@ export class Unit {
|
|||
*/
|
||||
findDenomination(valueWithDenom: string, country: () => string): [string, Denomination] {
|
||||
if (valueWithDenom === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const defaultDenom = this.getDefaultDenomination(country)
|
||||
for (const denomination of this.denominationsSorted) {
|
||||
|
@ -103,27 +113,28 @@ export class Unit {
|
|||
|
||||
asHumanLongValue(value: string, country: () => string): BaseUIElement {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const [stripped, denom] = this.findDenomination(value, country)
|
||||
const human = stripped === "1" ? denom?.humanSingular : denom?.human
|
||||
if (human === undefined) {
|
||||
return new FixedUiElement(stripped ?? value);
|
||||
return new FixedUiElement(stripped ?? value)
|
||||
}
|
||||
|
||||
const elems = denom.prefix ? [human, stripped] : [stripped, human];
|
||||
const elems = denom.prefix ? [human, stripped] : [stripped, human]
|
||||
return new Combine(elems)
|
||||
|
||||
}
|
||||
|
||||
|
||||
public getDefaultInput(country: () => string | string[]) {
|
||||
console.log("Searching the default denomination for input", country)
|
||||
for (const denomination of this.denominations) {
|
||||
if (denomination.useAsDefaultInput === true) {
|
||||
return denomination
|
||||
}
|
||||
if (denomination.useAsDefaultInput === undefined || denomination.useAsDefaultInput === false) {
|
||||
if (
|
||||
denomination.useAsDefaultInput === undefined ||
|
||||
denomination.useAsDefaultInput === false
|
||||
) {
|
||||
continue
|
||||
}
|
||||
let countries: string | string[] = country()
|
||||
|
@ -131,19 +142,22 @@ export class Unit {
|
|||
countries = countries.split(",")
|
||||
}
|
||||
const denominationCountries: string[] = denomination.useAsDefaultInput
|
||||
if (countries.some(country => denominationCountries.indexOf(country) >= 0)) {
|
||||
if (countries.some((country) => denominationCountries.indexOf(country) >= 0)) {
|
||||
return denomination
|
||||
}
|
||||
}
|
||||
return this.denominations[0]
|
||||
}
|
||||
|
||||
public getDefaultDenomination(country: () => string){
|
||||
|
||||
public getDefaultDenomination(country: () => string) {
|
||||
for (const denomination of this.denominations) {
|
||||
if (denomination.useIfNoUnitGiven === true || denomination.canonical === "") {
|
||||
return denomination
|
||||
}
|
||||
if (denomination.useIfNoUnitGiven === undefined || denomination.useIfNoUnitGiven === false) {
|
||||
if (
|
||||
denomination.useIfNoUnitGiven === undefined ||
|
||||
denomination.useIfNoUnitGiven === false
|
||||
) {
|
||||
continue
|
||||
}
|
||||
let countries: string | string[] = country()
|
||||
|
@ -151,11 +165,10 @@ export class Unit {
|
|||
countries = countries.split(",")
|
||||
}
|
||||
const denominationCountries: string[] = denomination.useIfNoUnitGiven
|
||||
if (countries.some(country => denominationCountries.indexOf(country) >= 0)) {
|
||||
if (countries.some((country) => denominationCountries.indexOf(country) >= 0)) {
|
||||
return denomination
|
||||
}
|
||||
}
|
||||
return this.denominations[0]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export default interface SmallLicense {
|
||||
path: string,
|
||||
authors: string[],
|
||||
license: string,
|
||||
path: string
|
||||
authors: string[]
|
||||
license: string
|
||||
sources: string[]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue