Further improvements to entrances theme, add layer-crossdependency detection, add layers which another layer depends on automatically to the theme, add documentation on which layers depends on which other layers, regenerate documentation
This commit is contained in:
parent
8e40d76281
commit
0ee23ce36d
27 changed files with 9032 additions and 331 deletions
111
Models/ThemeConfig/DependencyCalculator.ts
Normal file
111
Models/ThemeConfig/DependencyCalculator.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
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())))
|
||||
|
||||
for (const part of parts) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }[] = []
|
||||
|
||||
for (let i = 0; layer.presets !== undefined && i < layer.presets.length; i++) {
|
||||
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
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
for (const tr of layer.AllTagRenderings()) {
|
||||
for (const dep of DependencyCalculator.GetTagRenderingDependencies(tr)) {
|
||||
deps.push({
|
||||
neededLayer: dep,
|
||||
reason: "a tagrendering needs this layer",
|
||||
context: tr.id,
|
||||
neededBy: layer.id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (layer.calculatedTags?.length > 0) {
|
||||
const obj = {
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [0, 0]
|
||||
},
|
||||
properties: {
|
||||
id: "node/1"
|
||||
}
|
||||
}
|
||||
let currentKey = undefined
|
||||
let currentLine = undefined
|
||||
const params: ExtraFuncParams = {
|
||||
getFeaturesWithin: (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
|
||||
})
|
||||
|
||||
return []
|
||||
},
|
||||
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;
|
||||
try {
|
||||
|
||||
const func = new Function("feat", "return " + code + ";");
|
||||
const result = func(obj)
|
||||
obj.properties[key] = JSON.stringify(result);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deps
|
||||
}
|
||||
}
|
|
@ -413,7 +413,11 @@ export default class LayerConfig extends WithContextLoader {
|
|||
|
||||
}
|
||||
|
||||
public GenerateDocumentation(usedInThemes: string[], addedByDefault = false, canBeIncluded = true): BaseUIElement {
|
||||
public GenerateDocumentation(usedInThemes: string[], layerIsNeededBy: Map<string, string[]>, dependencies: {
|
||||
context?: string;
|
||||
reason: string;
|
||||
neededLayer: string;
|
||||
}[], addedByDefault = false, canBeIncluded = true): BaseUIElement {
|
||||
const extraProps = []
|
||||
|
||||
if (canBeIncluded) {
|
||||
|
@ -441,8 +445,12 @@ export default class LayerConfig extends WithContextLoader {
|
|||
]
|
||||
}
|
||||
|
||||
for (const dep of Array.from(this.getDependencies())) {
|
||||
extraProps.push(new Combine(["This layer will automatically load ", new Link(dep, "#"+dep)," into the layout as it depends on it."]))
|
||||
for (const dep of dependencies) {
|
||||
extraProps.push(new Combine(["This layer will automatically load ", new Link(dep.neededLayer, "#"+dep.neededLayer)," into the layout as it depends on it: ", dep.reason, "("+dep.context+")"]))
|
||||
}
|
||||
|
||||
for(const revDep of layerIsNeededBy?.get(this.id) ?? []){
|
||||
extraProps.push(new Combine(["This layer is needed as dependency for layer",new Link(revDep, "#"+revDep)]))
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
|
@ -462,6 +470,10 @@ export default class LayerConfig extends WithContextLoader {
|
|||
}
|
||||
return this.calculatedTags.map((code) => code[1]);
|
||||
}
|
||||
|
||||
AllTagRenderings(): TagRenderingConfig[]{
|
||||
return Utils.NoNull([...this.tagRenderings, ...this.titleIcons, this.title, this.isShown])
|
||||
}
|
||||
|
||||
public ExtractImages(): Set<string> {
|
||||
const parts: Set<string>[] = [];
|
||||
|
@ -485,22 +497,4 @@ export default class LayerConfig extends WithContextLoader {
|
|||
return this.lineRendering.some(lr => lr.leftRightSensitive)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 getDependencies(): Set<string>{
|
||||
const deps = new Set<string>()
|
||||
|
||||
for (const preset of this.presets ?? []) {
|
||||
if(preset.preciseInput?.snapToLayers === undefined){
|
||||
continue
|
||||
}
|
||||
preset.preciseInput?.snapToLayers?.forEach(id => {
|
||||
deps.add(id);
|
||||
})
|
||||
}
|
||||
|
||||
return deps
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import LayerConfig from "./LayerConfig";
|
|||
import {LayerConfigJson} from "./Json/LayerConfigJson";
|
||||
import Constants from "../Constants";
|
||||
import TilesourceConfig from "./TilesourceConfig";
|
||||
import DependencyCalculator from "./DependencyCalculator";
|
||||
|
||||
export default class LayoutConfig {
|
||||
public readonly id: string;
|
||||
|
@ -193,7 +194,6 @@ export default class LayoutConfig {
|
|||
names = [names]
|
||||
}
|
||||
names.forEach(name => {
|
||||
|
||||
if (name === "type_node") {
|
||||
// This is a very special layer which triggers special behaviour
|
||||
exportAllNodes = true;
|
||||
|
@ -221,29 +221,47 @@ export default class LayoutConfig {
|
|||
const sharedLayer = AllKnownLayers.sharedLayers.get(defaultLayer)
|
||||
if (sharedLayer !== undefined) {
|
||||
result.push(sharedLayer)
|
||||
}else if(!AllKnownLayers.runningGenerateScript){
|
||||
throw "SharedLayer "+defaultLayer+" not found"
|
||||
}
|
||||
}
|
||||
|
||||
let unmetDependencies: { dependency: string, layer: string }[] = []
|
||||
if(AllKnownLayers.runningGenerateScript){
|
||||
return {layers: result, extractAllNodes: exportAllNodes}
|
||||
}
|
||||
// Verify cross-dependencies
|
||||
let unmetDependencies: { neededLayer: string, neededBy: string, reason: string, context?: string }[] = []
|
||||
do {
|
||||
const dependencies: { dependency: string, layer: string }[] = [].concat(...result.map(l => Array.from(l.getDependencies()).map(d => ({
|
||||
dependency: d,
|
||||
layer: l.id
|
||||
}))))
|
||||
const loadedLayers = new Set(result.map(r => r.id))
|
||||
unmetDependencies = dependencies.filter(dep => !loadedLayers.has(dep.dependency))
|
||||
for (const unmetDependency of unmetDependencies) {
|
||||
const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
|
||||
|
||||
console.log("Recursively loading unmet dependency ", unmetDependency.dependency, "(needed by " + unmetDependency.layer + ")")
|
||||
const dep = AllKnownLayers.sharedLayers.get(unmetDependency.dependency)
|
||||
for (const layerConfig of result) {
|
||||
const layerDeps = DependencyCalculator.getLayerDependencies(layerConfig)
|
||||
dependencies.push(...layerDeps)
|
||||
}
|
||||
|
||||
const loadedLayers = new Set(result.map(r => r.id))
|
||||
// During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
|
||||
// Their existance is checked elsewhere, so this is fine
|
||||
unmetDependencies = dependencies.filter(dep => !loadedLayers.has(dep.neededLayer))
|
||||
for (const unmetDependency of unmetDependencies) {
|
||||
const dep = AllKnownLayers.sharedLayers.get(unmetDependency.neededLayer)
|
||||
if (dep === undefined) {
|
||||
throw "The layer '" + unmetDependency.layer + "' needs '" + unmetDependency.dependency + "' to be loaded, but it could not be found as builtin layer (at " + context + ")"
|
||||
|
||||
const message =
|
||||
["Loading a dependency failed: layer "+unmetDependency.neededLayer+" is not found, neither as layer of "+json.id+" nor as builtin layer.",
|
||||
"This layer is needed by "+unmetDependency.neededBy,
|
||||
unmetDependency.reason+" (at "+unmetDependency.context+")",
|
||||
"Loaded layers are: "+result.map(l => l.id).join(",")
|
||||
|
||||
]
|
||||
throw message.join("\n\t");
|
||||
}
|
||||
result.unshift(dep)
|
||||
unmetDependencies = unmetDependencies.filter(d => d.dependency !== unmetDependency.dependency)
|
||||
unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer)
|
||||
}
|
||||
|
||||
} while (unmetDependencies.length > 0)
|
||||
|
||||
return {layers: result, extractAllNodes: exportAllNodes}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,11 +58,10 @@ export default class TagRenderingConfig {
|
|||
|
||||
|
||||
if (typeof json === "number") {
|
||||
json = ""+json
|
||||
json = "" + json
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (typeof json === "string") {
|
||||
this.render = Translations.T(json, context + ".render");
|
||||
this.multiAnswer = false;
|
||||
|
@ -71,12 +70,11 @@ export default class TagRenderingConfig {
|
|||
|
||||
|
||||
this.id = json.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
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
this.group = json.group ?? "";
|
||||
this.render = Translations.T(json.render, context + ".render");
|
||||
this.question = Translations.T(json.question, context + ".question");
|
||||
|
@ -106,9 +104,9 @@ export default class TagRenderingConfig {
|
|||
throw `Freeform.args is defined. This should probably be 'freeform.helperArgs' (at ${context})`
|
||||
|
||||
}
|
||||
|
||||
if(json.freeform.key === "questions"){
|
||||
if(this.id !== "questions"){
|
||||
|
||||
if (json.freeform.key === "questions") {
|
||||
if (this.id !== "questions") {
|
||||
throw `If you use a freeform key 'questions', the ID must be 'questions' too to trigger the special behaviour. The current id is '${this.id}' (at ${context})`
|
||||
}
|
||||
}
|
||||
|
@ -187,53 +185,52 @@ export default class TagRenderingConfig {
|
|||
|
||||
if (this.id === "questions" && this.render !== undefined) {
|
||||
for (const ln in this.render.translations) {
|
||||
const txt :string = this.render.translations[ln]
|
||||
if(txt.indexOf("{questions}") >= 0){
|
||||
const txt: string = this.render.translations[ln]
|
||||
if (txt.indexOf("{questions}") >= 0) {
|
||||
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"){
|
||||
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){
|
||||
if (this.render === undefined) {
|
||||
throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}`
|
||||
}
|
||||
for (const ln in this.render.translations) {
|
||||
const txt :string = this.render.translations[ln]
|
||||
if(txt === ""){
|
||||
throw context+" Rendering for language "+ln+" is empty"
|
||||
const txt: string = this.render.translations[ln]
|
||||
if (txt === "") {
|
||||
throw context + " Rendering for language " + ln + " is empty"
|
||||
}
|
||||
if(txt.indexOf("{"+this.freeform.key+"}") >= 0){
|
||||
if (txt.indexOf("{" + this.freeform.key + "}") >= 0) {
|
||||
continue
|
||||
}
|
||||
if(txt.indexOf("{"+this.freeform.key+":") >= 0){
|
||||
if (txt.indexOf("{" + this.freeform.key + ":") >= 0) {
|
||||
continue
|
||||
}
|
||||
if(txt.indexOf("{canonical("+this.freeform.key+")") >= 0){
|
||||
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){
|
||||
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}`
|
||||
}
|
||||
|
@ -377,7 +374,7 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
if(this.id === "questions"){
|
||||
if (this.id === "questions") {
|
||||
return this.render
|
||||
}
|
||||
|
||||
|
@ -391,6 +388,26 @@ export default class TagRenderingConfig {
|
|||
return defltValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all translations that might be rendered in all languages
|
||||
* USed for static analysis
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
EnumerateTranslations(): Translation[] {
|
||||
const translations: Translation[] = []
|
||||
for (const key in this) {
|
||||
if(!this.hasOwnProperty(key)){
|
||||
continue;
|
||||
}
|
||||
const o = this[key]
|
||||
if (o instanceof Translation) {
|
||||
translations.push(o)
|
||||
}
|
||||
}
|
||||
return translations;
|
||||
}
|
||||
|
||||
public ExtractImages(isIcon: boolean): Set<string> {
|
||||
|
||||
const usedIcons = new Set<string>()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue