forked from MapComplete/MapComplete
194 lines
6.9 KiB
TypeScript
194 lines
6.9 KiB
TypeScript
import TagRenderingConfig from "./TagRenderingConfig"
|
|
import { ExtraFuncParams, ExtraFunctions } from "../../Logic/ExtraFunctions"
|
|
import LayerConfig from "./LayerConfig"
|
|
import { SpecialVisualization } from "../../UI/SpecialVisualization"
|
|
import SpecialVisualizations from "../../UI/SpecialVisualizations"
|
|
|
|
export default class DependencyCalculator {
|
|
/**
|
|
* For every tagRendering in the listed layers, determines in what layers they end up
|
|
*/
|
|
public static tagRenderingImportedBy(
|
|
questionedLayer: LayerConfig,
|
|
layers: LayerConfig[]
|
|
): Map<
|
|
string,
|
|
{
|
|
layer: string
|
|
}[]
|
|
> {
|
|
const result: Map<string, { layer: string }[]> = new Map()
|
|
|
|
for (const layer of layers) {
|
|
const hasRightContext = layer.tagRenderings.filter(
|
|
(tr) => tr._definedIn !== undefined && tr?._definedIn?.[0] === questionedLayer.id
|
|
)
|
|
for (const tr of hasRightContext) {
|
|
const id = tr._definedIn[1]
|
|
if (!result.has(id)) {
|
|
result.set(id, [])
|
|
}
|
|
result.get(id).push({ layer: layer.id })
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Calculates what layers are introduced by a tagRenderingConfig
|
|
* @param tr
|
|
* @private
|
|
*/
|
|
private static getTagRenderingDependencies(tr: TagRenderingConfig): {
|
|
id: string
|
|
minzoom?: number
|
|
neededBy: string
|
|
}[] {
|
|
if (tr === undefined) {
|
|
throw "Got undefined tag rendering in getTagRenderingDependencies"
|
|
}
|
|
const deps: { id: string; minZoom?: number; neededBy: string }[] = []
|
|
|
|
for (const req of tr.requiredLayers ?? []) {
|
|
const dependency = { ...req, neededBy: tr.id }
|
|
if (!dependency.id) {
|
|
throw `The tagRendering ${tr.id} specifies a \`requiredBy\`, but this \`requiredBy\` does _not_ have an \`id\`-field`
|
|
}
|
|
deps.push(dependency)
|
|
}
|
|
|
|
// All translated snippets
|
|
const parts: string[] = [].concat(...tr.EnumerateTranslations().map((tr) => tr.AllValues()))
|
|
|
|
for (const part of parts) {
|
|
const specialVizs: { func: SpecialVisualization; args: string[] }[] =
|
|
SpecialVisualizations.constructSpecification(part)
|
|
.filter((p) => typeof p !== "string")
|
|
.map((p) => <{ func: SpecialVisualization; args: string[] }>p)
|
|
.filter((o) => o?.func?.getLayerDependencies !== undefined)
|
|
for (const specialViz of specialVizs) {
|
|
deps.push(
|
|
...specialViz.func
|
|
.getLayerDependencies(specialViz.args)
|
|
.map((id) => ({ id, neededBy: tr.id }))
|
|
)
|
|
}
|
|
}
|
|
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
|
|
checkHasSnapName: boolean
|
|
}[] {
|
|
const deps: {
|
|
neededLayer: string
|
|
reason: string
|
|
context?: string
|
|
neededBy: string
|
|
checkHasSnapName: boolean
|
|
minzoom?: number
|
|
}[] = []
|
|
|
|
for (let i = 0; layer.presets !== undefined && i < layer.presets.length; i++) {
|
|
const preset = layer.presets[i]
|
|
const snapTo = preset.preciseInput?.snapToLayers
|
|
if (snapTo && !Array.isArray(snapTo)) {
|
|
throw new Error(
|
|
`snapToLayers is not an array; it is ${snapTo}(used in preset ${i} for: ${layer.id})`
|
|
)
|
|
}
|
|
preset.preciseInput?.snapToLayers?.forEach((id) => {
|
|
deps.push({
|
|
neededLayer: id,
|
|
reason: `preset \`${preset.title.textFor("en")}\` snaps to this layer`,
|
|
context: `${layer.id}.presets[${i}]`,
|
|
neededBy: layer.id,
|
|
checkHasSnapName: true,
|
|
})
|
|
})
|
|
}
|
|
|
|
for (const tr of layer.AllTagRenderings()) {
|
|
for (const dep of DependencyCalculator.getTagRenderingDependencies(tr)) {
|
|
deps.push({
|
|
neededLayer: dep.id,
|
|
reason: `tagrendering ${dep.neededBy} needs this layer`,
|
|
minzoom: dep.minzoom,
|
|
context: tr.id,
|
|
neededBy: layer.id,
|
|
checkHasSnapName: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
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 detectNearyFeatures = (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,
|
|
checkHasSnapName: false,
|
|
})
|
|
|
|
return []
|
|
}
|
|
const params: ExtraFuncParams = {
|
|
getFeatureById: () => undefined,
|
|
getFeaturesWithin: detectNearyFeatures,
|
|
getProbablyOverlapsWith: detectNearyFeatures,
|
|
}
|
|
const helpers = ExtraFunctions.constructHelpers(params)
|
|
// ... 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",
|
|
"{" + ExtraFunctions.types.join(",") + "}",
|
|
"return " + code + ";"
|
|
)
|
|
const result = func(obj, helpers)
|
|
obj.properties[key] = JSON.stringify(result)
|
|
} catch (e) {
|
|
// pass
|
|
}
|
|
}
|
|
}
|
|
|
|
return deps
|
|
}
|
|
}
|