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:
Pieter Vander Vennet 2021-12-05 02:06:14 +01:00
parent 8e40d76281
commit 0ee23ce36d
27 changed files with 9032 additions and 331 deletions

View 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
}
}

View file

@ -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
}
}

View file

@ -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}
}

View file

@ -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>()