forked from MapComplete/MapComplete
		
	Studio: more finetuning, first version working
This commit is contained in:
		
							parent
							
								
									c4d4a57a08
								
							
						
					
					
						commit
						ac1e7c7f06
					
				
					 39 changed files with 2971 additions and 7187 deletions
				
			
		| 
						 | 
				
			
			@ -153,9 +153,6 @@ export class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
 | 
			
		|||
 | 
			
		||||
    convert(json: TIn, context: ConversionContext): TOut {
 | 
			
		||||
        const r0 = this._step0.convert(json, context.inOperation(this._step0.name))
 | 
			
		||||
        if (context.hasErrors()) {
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
        return this._step1.convert(r0, context.inOperation(this._step1.name))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -308,9 +305,6 @@ export class Fuse<T> extends DesugaringStep<T> {
 | 
			
		|||
                if (r === undefined || r === null) {
 | 
			
		||||
                    break
 | 
			
		||||
                }
 | 
			
		||||
                if (context.hasErrors()) {
 | 
			
		||||
                    break
 | 
			
		||||
                }
 | 
			
		||||
                json = r
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.error("Step " + step.name + " failed due to ", e, e.stack)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -731,6 +731,77 @@ export class ValidateLayer extends Conversion<
 | 
			
		|||
            return null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (typeof json === "string") {
 | 
			
		||||
            context.err(
 | 
			
		||||
                `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
 | 
			
		||||
            )
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (json.id === undefined) {
 | 
			
		||||
            context.err(`Not a valid layer: id is undefined: ${JSON.stringify(json)}`)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (json.source === undefined) {
 | 
			
		||||
            context.enter("source").err("No source section is defined")
 | 
			
		||||
        } else {
 | 
			
		||||
            if (json.source === "special" || json.source === "special:library") {
 | 
			
		||||
            } else if (json.source && json.source["osmTags"] === undefined) {
 | 
			
		||||
                context
 | 
			
		||||
                    .enters("source", "osmTags")
 | 
			
		||||
                    .err(
 | 
			
		||||
                        "No osmTags defined in the source section - these should always be present, even for geojson layer"
 | 
			
		||||
                    )
 | 
			
		||||
            } else {
 | 
			
		||||
                const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
 | 
			
		||||
                if (osmTags.isNegative()) {
 | 
			
		||||
                    context
 | 
			
		||||
                        .enters("source", "osmTags")
 | 
			
		||||
                        .err(
 | 
			
		||||
                            "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) {
 | 
			
		||||
                context
 | 
			
		||||
                    .enters("source", "geoJsonSource")
 | 
			
		||||
                    .err("Use 'geoJson' instead of 'geoJsonSource'")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (json.source["geojson"] !== undefined) {
 | 
			
		||||
                context
 | 
			
		||||
                    .enters("source", "geojson")
 | 
			
		||||
                    .err("Use 'geoJson' instead of 'geojson' (the J is a capital letter)")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (json.id?.toLowerCase() !== json.id) {
 | 
			
		||||
            context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
 | 
			
		||||
        }
 | 
			
		||||
        if (json.id?.match(/[a-z0-9-_]/) == null) {
 | 
			
		||||
            context.enter("id").err(`The id of a layer should match [a-z0-9-_]*: ${json.id}`)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            json.syncSelection !== undefined &&
 | 
			
		||||
            LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
 | 
			
		||||
        ) {
 | 
			
		||||
            context
 | 
			
		||||
                .enter("syncSelection")
 | 
			
		||||
                .err(
 | 
			
		||||
                    "Invalid sync-selection: must be one of " +
 | 
			
		||||
                        LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
 | 
			
		||||
                        " but got '" +
 | 
			
		||||
                        json.syncSelection +
 | 
			
		||||
                        "'"
 | 
			
		||||
                )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (context.hasErrors()) {
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
        let layerConfig: LayerConfig
 | 
			
		||||
        try {
 | 
			
		||||
            layerConfig = new LayerConfig(json, "validation", true)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -136,7 +136,7 @@ export interface LayerConfigJson {
 | 
			
		|||
     * There are a few extra functions available. Refer to <a>Docs/CalculatedTags.md</a> for more information
 | 
			
		||||
     * The functions will be run in order, e.g.
 | 
			
		||||
     * [
 | 
			
		||||
     *  "_max_overlap_m2=Math.max(...feat.overlapsWith("someOtherLayer").map(o => o.overlap))
 | 
			
		||||
 Not found...    *  "_max_overlap_m2=Math.max(...feat.overlapsWith("someOtherLayer").map(o => o.overlap))
 | 
			
		||||
     *  "_max_overlap_ratio=Number(feat._max_overlap_m2)/feat.area
 | 
			
		||||
     * ]
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			@ -208,6 +208,7 @@ export interface LayerConfigJson {
 | 
			
		|||
     * types: use a fixed translation ; Use a dynamic tagRendering ; hidden
 | 
			
		||||
     * typesdefault: 1
 | 
			
		||||
     * type: translation
 | 
			
		||||
     * inline: {translated{value}}
 | 
			
		||||
     */
 | 
			
		||||
    title?: TagRenderingConfigJson | Translatable
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -309,7 +310,7 @@ 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!
 | 
			
		||||
         *
 | 
			
		||||
         * question: What is the word to describe this object?
 | 
			
		||||
         * inline: Add <b>{value}</b> here
 | 
			
		||||
         * inline: Add <b>{translated(value)}</b> here
 | 
			
		||||
         */
 | 
			
		||||
        title: Translatable
 | 
			
		||||
        /**
 | 
			
		||||
| 
						 | 
				
			
			@ -360,7 +361,7 @@ export interface LayerConfigJson {
 | 
			
		|||
         * If further away, it'll be placed in the center of the location input
 | 
			
		||||
         * Distance in meter
 | 
			
		||||
         *
 | 
			
		||||
         * Default: 10
 | 
			
		||||
         * ifunset: Do not snap to a layer
 | 
			
		||||
         */
 | 
			
		||||
        maxSnapDistance?: number
 | 
			
		||||
    }[]
 | 
			
		||||
| 
						 | 
				
			
			@ -463,6 +464,7 @@ export interface LayerConfigJson {
 | 
			
		|||
     * types: Use an advanced delete configuration ; boolean
 | 
			
		||||
     * iftrue: Allow deletion
 | 
			
		||||
     * iffalse: Do not allow deletion
 | 
			
		||||
     * ifunset: Do not allow deletion
 | 
			
		||||
     *
 | 
			
		||||
     **/
 | 
			
		||||
    deletion?: DeleteConfigJson | boolean
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,6 @@ export default interface LineRenderingConfigJson {
 | 
			
		|||
     * types: dynamic value ; string
 | 
			
		||||
     * title: Line Colour
 | 
			
		||||
     * inline: The line colour always is <b>{value}</b>
 | 
			
		||||
     * ifunset: Round ending
 | 
			
		||||
     * type: color
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,6 +91,7 @@ export default interface PointRenderingConfigJson {
 | 
			
		|||
     * If the icon is undefined, then the label is shown in the center of the feature.
 | 
			
		||||
     * types: Dynamic value | string
 | 
			
		||||
     * inline: Always show label <b>{value}</b> beneath the marker
 | 
			
		||||
     * ifunset: Do not show a label beneath the marker
 | 
			
		||||
     */
 | 
			
		||||
    label?: string | TagRenderingConfigJson
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -100,6 +101,8 @@ export default interface PointRenderingConfigJson {
 | 
			
		|||
     * This will be applied to the _container_ containing both the marker and the label
 | 
			
		||||
     * inline: Apply CSS-style <b>{value}</b> to the _entire marker_
 | 
			
		||||
     * types: Dynamic value ; string
 | 
			
		||||
     * ifunset: Do not apply extra CSS element to the entire marker
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    css?: string | TagRenderingConfigJson
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -111,7 +114,9 @@ export default interface PointRenderingConfigJson {
 | 
			
		|||
     * You can use most Tailwind-css classes, see https://tailwindcss.com/ for more information
 | 
			
		||||
     * For example: `center bg-gray-500 mx-2 my-1 rounded-full`
 | 
			
		||||
     * inline: Apply CSS-classes <b>{value}</b> to the entire container
 | 
			
		||||
     * ifunset: Do not apply extra CSS-classes to the label
 | 
			
		||||
     * types: Dynamic value ; string
 | 
			
		||||
     * ifunset: Do not apply extra CSS-classes to the entire marker
 | 
			
		||||
     */
 | 
			
		||||
    cssClasses?: string | TagRenderingConfigJson
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -120,6 +125,8 @@ export default interface PointRenderingConfigJson {
 | 
			
		|||
     * You can set the css-properties here, e.g. `background: red; font-size: 12px; `
 | 
			
		||||
     * inline: Apply CSS-style <b>{value}</b> to the label
 | 
			
		||||
     * types: Dynamic value ; string
 | 
			
		||||
     * ifunset: Do not apply extra CSS-labels to the label
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    labelCss?: TagRenderingConfigJson | string
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -131,6 +138,7 @@ export default interface PointRenderingConfigJson {
 | 
			
		|||
     * For example: `center bg-gray-500 mx-2 my-1 rounded-full`
 | 
			
		||||
     * inline: Apply CSS-classes <b>{value}</b> to the label
 | 
			
		||||
     * types: Dynamic value ; string
 | 
			
		||||
     * ifunset: Do not apply extra CSS-classes to the label
 | 
			
		||||
     */
 | 
			
		||||
    labelCssClasses?: string | TagRenderingConfigJson
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -187,7 +187,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
 | 
			
		|||
     *
 | 
			
		||||
     * question: Should a contributor be allowed to select multiple mappings?
 | 
			
		||||
     *
 | 
			
		||||
     * iftrue: allow to select multiple mappigns
 | 
			
		||||
     * iftrue: allow to select multiple mappings
 | 
			
		||||
     * iffalse: only allow to select a single mapping
 | 
			
		||||
     * ifunset: only allow to select a single mapping
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -250,7 +250,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
 | 
			
		|||
         * For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked
 | 
			
		||||
         * ifunset: The question will be considered answered if any value is set for the key
 | 
			
		||||
         */
 | 
			
		||||
        invalidValues?: string[]
 | 
			
		||||
        invalidValues?: TagConfigJson
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,65 +75,17 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
        const translationContext = "layers:" + json.id
 | 
			
		||||
        super(json, context)
 | 
			
		||||
        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})`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (json.source === undefined) {
 | 
			
		||||
            throw "Layer " + this.id + " does not define a source section (" + context + ")"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (json.source === "special" || json.source === "special:library") {
 | 
			
		||||
            this.source = null
 | 
			
		||||
        } else 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 +
 | 
			
		||||
                ")"
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (json.id.toLowerCase() !== json.id) {
 | 
			
		||||
            throw `${context}: The id of a layer should be lowercase: ${json.id}`
 | 
			
		||||
        }
 | 
			
		||||
        if (json.id.match(/[a-z0-9-_]/) == null) {
 | 
			
		||||
            throw `${context}: The id of a layer should match [a-z0-9-_]*: ${json.id}`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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"
 | 
			
		||||
        if (typeof json.source !== "string") {
 | 
			
		||||
            this.maxAgeOfCache = json.source["maxCacheAge"] ?? 24 * 60 * 60 * 30
 | 
			
		||||
            const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
 | 
			
		||||
            if (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, {})
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.source = new SourceConfig(
 | 
			
		||||
                {
 | 
			
		||||
                    osmTags: osmTags,
 | 
			
		||||
                    osmTags: TagUtils.Tag(json.source["osmTags"], context + "source.osmTags"),
 | 
			
		||||
                    geojsonSource: json.source["geoJson"],
 | 
			
		||||
                    geojsonSourceLevel: json.source["geoJsonZoomLevel"],
 | 
			
		||||
                    overpassScript: json.source["overpassScript"],
 | 
			
		||||
| 
						 | 
				
			
			@ -145,14 +97,6 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (json.source["geoJsonSource"] !== undefined) {
 | 
			
		||||
            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)"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.allowSplit = json.allowSplit ?? false
 | 
			
		||||
        this.name = Translations.T(json.name, translationContext + ".name")
 | 
			
		||||
        if (json.units !== undefined && !Array.isArray(json.units)) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,6 +53,7 @@ export default class TagRenderingConfig {
 | 
			
		|||
    public readonly question?: TypedTranslation<object>
 | 
			
		||||
    public readonly questionhint?: TypedTranslation<object>
 | 
			
		||||
    public readonly condition?: TagsFilter
 | 
			
		||||
    public readonly invalidValues?: TagsFilter
 | 
			
		||||
    /**
 | 
			
		||||
     * Evaluated against the current 'usersettings'-state
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -133,6 +134,9 @@ export default class TagRenderingConfig {
 | 
			
		|||
        this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
 | 
			
		||||
        this.description = Translations.T(json.description, translationKey + ".description")
 | 
			
		||||
        this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
 | 
			
		||||
        this.invalidValues = json["invalidValues"]
 | 
			
		||||
            ? TagUtils.Tag(json["invalidValues"], `${context}.invalidValues`)
 | 
			
		||||
            : undefined
 | 
			
		||||
        if (typeof json.icon === "string") {
 | 
			
		||||
            this.renderIcon = json.icon
 | 
			
		||||
            this.renderIconClass = "small"
 | 
			
		||||
| 
						 | 
				
			
			@ -469,9 +473,12 @@ export default class TagRenderingConfig {
 | 
			
		|||
     */
 | 
			
		||||
    public IsKnown(tags: Record<string, string>): boolean {
 | 
			
		||||
        if (this.condition && !this.condition.matchesProperties(tags)) {
 | 
			
		||||
            // Filtered away by the condition, so it is kindof known
 | 
			
		||||
            // Filtered away by the condition, so it is kind of known
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        if (this.invalidValues && this.invalidValues.matchesProperties(tags)) {
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
        if (this.multiAnswer) {
 | 
			
		||||
            for (const m of this.mappings ?? []) {
 | 
			
		||||
                if (TagUtils.MatchesMultiAnswer(m.if, tags)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -482,6 +489,9 @@ export default class TagRenderingConfig {
 | 
			
		|||
            const free = this.freeform?.key
 | 
			
		||||
            if (free !== undefined) {
 | 
			
		||||
                const value = tags[free]
 | 
			
		||||
                if (typeof value === "object") {
 | 
			
		||||
                    return Object.keys(value).length > 0
 | 
			
		||||
                }
 | 
			
		||||
                return value !== undefined && value !== ""
 | 
			
		||||
            }
 | 
			
		||||
            return false
 | 
			
		||||
| 
						 | 
				
			
			@ -679,7 +689,9 @@ export default class TagRenderingConfig {
 | 
			
		|||
        multiSelectedMapping: boolean[] | undefined,
 | 
			
		||||
        currentProperties: Record<string, string>
 | 
			
		||||
    ): UploadableTag {
 | 
			
		||||
        freeformValue = freeformValue?.trim()
 | 
			
		||||
        if (typeof freeformValue === "string") {
 | 
			
		||||
            freeformValue = freeformValue?.trim()
 | 
			
		||||
        }
 | 
			
		||||
        const validator = Validators.get(<ValidatorType>this.freeform?.type)
 | 
			
		||||
        if (validator && freeformValue) {
 | 
			
		||||
            freeformValue = validator.reformat(freeformValue, () => currentProperties["_country"])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue