From ff0ee35ec11ec57c62292638d42e67815227cb32 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 22 Oct 2021 18:53:07 +0200 Subject: [PATCH] First usable sidewalks theme --- Logic/FeatureSource/FeaturePipeline.ts | 2 +- .../RenderingMultiPlexerFeatureSource.ts | 107 +++++++++++++++ .../WayHandlingApplyingFeatureSource.ts | 65 --------- Logic/State/ElementsState.ts | 2 +- Logic/UIEventSource.ts | 14 ++ Models/ThemeConfig/Json/LayerConfigJson.ts | 7 +- .../Json/PointRenderingConfigJson.ts | 2 +- .../Json/TagRenderingConfigJson.ts | 6 + Models/ThemeConfig/LayerConfig.ts | 128 +++++++++++++++--- Models/ThemeConfig/PointRenderingConfig.ts | 2 +- Models/ThemeConfig/TagRenderingConfig.ts | 5 +- Models/ThemeConfig/WithContextLoader.ts | 60 ++++---- UI/Input/ValidatedTextField.ts | 3 +- UI/Popup/FeatureInfoBox.ts | 24 ++-- UI/ShowDataLayer/ShowDataLayer.ts | 17 ++- UI/SpecialVisualizations.ts | 119 ++++++++++++---- .../left_right_style/left_right_style.json | 41 ++++++ assets/themes/sidewalks/sidewalks.json | 105 +++++++++++++- scripts/lint.ts | 2 +- 19 files changed, 537 insertions(+), 174 deletions(-) create mode 100644 Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts delete mode 100644 Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts create mode 100644 assets/layers/left_right_style/left_right_style.json diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 9aa8937c3c..1d2636d902 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -12,7 +12,7 @@ import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; import {Changes} from "../Osm/Changes"; import GeoJsonSource from "./Sources/GeoJsonSource"; import Loc from "../../Models/Loc"; -import WayHandlingApplyingFeatureSource from "./Sources/WayHandlingApplyingFeatureSource"; +import WayHandlingApplyingFeatureSource from "./Sources/RenderingMultiPlexerFeatureSource"; import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor"; import TiledFromLocalStorageSource from "./TiledFeatureSource/TiledFromLocalStorageSource"; import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor"; diff --git a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts new file mode 100644 index 0000000000..5d2161a4e9 --- /dev/null +++ b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts @@ -0,0 +1,107 @@ +/** + * This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indiciates with what renderConfig it should be rendered. + */ +import {UIEventSource} from "../../UIEventSource"; +import {GeoOperations} from "../../GeoOperations"; +import FeatureSource from "../FeatureSource"; +import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig"; +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; + + +export default class RenderingMultiPlexerFeatureSource { + public readonly features: UIEventSource<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>; + + constructor(upstream: FeatureSource, layer: LayerConfig) { + this.features = upstream.features.map( + features => { + if (features === undefined) { + return; + } + + const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({ + rendering: r, + index: i + })) + const pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point")) + const centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid")) + const startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start")) + const endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end")) + + const lineRenderObjects = layer.lineRendering + + const withIndex: (any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[] = []; + + + function addAsPoint(feat, rendering, coordinate) { + const patched = { + ...feat, + pointRenderingIndex: rendering.index + } + patched.geometry = { + type: "Point", + coordinates: coordinate + } + withIndex.push(patched) + } + + for (const f of features) { + const feat = f.feature; + + + if (feat.geometry.type === "Point") { + + for (const rendering of pointRenderings) { + withIndex.push({ + ...feat, + pointRenderingIndex: rendering.index + }) + } + } else { + // This is a a line + for (const rendering of centroidRenderings) { + addAsPoint(feat, rendering, GeoOperations.centerpointCoordinates(feat)) + } + + if (feat.geometry.type === "LineString") { + const coordinates = feat.geometry.coordinates + for (const rendering of startRenderings) { + addAsPoint(feat, rendering, coordinates[0]) + } + for (const rendering of endRenderings) { + const coordinate = coordinates[coordinates.length - 1] + addAsPoint(feat, rendering, coordinate) + } + } + + if (feat.geometry.type === "MultiLineString") { + const lineList = feat.geometry.coordinates + for (const coordinates of lineList) { + + for (const rendering of startRenderings) { + const coordinate = coordinates[0] + addAsPoint(feat, rendering, coordinate) + } + for (const rendering of endRenderings) { + const coordinate = coordinates[coordinates.length - 1] + addAsPoint(feat, rendering, coordinate) + } + } + } + + + for (let i = 0; i < lineRenderObjects.length; i++) { + withIndex.push({ + ...feat, + lineRenderingIndex: i + }) + } + + } + } + return withIndex; + } + ); + + } + +} \ No newline at end of file diff --git a/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts b/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts deleted file mode 100644 index f996ad9a0c..0000000000 --- a/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indiciates with what renderConfig it should be rendered. - */ -import {UIEventSource} from "../../UIEventSource"; -import {GeoOperations} from "../../GeoOperations"; -import FeatureSource from "../FeatureSource"; -import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig"; -import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; - - -export default class RenderingMultiPlexerFeatureSource { - public readonly features: UIEventSource<(any & {pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined})[]>; - - constructor(upstream: FeatureSource, layer: LayerConfig) { - this.features = upstream.features.map( - features => { - if (features === undefined) { - return; - } - - const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({rendering: r, index: i})) - const pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point")) - const centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid")) - - const lineRenderObjects = layer.lineRendering - - const withIndex : (any & {pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined})[] = []; - - for (const f of features) { - const feat = f.feature; - - if(feat.geometry.type === "Point"){ - - for (const rendering of pointRenderings) { - withIndex.push({ - ...feat, - pointRenderingIndex: rendering.index - }) - } - }else{ - // This is a a line - for (const rendering of centroidRenderings) { - withIndex.push({ - ...GeoOperations.centerpoint(feat), - pointRenderingIndex: rendering.index - }) - } - - - for (let i = 0; i < lineRenderObjects.length; i++){ - withIndex.push({ - ...feat, - lineRenderingIndex:i - }) - } - - } - } - return withIndex; - } - ); - - } - -} \ No newline at end of file diff --git a/Logic/State/ElementsState.ts b/Logic/State/ElementsState.ts index a4674430b0..637482e60b 100644 --- a/Logic/State/ElementsState.ts +++ b/Logic/State/ElementsState.ts @@ -49,7 +49,7 @@ export default class ElementsState extends FeatureSwitchState { constructor(layoutToUse: LayoutConfig) { super(layoutToUse); - this.changes = new Changes(layoutToUse.isLeftRightSensitive()) + this.changes = new Changes(layoutToUse?.isLeftRightSensitive() ?? false) { // -- Location control initialization diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index 571c74f8e3..8051128ce6 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -109,6 +109,20 @@ export class UIEventSource { promise?.catch(err => src.setData({error: err})) return src } + + public withEqualityStabilized(comparator: (t:T | undefined, t1:T | undefined) => boolean): UIEventSource{ + let oldValue = undefined; + return this.map(v => { + if(v == oldValue){ + return oldValue + } + if(comparator(oldValue, v)){ + return oldValue + } + oldValue = v; + return v; + }) + } /** * Given a UIEVentSource with a list, returns a new UIEventSource which is only updated if the _contents_ of the list are different. diff --git a/Models/ThemeConfig/Json/LayerConfigJson.ts b/Models/ThemeConfig/Json/LayerConfigJson.ts index 353cfcce58..09e8284a4a 100644 --- a/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -197,7 +197,12 @@ export interface LayerConfigJson { * A special value is 'questions', which indicates the location of the questions box. If not specified, it'll be appended to the bottom of the featureInfobox. * */ - tagRenderings?: (string | {builtin: string, override: any} | TagRenderingConfigJson) [], + tagRenderings?: (string | {builtin: string, override: any} | TagRenderingConfigJson | { + leftRightKeys: string[], + renderings: (string | {builtin: string, override: any} | TagRenderingConfigJson)[] + }) [], + + /** diff --git a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts index 1ab46c8d04..45f34b7556 100644 --- a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts @@ -15,7 +15,7 @@ export default interface PointRenderingConfigJson { * All the locations that this point should be rendered at. * Using `location: ["point", "centroid"] will always render centerpoint */ - location: ("point" | "centroid")[] + location: ("point" | "centroid" | "start" | "end")[] /** * The icon for an element. diff --git a/Models/ThemeConfig/Json/TagRenderingConfigJson.ts b/Models/ThemeConfig/Json/TagRenderingConfigJson.ts index f834ec19b6..c2aa3d3238 100644 --- a/Models/ThemeConfig/Json/TagRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/TagRenderingConfigJson.ts @@ -11,6 +11,12 @@ export interface TagRenderingConfigJson { * Used to keep the translations in sync. Only used in the tagRenderings-array of a layerConfig, not requered otherwise */ id?: string, + + /** + * Optional: this can group questions together in one question box. + * Written by 'left-right'-keys automatically + */ + group?: string /** * Renders this value. Note that "{key}"-parts are substituted by the corresponding values of the element. diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index f9888f9b0a..6518caca94 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -15,8 +15,9 @@ import WithContextLoader from "./WithContextLoader"; import LineRenderingConfig from "./LineRenderingConfig"; import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"; import LineRenderingConfigJson from "./Json/LineRenderingConfigJson"; +import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson"; -export default class LayerConfig extends WithContextLoader{ +export default class LayerConfig extends WithContextLoader { id: string; name: Translation; @@ -31,7 +32,7 @@ export default class LayerConfig extends WithContextLoader{ maxzoom: number; title?: TagRenderingConfig; titleIcons: TagRenderingConfig[]; - + public readonly mapRendering: PointRenderingConfig[] public readonly lineRendering: LineRenderingConfig[] @@ -82,7 +83,7 @@ export default class LayerConfig extends WithContextLoader{ throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)"; } - this. source = new SourceConfig( + this.source = new SourceConfig( { osmTags: osmTags, geojsonSource: json.source["geoJson"], @@ -92,14 +93,15 @@ export default class LayerConfig extends WithContextLoader{ }, json.id ); + }else if(legacy === undefined){ + throw "No valid source defined ("+context+")" } else { - this. source = new SourceConfig({ + this.source = new SourceConfig({ osmTags: legacy, }); } - this.id = json.id; this.allowSplit = json.allowSplit ?? false; this.name = Translations.T(json.name, context + ".name"); @@ -191,22 +193,21 @@ export default class LayerConfig extends WithContextLoader{ return config; }); - if(json.mapRendering === undefined){ - throw "MapRendering is undefined in "+context + if (json.mapRendering === undefined) { + throw "MapRendering is undefined in " + context } this.mapRendering = json.mapRendering .filter(r => r["icon"] !== undefined || r["label"] !== undefined) - .map((r, i) => new PointRenderingConfig(r, context+".mapRendering["+i+"]")) + .map((r, i) => new PointRenderingConfig(r, context + ".mapRendering[" + i + "]")) this.lineRendering = json.mapRendering .filter(r => r["icon"] === undefined && r["label"] === undefined) - .map((r, i) => new LineRenderingConfig(r, context+".mapRendering["+i+"]")) + .map((r, i) => new LineRenderingConfig(r, context + ".mapRendering[" + i + "]")) - this.tagRenderings = this.trs(json.tagRenderings, false); - - const missingIds = json.tagRenderings?.filter(tr => typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined) ?? []; + this.tagRenderings = this.ExtractLayerTagRenderings(json) + const missingIds = json.tagRenderings?.filter(tr => typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined && tr["leftRightKeys"] === undefined) ?? []; if (missingIds.length > 0 && official) { console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds) @@ -237,11 +238,11 @@ export default class LayerConfig extends WithContextLoader{ } } - this.titleIcons = this.trs(titleIcons, true); + this.titleIcons = this.ParseTagRenderings(titleIcons, true); this.title = this.tr("title", undefined); this.isShown = this.tr("isShown", "yes"); - + this.deletion = null; if (json.deletion === true) { json.deletion = {}; @@ -269,6 +270,98 @@ export default class LayerConfig extends WithContextLoader{ } } + public ExtractLayerTagRenderings(json: LayerConfigJson): TagRenderingConfig[] { + + if (json.tagRenderings === undefined) { + return [] + } + + const normalTagRenderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[] = [] + const leftRightRenderings: ({ leftRightKeys: string[], renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[] })[] = [] + for (let i = 0; i < json.tagRenderings.length; i++) { + const tr = json.tagRenderings[i]; + const lrkDefined = tr["leftRightKeys"] !== undefined + const renderingsDefined = tr["renderings"] + + if (!lrkDefined && !renderingsDefined) { + // @ts-ignore + normalTagRenderings.push(tr) + continue + } + if (lrkDefined && renderingsDefined) { + // @ts-ignore + leftRightRenderings.push(tr) + continue + } + throw `Error in ${this._context}.tagrenderings[${i}]: got a value which defines either \`leftRightKeys\` or \`renderings\`, but not both. Either define both or move the \`renderings\` out of this scope` + } + + const allRenderings = this.ParseTagRenderings(normalTagRenderings, false); + + if(leftRightRenderings.length === 0){ + return allRenderings + } + + const leftRenderings : TagRenderingConfig[] = [] + const rightRenderings : TagRenderingConfig[] = [] + + function prepConfig(target:string, tr: TagRenderingConfigJson){ + + function replaceRecursive(transl: string | any){ + if(typeof transl === "string"){ + return transl.replace("left|right", target) + } + if(transl.map !== undefined){ + return transl.map(o => replaceRecursive(o)) + } + transl = {...transl} + for (const key in transl) { + transl[key] = replaceRecursive(transl[key]) + } + return transl + } + + const orig = tr; + tr = replaceRecursive(tr) + + tr.id = target+"-"+orig.id + tr.group = target + return tr + } + + + for (const leftRightRendering of leftRightRenderings) { + + const keysToRewrite = leftRightRendering.leftRightKeys + const tagRenderings = leftRightRendering.renderings + + const left = this.ParseTagRenderings(tagRenderings, false, tr => prepConfig("left", tr)) + const right = this.ParseTagRenderings(tagRenderings, false, tr => prepConfig("right", tr)) + + leftRenderings.push(...left) + rightRenderings.push(...right) + + } + + leftRenderings.push(new TagRenderingConfig({ + id: "questions", + group:"left", + }, "layerConfig.ts.leftQuestionBox")) + + rightRenderings.push(new TagRenderingConfig({ + id: "questions", + group:"right", + }, "layerConfig.ts.rightQuestionBox")) + + allRenderings.push(...leftRenderings) + allRenderings.push(...rightRenderings) + + + return allRenderings; + + } + + public CustomCodeSnippets(): string[] { if (this.calculatedTags === undefined) { return []; @@ -277,8 +370,6 @@ export default class LayerConfig extends WithContextLoader{ } - - public ExtractImages(): Set { const parts: Set[] = []; parts.push(...this.tagRenderings?.map((tr) => tr.ExtractImages(false))); @@ -293,12 +384,11 @@ export default class LayerConfig extends WithContextLoader{ for (const part of parts) { part?.forEach(allIcons.add, allIcons); } - return allIcons; } - - public isLeftRightSensitive() : boolean{ + + public isLeftRightSensitive(): boolean { return this.lineRendering.some(lr => lr.leftRightSensitive) } } \ No newline at end of file diff --git a/Models/ThemeConfig/PointRenderingConfig.ts b/Models/ThemeConfig/PointRenderingConfig.ts index e50891c8e4..e34d6e34c0 100644 --- a/Models/ThemeConfig/PointRenderingConfig.ts +++ b/Models/ThemeConfig/PointRenderingConfig.ts @@ -15,7 +15,7 @@ import {VariableUiElement} from "../../UI/Base/VariableUIElement"; export default class PointRenderingConfig extends WithContextLoader { - public readonly location: Set<"point" | "centroid"> + public readonly location: Set<"point" | "centroid" | "start" | "end"> public readonly icon: TagRenderingConfig; public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[]; diff --git a/Models/ThemeConfig/TagRenderingConfig.ts b/Models/ThemeConfig/TagRenderingConfig.ts index 51d3e9f537..5f223e6d63 100644 --- a/Models/ThemeConfig/TagRenderingConfig.ts +++ b/Models/ThemeConfig/TagRenderingConfig.ts @@ -14,6 +14,7 @@ import {Utils} from "../../Utils"; export default class TagRenderingConfig { readonly id: string; + readonly group: string; readonly render?: Translation; readonly question?: Translation; readonly condition?: TagsFilter; @@ -46,7 +47,8 @@ export default class TagRenderingConfig { this.question = null; this.condition = null; } - + + if(typeof json === "number"){ this.render = Translations.WT( ""+json) return; @@ -64,6 +66,7 @@ export default class TagRenderingConfig { this.id = json.id ?? ""; + this.group = json.group ?? ""; this.render = Translations.T(json.render, context + ".render"); this.question = Translations.T(json.question, context + ".question"); this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`); diff --git a/Models/ThemeConfig/WithContextLoader.ts b/Models/ThemeConfig/WithContextLoader.ts index cfdc5439b1..0cab6616c5 100644 --- a/Models/ThemeConfig/WithContextLoader.ts +++ b/Models/ThemeConfig/WithContextLoader.ts @@ -5,8 +5,8 @@ import {Utils} from "../../Utils"; export default class WithContextLoader { private readonly _json: any; - private readonly _context: string; - + protected readonly _context: string; + constructor(json: any, context: string) { this._json = json; this._context = context; @@ -43,20 +43,22 @@ export default class WithContextLoader { * Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig * A string is interpreted as a name to call */ - public trs( + public ParseTagRenderings( tagRenderings?: (string | { builtin: string, override: any } | TagRenderingConfigJson)[], - readOnly = false + readOnly = false, + prepConfig: ((config: TagRenderingConfigJson) => TagRenderingConfigJson) = undefined ) { if (tagRenderings === undefined) { return []; } - - const context = this._context - - const renderings: TagRenderingConfig[] = [] + const context = this._context + const renderings: TagRenderingConfig[] = [] + if (prepConfig === undefined) { + prepConfig = c => c + } for (let i = 0; i < tagRenderings.length; i++) { - let renderingJson= tagRenderings[i] + let renderingJson = tagRenderings[i] if (typeof renderingJson === "string") { renderingJson = {builtin: renderingJson, override: undefined} } @@ -70,41 +72,29 @@ export default class WithContextLoader { )}`; } - const tr = new TagRenderingConfig("questions", context); - renderings.push(tr) + const tr = new TagRenderingConfig("questions", context); + renderings.push(tr) continue; } if (renderingJson["override"] !== undefined) { - const sharedJson = SharedTagRenderings.SharedTagRenderingJson.get(renderingId) - const tr = new TagRenderingConfig( - Utils.Merge(renderingJson["override"], sharedJson), - `${context}.tagrendering[${i}]+override` - ); - renderings.push(tr) - continue - } + let sharedJson = SharedTagRenderings.SharedTagRenderingJson.get(renderingId) - const shared = SharedTagRenderings.SharedTagRendering.get(renderingId); + if (sharedJson === undefined) { + const keys = Array.from(SharedTagRenderings.SharedTagRenderingJson.keys()); + throw `Predefined tagRendering ${renderingId} not found in ${context}.\n Try one of ${keys.join( + ", " + )}\n If you intent to output this text literally, use {\"render\": } instead"}`; + } - if (shared !== undefined) { - renderings.push( shared) - continue + renderingJson = Utils.Merge(renderingJson["override"], sharedJson) } - if (Utils.runningFromConsole) { - continue - } - - const keys = Array.from( SharedTagRenderings.SharedTagRendering.keys() ); - throw `Predefined tagRendering ${renderingId} not found in ${context}.\n Try one of ${keys.join( - ", " - )}\n If you intent to output this text literally, use {\"render\": } instead"}`; } - const tr = new TagRenderingConfig( - renderingJson, - `${context}.tagrendering[${i}]` - ); + + const patchedConfig = prepConfig(renderingJson) + + const tr = new TagRenderingConfig(patchedConfig, `${context}.tagrendering[${i}]`); renderings.push(tr) } diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index ad55543462..78d543657d 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -117,7 +117,8 @@ export default class ValidatedTextField { if (args[0]) { zoom = Number(args[0]) if (isNaN(zoom)) { - throw "Invalid zoom level for argument at 'length'-input" + console.error("Invalid zoom level for argument at 'length'-input. The offending argument is: ",args[0]," (using 19 instead)") + zoom = 19 } } diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index c31eff3958..dde27d9c1b 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -18,6 +18,7 @@ import {Translation} from "../i18n/Translation"; import {Utils} from "../../Utils"; import {SubstitutedTranslation} from "../SubstitutedTranslation"; import MoveWizard from "./MoveWizard"; +import {FixedUiElement} from "../Base/FixedUiElement"; export default class FeatureInfoBox extends ScrollableFullScreen { @@ -52,26 +53,33 @@ export default class FeatureInfoBox extends ScrollableFullScreen { private static GenerateContent(tags: UIEventSource, layerConfig: LayerConfig): BaseUIElement { - let questionBox: BaseUIElement = undefined; + let questionBoxes: Map = new Map(); if (State.state.featureSwitchUserbadge.data) { - questionBox = new QuestionBox(tags, layerConfig.tagRenderings, layerConfig.units); + const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map(tr => tr.group)) + for (const groupName of allGroupNames) { + const questions = layerConfig.tagRenderings.filter(tr => tr.group === groupName) + const questionBox = new QuestionBox(tags, questions, layerConfig.units); + console.log("Groupname:", groupName) + questionBoxes.set(groupName, questionBox) + } } - let questionBoxIsUsed = false; const renderings: BaseUIElement[] = layerConfig.tagRenderings.map(tr => { - if (tr.question === null) { - // This is the question box! - questionBoxIsUsed = true; + if (tr.question === null || tr.id === "questions") { + console.log("Rendering is", tr) + // This is a question box! + const questionBox = questionBoxes.get(tr.group) + questionBoxes.delete(tr.group) return questionBox; } return new EditableTagRendering(tags, tr, layerConfig.units); }); let editElements: BaseUIElement[] = [] - if (!questionBoxIsUsed) { + questionBoxes.forEach(questionBox => { editElements.push(questionBox); - } + }) if(layerConfig.allowMove) { editElements.push( diff --git a/UI/ShowDataLayer/ShowDataLayer.ts b/UI/ShowDataLayer/ShowDataLayer.ts index 00f04c1995..589d2f726b 100644 --- a/UI/ShowDataLayer/ShowDataLayer.ts +++ b/UI/ShowDataLayer/ShowDataLayer.ts @@ -4,7 +4,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import FeatureInfoBox from "../Popup/FeatureInfoBox"; import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; import {ElementStorage} from "../../Logic/ElementStorage"; -import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource"; +import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource"; /* // import 'leaflet-polylineoffset'; We don't actually import it here. It is imported in the 'MinimapImplementation'-class, which'll result in a patched 'L' object. @@ -161,7 +161,18 @@ export default class ShowDataLayer { const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates) const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource(feat.properties); let offsettedLine; - tagsSource.map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags)).addCallbackAndRunD(lineStyle => { + tagsSource + .map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags)) + .withEqualityStabilized((a, b) => { + if(a === b){ + return true + } + if(a === undefined || b === undefined){ + return false + } + return a.offset === b.offset && a.color === b.color && a.weight === b.weight && a.dashArray === b.dashArray + }) + .addCallbackAndRunD(lineStyle => { if (offsettedLine !== undefined) { self.geoLayer.removeLayer(offsettedLine) } @@ -261,7 +272,7 @@ export default class ShowDataLayer { let infobox: FeatureInfoBox = undefined; - const id = `popup-${feature.properties.id}-${feature.geometry.type}-${this.showDataLayerid}-${this._cleanCount}-${feature.pointerRenderingIndex ?? feature.lineRenderingIndex}` + const id = `popup-${feature.properties.id}-${feature.geometry.type}-${this.showDataLayerid}-${this._cleanCount}-${feature.pointRenderingIndex ?? feature.lineRenderingIndex}` popup.setContent(`
Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading
`) leafletLayer.on("popupopen", () => { if (infobox === undefined) { diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 1882152965..6d0784b7d9 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -29,6 +29,8 @@ import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"; import WikipediaBox from "./Wikipedia/WikipediaBox"; import SimpleMetaTagger from "../Logic/SimpleMetaTagger"; import MultiApply from "./Popup/MultiApply"; +import AllKnownLayers from "../Customizations/AllKnownLayers"; +import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; export interface SpecialVisualization { funcName: string, @@ -49,7 +51,7 @@ export default class SpecialVisualizations { constr: ((state: State, tags: UIEventSource) => { const calculatedTags = [].concat( SimpleMetaTagger.lazyTags, - ... state.layoutToUse.layers.map(l => l.calculatedTags?.map(c => c[0]) ?? [])) + ...state.layoutToUse.layers.map(l => l.calculatedTags?.map(c => c[0]) ?? [])) return new VariableUiElement(tags.map(tags => { const parts = []; for (const key in tags) { @@ -57,20 +59,20 @@ export default class SpecialVisualizations { continue } let v = tags[key] - if(v === ""){ + if (v === "") { v = "empty string" } parts.push([key, v ?? "undefined"]); } - - for(const key of calculatedTags){ + + for (const key of calculatedTags) { const value = tags[key] - if(value === undefined){ + if (value === undefined) { continue } - parts.push([ ""+key+"", value ]) + parts.push(["" + key + "", value]) } - + return new Table( ["key", "value"], parts @@ -88,7 +90,7 @@ export default class SpecialVisualizations { }], constr: (state: State, tags, args) => { let imagePrefixes: string[] = undefined; - if(args.length > 0){ + if (args.length > 0) { imagePrefixes = [].concat(...args.map(a => a.split(","))); } return new ImageCarousel(AllImageProviders.LoadImagesFor(tags, imagePrefixes), tags, imagePrefixes); @@ -101,9 +103,9 @@ export default class SpecialVisualizations { name: "image-key", doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", defaultValue: "image" - },{ - name:"label", - doc:"The text to show on the button", + }, { + name: "label", + doc: "The text to show on the button", defaultValue: "Add image" }], constr: (state: State, tags, args) => { @@ -125,17 +127,16 @@ export default class SpecialVisualizations { new VariableUiElement( tagsSource.map(tags => tags[args[0]]) .map(wikidata => { - const wikidatas : string[] = + const wikidatas: string[] = Utils.NoEmpty(wikidata?.split(";")?.map(wd => wd.trim()) ?? []) return new WikipediaBox(wikidatas) }) - ) - + }, { funcName: "minimap", - docs: "A small map showing the selected feature. Note that no styling is applied, wrap this in a div", + docs: "A small map showing the selected feature.", args: [ { doc: "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close", @@ -214,6 +215,54 @@ export default class SpecialVisualizations { ) + minimap.SetStyle("overflow: hidden; pointer-events: none;") + return minimap; + } + }, + + { + funcName: "sided_minimap", + docs: "A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically introduced", + args: [ + { + doc: "The side to show, either `left` or `right`", + name: "side", + } + ], + example: "`{sided_minimap(left)}`", + constr: (state, tagSource, args) => { + + const properties = tagSource.data; + const locationSource = new UIEventSource({ + lat: Number(properties._lat), + lon: Number(properties._lon), + zoom: 18 + }) + const minimap = Minimap.createMiniMap( + { + background: state.backgroundLayer, + location: locationSource, + allowMoving: false + } + ) + const side = args[0] + const feature = state.allElements.ContainingFeatures.get(tagSource.data.id) + const copy = {...feature} + copy.properties = { + id: side + } + new ShowDataLayer( + { + leafletMap: minimap["leafletMap"], + enablePopups: false, + zoomToFeatures: true, + layerToShow: AllKnownLayers.sharedLayers.get("left_right_style"), + features: new StaticFeatureSource([copy], false), + allElements: State.state.allElements + } + ) + + minimap.SetStyle("overflow: hidden; pointer-events: none;") return minimap; } @@ -432,9 +481,11 @@ export default class SpecialVisualizations { doc: "A nice icon to show in the button", defaultValue: "./assets/svg/addSmall.svg" }, - {name:"minzoom", - doc: "How far the contributor must zoom in before being able to import the point", - defaultValue: "18"}], + { + name: "minzoom", + doc: "How far the contributor must zoom in before being able to import the point", + defaultValue: "18" + }], docs: `This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes. If you want to import a dataset, make sure that: @@ -487,14 +538,24 @@ There are also some technicalities in your theme to keep in mind: ) } }, - {funcName: "multi_apply", + { + funcName: "multi_apply", docs: "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags", - args:[ + args: [ {name: "feature_ids", doc: "A JSOn-serialized list of IDs of features to apply the tagging on"}, - {name: "keys", doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features." }, + { + name: "keys", + doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features." + }, {name: "text", doc: "The text to show on the button"}, - {name:"autoapply",doc:"A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown"}, - {name:"overwrite",doc:"If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change"} + { + name: "autoapply", + doc: "A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown" + }, + { + name: "overwrite", + doc: "If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change" + } ], example: "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}", constr: (state, tagsSource, args) => { @@ -503,14 +564,14 @@ There are also some technicalities in your theme to keep in mind: const text = args[2] const autoapply = args[3]?.toLowerCase() === "true" const overwrite = args[4]?.toLowerCase() === "true" - const featureIds : UIEventSource = tagsSource.map(tags => { - const ids = tags[featureIdsKey] - try{ - if(ids === undefined){ + const featureIds: UIEventSource = tagsSource.map(tags => { + const ids = tags[featureIdsKey] + try { + if (ids === undefined) { return [] } return JSON.parse(ids); - }catch(e){ + } catch (e) { console.warn("Could not parse ", ids, "as JSON to extract IDS which should be shown on the map.") return [] } @@ -526,7 +587,7 @@ There are also some technicalities in your theme to keep in mind: state } ); - + } } ] diff --git a/assets/layers/left_right_style/left_right_style.json b/assets/layers/left_right_style/left_right_style.json new file mode 100644 index 0000000000..029cad2f1a --- /dev/null +++ b/assets/layers/left_right_style/left_right_style.json @@ -0,0 +1,41 @@ +{ + "id": "left_right_style", + "description": "Special meta-style which will show one single line, either on the left or on the right depending on the id. This is used in the small popups with left_right roads", + "source": { + "osmTags": { + "or": [ + "id=left", + "id=right" + ] + } + }, + "mapRendering": [ + { + "width": 6, + "offset": { + "mappings": [ + { + "if": "id=left", + "then": "-5" + }, + { + "if": "id=right", + "then": "5" + } + ] + }, + "color": { + "mappings": [ + { + "if": "id=left", + "then": "#00f" + }, + { + "if": "id=right", + "then": "#f00" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/assets/themes/sidewalks/sidewalks.json b/assets/themes/sidewalks/sidewalks.json index 940caba80f..8e30b0a2b4 100644 --- a/assets/themes/sidewalks/sidewalks.json +++ b/assets/themes/sidewalks/sidewalks.json @@ -32,20 +32,111 @@ }, "title": { "render": { - "en": "Street {name}" - } + "en": "{name}" + }, + "mappings": [ + { + "if": "name=", + "then": "Nameless street" + } + ] }, "description": { "en": "Layer showing sidewalks of highways" }, - "tagRenderings": [], + "tagRenderings": [ + + { + "id": "streetname", + "render": { + "en": "This street is named {name}" + } + }, + + { + "leftRightKeys": "sidewalk:left|right", + "renderings": [ + { + "id": "sidewalk_minimap", + "render": "{sided_minimap(left|right):height:3rem;border-radius:0.5rem;overflow:hidden}" + }, + { + "id": "has_sidewalk", + "question": "Is there a sidewalk on the left|right side of the road?", + "mappings": [{ + "if": "sidewalk:left|right=yes", + "then": "Yes, there is a sidewalk on this side of the road" + }, + { + "if": "sidewalk:left|right=no", + "then": "No, there is no seperated sidewalk to walk on" + }] + }, + { + "id": "sidewalk_width", + "question": "What is the width of the sidewalk on this side of the road?", + "render": "This sidewalk is {sidewalk:left|right:width}m wide", + "condition": "sidewalk:left|right=yes", + "freeform": { + "key": "sidewalk:left|right:width", + "type": "length", + "helperArgs": ["21", "map"] + } + } + ] + + + }], "mapRendering": [ { - "location": [ - "point" - ] + "location": ["start","end"], + "icon": "circle:#ccc", + "iconSize": "20,20,center" }, - {} + { + "color": "#ffffff55", + "width": 8 + }, + { + "color": { + "render": "#888" + }, + "width": { + "render": 6, + "mappings": [ + { + "if": { + "or": [ + "sidewalk:left=", + "sidewalk:left=no", + "sidewalk:left=separate" + ] + }, + "then": 0 + } + ] + }, + "offset": -6 + }, + { + "color": "#888", + "width": { + "render": 6, + "mappings": [ + { + "if": { + "or": [ + "sidewalk:right=", + "sidewalk:right=no", + "sidewalk:right=separate" + ] + }, + "then": 0 + } + ] + }, + "offset": 6 + } ], "allowSplit": true } diff --git a/scripts/lint.ts b/scripts/lint.ts index 633c4a98b4..31261d6b11 100644 --- a/scripts/lint.ts +++ b/scripts/lint.ts @@ -32,7 +32,7 @@ function fixLayerConfig(config: LayerConfigJson): void { } } - if (config.mapRendering === undefined || true) { + if (config.mapRendering === undefined) { // This is a legacy format, lets create a pointRendering let location: ("point" | "centroid")[] = ["point"] let wayHandling: number = config["wayHandling"] ?? 0