forked from MapComplete/MapComplete
Add question box as special rendering
This commit is contained in:
parent
15664df63f
commit
d47fd7e746
42 changed files with 956 additions and 311 deletions
|
@ -23,6 +23,8 @@ import predifined_filters from "../../../assets/layers/filters/filters.json"
|
|||
import { TagConfigJson } from "../Json/TagConfigJson"
|
||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
||||
import ValidationUtils from "./ValidationUtils"
|
||||
import { RenderingSpecification } from "../../../UI/SpecialVisualization"
|
||||
|
||||
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
||||
private static readonly predefinedFilters = ExpandFilter.load_filters()
|
||||
|
@ -218,7 +220,7 @@ class ExpandTagRendering extends Conversion<
|
|||
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.labels?.indexOf(id_) >= 0)
|
||||
} else {
|
||||
matchingTrs = layerTrs.filter((tr) => tr.id === id || tr.labels?.indexOf(id) >= 0)
|
||||
}
|
||||
|
@ -255,13 +257,6 @@ class ExpandTagRendering extends Conversion<
|
|||
ctx: string
|
||||
): TagRenderingConfigJson[] {
|
||||
const state = this._state
|
||||
if (tr === "questions") {
|
||||
return [
|
||||
{
|
||||
id: "questions",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (typeof tr === "string") {
|
||||
const lookup = this.lookup(tr)
|
||||
|
@ -415,6 +410,111 @@ class ExpandTagRendering extends Conversion<
|
|||
}
|
||||
}
|
||||
|
||||
export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
||||
constructor() {
|
||||
super(
|
||||
"Adds a 'questions'-object if no question element is added yet",
|
||||
["tagRenderings"],
|
||||
"AddQuestionBox"
|
||||
)
|
||||
}
|
||||
|
||||
convert(
|
||||
json: LayerConfigJson,
|
||||
context: string
|
||||
): { result: LayerConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
if (json.tagRenderings === undefined) {
|
||||
return { result: json }
|
||||
}
|
||||
json = JSON.parse(JSON.stringify(json))
|
||||
const allSpecials: Exclude<RenderingSpecification, string>[] = []
|
||||
.concat(
|
||||
...json.tagRenderings.map((tr) =>
|
||||
ValidationUtils.getSpecialVisualsationsWithArgs(<TagRenderingConfigJson>tr)
|
||||
)
|
||||
)
|
||||
.filter((spec) => typeof spec !== "string")
|
||||
|
||||
const questionSpecials = allSpecials.filter((sp) => sp.func.funcName === "questions")
|
||||
const noLabels = questionSpecials.filter(
|
||||
(sp) => sp.args.length === 0 || sp.args[0].trim() === ""
|
||||
)
|
||||
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
if (noLabels.length > 1) {
|
||||
console.log(json.tagRenderings)
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
": multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
|
||||
)
|
||||
}
|
||||
|
||||
// ALl labels that are used in this layer
|
||||
const allLabels = new Set(
|
||||
[].concat(...json.tagRenderings.map((tr) => (<TagRenderingConfigJson>tr).labels ?? []))
|
||||
)
|
||||
const seen = new Set()
|
||||
for (const questionSpecial of questionSpecials) {
|
||||
if (typeof questionSpecial === "string") {
|
||||
continue
|
||||
}
|
||||
const used = questionSpecial.args[0]
|
||||
?.split(";")
|
||||
?.map((a) => a.trim())
|
||||
?.filter((s) => s != "")
|
||||
const blacklisted = questionSpecial.args[1]
|
||||
?.split(";")
|
||||
?.map((a) => a.trim())
|
||||
?.filter((s) => s != "")
|
||||
if (blacklisted?.length > 0 && used?.length > 0) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
": the {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
|
||||
"\n Whitelisted: " +
|
||||
used.join(", ") +
|
||||
"\n Blacklisted: " +
|
||||
blacklisted.join(", ")
|
||||
)
|
||||
}
|
||||
for (const usedLabel of used) {
|
||||
if (!allLabels.has(usedLabel)) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
": this layers specifies a special question element for label `" +
|
||||
usedLabel +
|
||||
"`, but this label doesn't exist.\n" +
|
||||
" Available labels are " +
|
||||
Array.from(allLabels).join(", ")
|
||||
)
|
||||
}
|
||||
seen.add(usedLabel)
|
||||
}
|
||||
}
|
||||
|
||||
if (noLabels.length == 0) {
|
||||
/* At this point, we know which question labels are not yet handled and which already are handled, and we
|
||||
* know there is no previous catch-all questions
|
||||
*/
|
||||
const question: TagRenderingConfigJson = {
|
||||
id: "leftover-questions",
|
||||
render: {
|
||||
"*": `{questions( ,${Array.from(seen).join(";")})}`,
|
||||
},
|
||||
}
|
||||
json.tagRenderings.push(question)
|
||||
}
|
||||
return {
|
||||
result: json,
|
||||
errors,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[]> {
|
||||
constructor() {
|
||||
super("Applies a rewrite", [], "ExpandRewrite")
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
SetDefault,
|
||||
} from "./Conversion"
|
||||
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||
import { PrepareLayer } from "./PrepareLayer"
|
||||
import { AddQuestionBox, PrepareLayer } from "./PrepareLayer"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import Constants from "../../Constants"
|
||||
|
@ -336,7 +336,6 @@ export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
|
|||
if (!hasMinimap) {
|
||||
layerConfig = { ...layerConfig }
|
||||
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
|
||||
layerConfig.tagRenderings.push(state.tagRenderings.get("questions"))
|
||||
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
|
||||
}
|
||||
|
||||
|
@ -662,6 +661,7 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
|
|||
: new AddDefaultLayers(state),
|
||||
new AddDependencyLayersToTheme(state),
|
||||
new AddImportLayers(),
|
||||
new On("layers", new Each(new AddQuestionBox())),
|
||||
new On("layers", new Each(new AddMiniMap(state)))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -619,12 +619,12 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
': detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
|
||||
)
|
||||
}
|
||||
if (json.group) {
|
||||
if (json["group"]) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
': groups are deprecated, use `"label": ["' +
|
||||
json.group +
|
||||
json["group"] +
|
||||
'"]` instead'
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
|
||||
import { SpecialVisualization } from "../../../UI/SpecialVisualization"
|
||||
import { RenderingSpecification, SpecialVisualization } from "../../../UI/SpecialVisualization"
|
||||
|
||||
export default class ValidationUtils {
|
||||
/**
|
||||
|
@ -11,11 +11,18 @@ export default class ValidationUtils {
|
|||
public static getSpecialVisualisations(
|
||||
renderingConfig: TagRenderingConfigJson
|
||||
): SpecialVisualization[] {
|
||||
return ValidationUtils.getSpecialVisualsationsWithArgs(renderingConfig).map(
|
||||
(spec) => spec["func"]
|
||||
)
|
||||
}
|
||||
public static getSpecialVisualsationsWithArgs(
|
||||
renderingConfig: TagRenderingConfigJson
|
||||
): RenderingSpecification[] {
|
||||
const translations: any[] = Utils.NoNull([
|
||||
renderingConfig.render,
|
||||
...(renderingConfig.mappings ?? []).map((m) => m.then),
|
||||
])
|
||||
const all: SpecialVisualization[] = []
|
||||
const all: RenderingSpecification[] = []
|
||||
for (let translation of translations) {
|
||||
if (typeof translation == "string") {
|
||||
translation = { "*": translation }
|
||||
|
@ -28,9 +35,7 @@ export default class ValidationUtils {
|
|||
|
||||
const template = translation[key]
|
||||
const parts = SpecialVisualizations.constructSpecification(template)
|
||||
const specials = parts
|
||||
.filter((p) => typeof p !== "string")
|
||||
.map((special) => special["func"])
|
||||
const specials = parts.filter((p) => typeof p !== "string")
|
||||
all.push(...specials)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -244,8 +244,8 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
iconAndBadges.SetClass("w-full h-full")
|
||||
}
|
||||
|
||||
const css = this.cssDef?.GetRenderValue(tags, undefined)?.txt
|
||||
const cssClasses = this.cssClasses?.GetRenderValue(tags, undefined)?.txt
|
||||
const css = this.cssDef?.GetRenderValue(tags)?.txt
|
||||
const cssClasses = this.cssClasses?.GetRenderValue(tags)?.txt
|
||||
|
||||
let label = this.GetLabel(tags)
|
||||
let htmlEl: BaseUIElement
|
||||
|
|
|
@ -203,19 +203,6 @@ export default class TagRenderingConfig {
|
|||
throw `${context}: A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}`
|
||||
}
|
||||
|
||||
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) {
|
||||
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}`
|
||||
|
@ -509,15 +496,11 @@ export default class TagRenderingConfig {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
return applicableMappings
|
||||
}
|
||||
|
||||
public GetRenderValue(
|
||||
tags: any,
|
||||
defltValue: any = undefined
|
||||
): TypedTranslation<any> | undefined {
|
||||
return this.GetRenderValueWithImage(tags, defltValue)?.then
|
||||
public GetRenderValue(tags: Record<string, string>): TypedTranslation<any> | undefined {
|
||||
return this.GetRenderValueWithImage(tags)?.then
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -526,8 +509,7 @@ export default class TagRenderingConfig {
|
|||
* @constructor
|
||||
*/
|
||||
public GetRenderValueWithImage(
|
||||
tags: any,
|
||||
defltValue: any = undefined
|
||||
tags: Record<string, string>
|
||||
): { then: TypedTranslation<any>; icon?: string } | undefined {
|
||||
if (this.condition !== undefined) {
|
||||
if (!this.condition.matchesProperties(tags)) {
|
||||
|
@ -554,7 +536,7 @@ export default class TagRenderingConfig {
|
|||
return { then: this.render }
|
||||
}
|
||||
|
||||
return { then: defltValue }
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -625,6 +607,76 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a value for the freeform key and an overview of the selected mappings, construct the correct tagsFilter to apply
|
||||
*
|
||||
* @param freeformValue The freeform value which will be applied as 'freeform.key'. Ignored if 'freeform.key' is not set
|
||||
*
|
||||
* @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform
|
||||
* @param multiSelectedMapping (Only used if multiAnswer == true): all the mappings that must be applied. Set multiSelectedMapping[mappings.length] to use the freeform as well
|
||||
*/
|
||||
public constructChangeSpecification(
|
||||
freeformValue: string | undefined,
|
||||
singleSelectedMapping: number,
|
||||
multiSelectedMapping: boolean[] | undefined
|
||||
): UploadableTag {
|
||||
if (
|
||||
freeformValue === undefined &&
|
||||
singleSelectedMapping === undefined &&
|
||||
multiSelectedMapping === undefined
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
if (this.mappings === undefined && freeformValue === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (
|
||||
this.freeform !== undefined &&
|
||||
(this.mappings === undefined ||
|
||||
this.mappings.length == 0 ||
|
||||
(singleSelectedMapping === this.mappings.length && !this.multiAnswer))
|
||||
) {
|
||||
// Either no mappings, or this is a radio-button selected freeform value
|
||||
return new And([
|
||||
new Tag(this.freeform.key, freeformValue),
|
||||
...(this.freeform.addExtraTags ?? []),
|
||||
])
|
||||
}
|
||||
|
||||
if (this.multiAnswer) {
|
||||
let selectedMappings: UploadableTag[] = this.mappings
|
||||
.filter((_, i) => multiSelectedMapping[i])
|
||||
.map((m) => new And([m.if, ...(m.addExtraTags ?? [])]))
|
||||
|
||||
let unselectedMappings: UploadableTag[] = this.mappings
|
||||
.filter((_, i) => !multiSelectedMapping[i])
|
||||
.map((m) => m.ifnot)
|
||||
|
||||
if (multiSelectedMapping.at(-1)) {
|
||||
// The freeform value was selected as well
|
||||
selectedMappings.push(
|
||||
new And([
|
||||
new Tag(this.freeform.key, freeformValue),
|
||||
...(this.freeform.addExtraTags ?? []),
|
||||
])
|
||||
)
|
||||
}
|
||||
return TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
|
||||
} else {
|
||||
if (singleSelectedMapping === this.mappings.length) {
|
||||
return new And([
|
||||
new Tag(this.freeform.key, freeformValue),
|
||||
...(this.freeform.addExtraTags ?? []),
|
||||
])
|
||||
} else {
|
||||
return new And([
|
||||
this.mappings[singleSelectedMapping].if,
|
||||
...(this.mappings[singleSelectedMapping].addExtraTags ?? []),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GenerateDocumentation(): BaseUIElement {
|
||||
let withRender: (BaseUIElement | string)[] = []
|
||||
if (this.freeform?.key !== undefined) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue