More dashboard view, add documentation in dashboard view

This commit is contained in:
Pieter Vander Vennet 2022-07-16 03:57:13 +02:00
parent 89278f6d74
commit a89f5a0e3e
10 changed files with 189 additions and 97 deletions

View file

@ -25,6 +25,11 @@ export interface TagRenderingConfigJson {
*/ */
labels?: string[] labels?: string[]
/**
* A human-readable text explaining what this tagRendering does
*/
description?: string | any
/** /**
* Renders this value. Note that "{key}"-parts are substituted by the corresponding values of the element. * Renders this value. Note that "{key}"-parts are substituted by the corresponding values of the element.
* If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value. * If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value.

View file

@ -28,6 +28,9 @@ import {And} from "../../Logic/Tags/And";
import {Overpass} from "../../Logic/Osm/Overpass"; import {Overpass} from "../../Logic/Osm/Overpass";
import Constants from "../Constants"; import Constants from "../Constants";
import {FixedUiElement} from "../../UI/Base/FixedUiElement"; import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import Svg from "../../Svg";
import {UIEventSource} from "../../Logic/UIEventSource";
import {OsmTags} from "../OsmFeature";
export default class LayerConfig extends WithContextLoader { export default class LayerConfig extends WithContextLoader {
@ -191,8 +194,8 @@ export default class LayerConfig extends WithContextLoader {
this.doNotDownload = json.doNotDownload ?? false; this.doNotDownload = json.doNotDownload ?? false;
this.passAllFeatures = json.passAllFeatures ?? false; this.passAllFeatures = json.passAllFeatures ?? false;
this.minzoom = json.minzoom ?? 0; this.minzoom = json.minzoom ?? 0;
if(json["minZoom"] !== undefined){ if (json["minZoom"] !== undefined) {
throw "At "+context+": minzoom is written all lowercase" throw "At " + context + ": minzoom is written all lowercase"
} }
this.minzoomVisible = json.minzoomVisible ?? this.minzoom; this.minzoomVisible = json.minzoomVisible ?? this.minzoom;
this.shownByDefault = json.shownByDefault ?? true; this.shownByDefault = json.shownByDefault ?? true;
@ -352,7 +355,7 @@ export default class LayerConfig extends WithContextLoader {
neededLayer: string; neededLayer: string;
}[] = [] }[] = []
, addedByDefault = false, canBeIncluded = true): BaseUIElement { , addedByDefault = false, canBeIncluded = true): BaseUIElement {
const extraProps = [] const extraProps : (string | BaseUIElement)[] = []
extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher") extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher")
@ -377,7 +380,11 @@ export default class LayerConfig extends WithContextLoader {
} }
if (this.source.geojsonSource !== undefined) { if (this.source.geojsonSource !== undefined) {
extraProps.push("<img src='../warning.svg' height='1rem'/> This layer is loaded from an external source, namely `" + this.source.geojsonSource + "`") extraProps.push(
new Combine([
Utils.runningFromConsole ? "<img src='../warning.svg' height='1rem'/>" : undefined,
"This layer is loaded from an external source, namely ",
new FixedUiElement( this.source.geojsonSource).SetClass("code")]));
} }
} else { } else {
extraProps.push("This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.") extraProps.push("This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.")
@ -409,16 +416,16 @@ export default class LayerConfig extends WithContextLoader {
if (values == undefined) { if (values == undefined) {
return undefined return undefined
} }
const embedded: (Link | string)[] = values.values?.map(v => Link.OsmWiki(values.key, v, true)) ?? ["_no preset options defined, or no values in them_"] const embedded: (Link | string)[] = values.values?.map(v => Link.OsmWiki(values.key, v, true).SetClass("mr-2")) ?? ["_no preset options defined, or no values in them_"]
return [ return [
new Combine([ new Combine([
new Link( new Link(
"<img src='https://mapcomplete.osm.be/assets/svg/statistics.svg' height='18px'>", Utils.runningFromConsole ? "<img src='https://mapcomplete.osm.be/assets/svg/statistics.svg' height='18px'>" : Svg.statistics_svg().SetClass("w-4 h-4 mr-2"),
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values" "https://taginfo.openstreetmap.org/keys/" + values.key + "#values", true
), Link.OsmWiki(values.key) ), Link.OsmWiki(values.key)
]), ]).SetClass("flex"),
values.type === undefined ? "Multiple choice" : new Link(values.type, "../SpecialInputElements.md#" + values.type), values.type === undefined ? "Multiple choice" : new Link(values.type, "../SpecialInputElements.md#" + values.type),
new Combine(embedded) new Combine(embedded).SetClass("flex")
]; ];
})) }))
@ -427,18 +434,27 @@ export default class LayerConfig extends WithContextLoader {
quickOverview = new Combine([ quickOverview = new Combine([
new FixedUiElement("Warning: ").SetClass("bold"), new FixedUiElement("Warning: ").SetClass("bold"),
"this quick overview is incomplete", "this quick overview is incomplete",
new Table(["attribute", "type", "values which are supported by this layer"], tableRows) new Table(["attribute", "type", "values which are supported by this layer"], tableRows).SetClass("zebra-table")
]).SetClass("flex-col flex") ]).SetClass("flex-col flex")
} }
const icon = this.mapRendering
.filter(mr => mr.location.has("point")) let iconImg: BaseUIElement = new FixedUiElement("")
.map(mr => mr.icon?.render?.txt)
.find(i => i !== undefined) if (Utils.runningFromConsole) {
let iconImg = "" const icon = this.mapRendering
if (icon !== undefined) { .filter(mr => mr.location.has("point"))
// This is for the documentation, so we have to use raw HTML .map(mr => mr.icon?.render?.txt)
iconImg = `<img src='https://mapcomplete.osm.be/${icon}' height="100px"> ` .find(i => i !== undefined)
// This is for the documentation in a markdown-file, so we have to use raw HTML
if (icon !== undefined) {
iconImg = new FixedUiElement(`<img src='https://mapcomplete.osm.be/${icon}' height="100px"> `)
}
} else {
iconImg = this.mapRendering
.filter(mr => mr.location.has("point"))
.map(mr => mr.GenerateLeafletStyle(new UIEventSource<OsmTags>({id:"node/-1"}), false, {includeBadges: false}).html)
.find(i => i !== undefined)
} }
let overpassLink: BaseUIElement = undefined; let overpassLink: BaseUIElement = undefined;
@ -467,7 +483,7 @@ export default class LayerConfig extends WithContextLoader {
new Title("Supported attributes", 2), new Title("Supported attributes", 2),
quickOverview, quickOverview,
...this.tagRenderings.map(tr => tr.GenerateDocumentation()) ...this.tagRenderings.map(tr => tr.GenerateDocumentation())
]).SetClass("flex-col") ]).SetClass("flex-col").SetClass("link-underline")
} }
public CustomCodeSnippets(): string[] { public CustomCodeSnippets(): string[] {

View file

@ -14,6 +14,8 @@ import List from "../../UI/Base/List";
import {MappingConfigJson, QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson"; import {MappingConfigJson, QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson";
import {FixedUiElement} from "../../UI/Base/FixedUiElement"; import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import {Paragraph} from "../../UI/Base/Paragraph"; import {Paragraph} from "../../UI/Base/Paragraph";
import spec = Mocha.reporters.spec;
import SpecialVisualizations from "../../UI/SpecialVisualizations";
export interface Mapping { export interface Mapping {
readonly if: TagsFilter, readonly if: TagsFilter,
@ -37,6 +39,7 @@ export default class TagRenderingConfig {
public readonly render?: TypedTranslation<object>; public readonly render?: TypedTranslation<object>;
public readonly question?: TypedTranslation<object>; public readonly question?: TypedTranslation<object>;
public readonly condition?: TagsFilter; public readonly condition?: TagsFilter;
public readonly description?: Translation;
public readonly configuration_warnings: string[] = [] public readonly configuration_warnings: string[] = []
@ -55,6 +58,7 @@ export default class TagRenderingConfig {
public readonly mappings?: Mapping[] public readonly mappings?: Mapping[]
public readonly labels: string[] public readonly labels: string[]
constructor(json: string | QuestionableTagRenderingConfigJson, context?: string) { constructor(json: string | QuestionableTagRenderingConfigJson, context?: string) {
if (json === undefined) { if (json === undefined) {
throw "Initing a TagRenderingConfig with undefined in " + context; throw "Initing a TagRenderingConfig with undefined in " + context;
@ -106,6 +110,7 @@ export default class TagRenderingConfig {
this.labels = json.labels ?? [] this.labels = json.labels ?? []
this.render = Translations.T(json.render, translationKey + ".render"); this.render = Translations.T(json.render, translationKey + ".render");
this.question = Translations.T(json.question, translationKey + ".question"); this.question = Translations.T(json.question, translationKey + ".question");
this.description = Translations.T(json.description, translationKey + ".description");
this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`); this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`);
if (json.freeform) { if (json.freeform) {
@ -563,8 +568,8 @@ export default class TagRenderingConfig {
new Combine( new Combine(
[ [
new FixedUiElement(m.then.txt).SetClass("bold"), new FixedUiElement(m.then.txt).SetClass("bold"),
"corresponds with ", " corresponds with ",
m.if.asHumanString(true, false, {}) new FixedUiElement( m.if.asHumanString(true, false, {})).SetClass("code")
] ]
) )
] ]
@ -599,12 +604,14 @@ export default class TagRenderingConfig {
labels = new Combine([ labels = new Combine([
"This tagrendering has labels ", "This tagrendering has labels ",
...this.labels.map(label => new FixedUiElement(label).SetClass("code")) ...this.labels.map(label => new FixedUiElement(label).SetClass("code"))
]) ]).SetClass("flex")
} }
return new Combine([ return new Combine([
new Title(this.id, 3), new Title(this.id, 3),
this.description,
this.question !== undefined ? this.question !== undefined ?
new Combine(["The question is ", new FixedUiElement(this.question.txt).SetClass("bold")]) : new Combine(["The question is ", new FixedUiElement(this.question.txt).SetClass("font-bold bold")]) :
new FixedUiElement( new FixedUiElement(
"This tagrendering has no question and is thus read-only" "This tagrendering has no question and is thus read-only"
).SetClass("italic"), ).SetClass("italic"),
@ -613,6 +620,6 @@ export default class TagRenderingConfig {
condition, condition,
group, group,
labels labels
]).SetClass("flex-col"); ]).SetClass("flex flex-col");
} }
} }

View file

@ -26,9 +26,9 @@ export default class Link extends BaseUIElement {
if (!hideKey) { if (!hideKey) {
k = key + "=" k = key + "="
} }
return new Link(k + value, `https://wiki.openstreetmap.org/wiki/Tag:${key}%3D${value}`) return new Link(k + value, `https://wiki.openstreetmap.org/wiki/Tag:${key}%3D${value}`, true)
} }
return new Link(key, "https://wiki.openstreetmap.org/wiki/Key:" + key) return new Link(key, "https://wiki.openstreetmap.org/wiki/Key:" + key, true)
} }
AsMarkdown(): string { AsMarkdown(): string {

View file

@ -23,30 +23,35 @@ import TagRenderingAnswer from "./Popup/TagRenderingAnswer";
import Hash from "../Logic/Web/Hash"; import Hash from "../Logic/Web/Hash";
import FilterView from "./BigComponents/FilterView"; import FilterView from "./BigComponents/FilterView";
import {FilterState} from "../Models/FilteredLayer"; import {FilterState} from "../Models/FilteredLayer";
import Translations from "./i18n/Translations";
import Constants from "../Models/Constants";
import {Layer} from "leaflet";
import doc = Mocha.reporters.doc;
export default class DashboardGui { export default class DashboardGui {
private readonly state: FeaturePipelineState; private readonly state: FeaturePipelineState;
private readonly currentView: UIEventSource<string | BaseUIElement> = new UIEventSource<string | BaseUIElement>("No selection") private readonly currentView: UIEventSource<{ title: string | BaseUIElement, contents: string | BaseUIElement }> = new UIEventSource(undefined)
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
this.state = state; this.state = state;
} }
private viewSelector(shown: BaseUIElement, fullview: BaseUIElement, hash?: string): BaseUIElement { private viewSelector(shown: BaseUIElement, title: string | BaseUIElement, contents: string | BaseUIElement, hash?: string): BaseUIElement {
const currentView = this.currentView const currentView = this.currentView
const v = {title, contents}
shown.SetClass("pl-1 pr-1 rounded-md") shown.SetClass("pl-1 pr-1 rounded-md")
shown.onClick(() => { shown.onClick(() => {
currentView.setData(fullview) currentView.setData(v)
}) })
Hash.hash.addCallbackAndRunD(h => { Hash.hash.addCallbackAndRunD(h => {
if (h === hash) { if (h === hash) {
currentView.setData(fullview) currentView.setData(v)
} }
}) })
currentView.addCallbackAndRunD(cv => { currentView.addCallbackAndRunD(cv => {
if (cv == fullview) { if (cv == v) {
shown.SetClass("bg-unsubtle") shown.SetClass("bg-unsubtle")
Hash.hash.setData(hash) Hash.hash.setData(hash)
} else { } else {
@ -64,16 +69,15 @@ export default class DashboardGui {
} }
const tags = this.state.allElements.getEventSourceById(element.properties.id) const tags = this.state.allElements.getEventSourceById(element.properties.id)
const title = new Combine([new Title(new TagRenderingAnswer(tags, layer.title, this.state), 4), const title = new Combine([new Title(new TagRenderingAnswer(tags, layer.title, this.state), 4),
distance < 900 ? Math.floor(distance)+"m away": distance < 900 ? Math.floor(distance) + "m away" :
Utils.Round(distance / 1000) + "km away" Utils.Round(distance / 1000) + "km away"
]).SetClass("flex justify-between"); ]).SetClass("flex justify-between");
const info = new Lazy(() => new Combine([ return this.singleElementCache[element.properties.id] = this.viewSelector(title,
FeatureInfoBox.GenerateTitleBar(tags, layer, this.state), new Lazy(() => FeatureInfoBox.GenerateTitleBar(tags, layer, this.state)),
FeatureInfoBox.GenerateContent(tags, layer, this.state)]).SetStyle("overflox-x: hidden")); new Lazy(() => FeatureInfoBox.GenerateContent(tags, layer, this.state)),
// element.properties.id
);
return this.viewSelector(title, info);
} }
private mainElementsView(elements: { element: OsmFeature, layer: LayerConfig, distance: number }[]): BaseUIElement { private mainElementsView(elements: { element: OsmFeature, layer: LayerConfig, distance: number }[]): BaseUIElement {
@ -87,8 +91,8 @@ export default class DashboardGui {
return new Combine(elements.map(e => self.singleElementView(e.element, e.layer, e.distance))) return new Combine(elements.map(e => self.singleElementView(e.element, e.layer, e.distance)))
} }
private visibleElements(map: MinimapObj & BaseUIElement, layers: Record<string, LayerConfig>): { distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[]{ private visibleElements(map: MinimapObj & BaseUIElement, layers: Record<string, LayerConfig>): { distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[] {
const bbox= map.bounds.data const bbox = map.bounds.data
if (bbox === undefined) { if (bbox === undefined) {
return undefined return undefined
} }
@ -101,10 +105,10 @@ export default class DashboardGui {
let seenElements = new Set<string>() let seenElements = new Set<string>()
for (const elementsWithMetaElement of elementsWithMeta) { for (const elementsWithMetaElement of elementsWithMeta) {
const layer = layers[elementsWithMetaElement.layer] const layer = layers[elementsWithMetaElement.layer]
const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer); const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer);
for (const element of elementsWithMetaElement.features) { for (let i = 0; i < elementsWithMetaElement.features.length; i++) {
console.log("Inspecting ", element.properties.id) const element = elementsWithMetaElement.features[i];
if(!filtered.isDisplayed.data){ if (!filtered.isDisplayed.data) {
continue continue
} }
if (seenElements.has(element.properties.id)) { if (seenElements.has(element.properties.id)) {
@ -117,8 +121,8 @@ export default class DashboardGui {
if (layer?.isShown?.GetRenderValue(element)?.Subs(element.properties)?.txt === "no") { if (layer?.isShown?.GetRenderValue(element)?.Subs(element.properties)?.txt === "no") {
continue continue
} }
const activeFilters : FilterState[] = Array.from(filtered.appliedFilters.data.values()); const activeFilters: FilterState[] = Array.from(filtered.appliedFilters.data.values());
if(activeFilters.some(filter => !filter?.currentFilter?.matchesProperties(element.properties))){ if (activeFilters.some(filter => !filter?.currentFilter?.matchesProperties(element.properties))) {
continue continue
} }
const center = GeoOperations.centerpointCoordinates(element); const center = GeoOperations.centerpointCoordinates(element);
@ -138,7 +142,24 @@ export default class DashboardGui {
return elements; return elements;
} }
private documentationButtonFor(layerConfig: LayerConfig): BaseUIElement {
return this.viewSelector(Translations.W(layerConfig.name?.Clone() ?? layerConfig.id), new Combine(["Documentation about ", layerConfig.name?.Clone() ?? layerConfig.id]),
layerConfig.GenerateDocumentation([]),
"documentation-" + layerConfig.id)
}
private allDocumentationButtons(): BaseUIElement {
const layers = this.state.layoutToUse.layers.filter(l => Constants.priviliged_layers.indexOf(l.id) < 0)
.filter(l => !l.id.startsWith("note_import_"));
if(layers.length === 1){
return this.documentationButtonFor(layers[0])
}
return this.viewSelector(new FixedUiElement("Documentation"), "Documentation",
new Combine(layers.map(l => this.documentationButtonFor(l).SetClass("flex flex-col"))))
}
public setup(): void { public setup(): void {
const state = this.state; const state = this.state;
@ -161,10 +182,11 @@ export default class DashboardGui {
const self = this; const self = this;
const elementsInview = new UIEventSource([]); const elementsInview = new UIEventSource([]);
function update(){
elementsInview.setData( self.visibleElements(map, layers)) function update() {
elementsInview.setData(self.visibleElements(map, layers))
} }
map.bounds.addCallbackAndRun(update) map.bounds.addCallbackAndRun(update)
state.featurePipeline.newDataLoadedSignal.addCallback(update); state.featurePipeline.newDataLoadedSignal.addCallback(update);
state.filteredLayers.addCallbackAndRun(fls => { state.filteredLayers.addCallbackAndRun(fls => {
@ -175,28 +197,36 @@ export default class DashboardGui {
}) })
const welcome = new Combine([state.layoutToUse.description, state.layoutToUse.descriptionTail]) const welcome = new Combine([state.layoutToUse.description, state.layoutToUse.descriptionTail])
self.currentView.setData(welcome) self.currentView.setData({title: state.layoutToUse.title, contents: welcome})
new Combine([ new Combine([
new Combine([ new Combine([
this.viewSelector(new Title(state.layoutToUse.title, 2), welcome), this.viewSelector(new Title(state.layoutToUse.title.Clone(), 2), state.layoutToUse.title.Clone(), welcome, "welcome"),
map.SetClass("w-full h-64 shrink-0 rounded-lg"), map.SetClass("w-full h-64 shrink-0 rounded-lg"),
new SearchAndGo(state), new SearchAndGo(state),
this.viewSelector(new Title( this.viewSelector(new Title(
new VariableUiElement(elementsInview.map(elements => "There are " + elements?.length + " elements in view"))), new FixedUiElement("Stats")), new VariableUiElement(elementsInview.map(elements => "There are " + elements?.length + " elements in view"))),
"Statistics",
new FixedUiElement("Stats"), "statistics"),
this.viewSelector(new FixedUiElement("Filter"), this.viewSelector(new FixedUiElement("Filter"),
"Filters",
new Lazy(() => { new Lazy(() => {
return new FilterView(state.filteredLayers, state.overlayToggles) return new FilterView(state.filteredLayers, state.overlayToggles)
}) }), "filters"
), ),
new VariableUiElement(elementsInview.map(elements => this.mainElementsView(elements).SetClass("block mx-2"))) new VariableUiElement(elementsInview.map(elements => this.mainElementsView(elements).SetClass("block m-2")))
.SetClass("block shrink-2 overflow-x-scroll h-full border-2 border-subtle rounded-lg"), .SetClass("block shrink-2 overflow-x-auto h-full border-2 border-subtle rounded-lg"),
new LanguagePicker(Object.keys(state.layoutToUse.title)).SetClass("mt-2") this.allDocumentationButtons(),
]) new LanguagePicker(Object.keys(state.layoutToUse.title.translations)).SetClass("mt-2")
.SetClass("w-1/2 m-4 flex flex-col"), ]).SetClass("w-1/2 m-4 flex flex-col shrink-0 grow-0"),
new VariableUiElement(this.currentView).SetClass("w-1/2 overflow-y-auto m-4 ml-0 p-2 border-2 border-subtle rounded-xl m-y-8") new VariableUiElement(this.currentView.map(({title, contents}) => {
return new Combine([
new Title(Translations.W(title), 2).SetClass("shrink-0 border-b-4 border-subtle"),
Translations.W(contents).SetClass("shrink-2 overflow-y-auto block")
]).SetClass("flex flex-col h-full")
})).SetClass("w-1/2 m-4 p-2 border-2 border-subtle rounded-xl m-4 ml-0 mr-8 shrink-0 grow-0")
]).SetClass("flex h-full") ]).SetClass("flex h-full")
.AttachTo("leafletDiv") .AttachTo("leafletDiv")

View file

@ -57,6 +57,7 @@ import {SaveButton} from "./Popup/SaveButton";
import {MapillaryLink} from "./BigComponents/MapillaryLink"; import {MapillaryLink} from "./BigComponents/MapillaryLink";
import {CheckBox} from "./Input/Checkboxes"; import {CheckBox} from "./Input/Checkboxes";
import Slider from "./Input/Slider"; import Slider from "./Input/Slider";
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
export interface SpecialVisualization { export interface SpecialVisualization {
funcName: string, funcName: string,
@ -207,7 +208,7 @@ class NearbyImageVis implements SpecialVisualization {
const nearby = new Lazy(() => { const nearby = new Lazy(() => {
const towardsCenter = new CheckBox(t.onlyTowards, false) const towardsCenter = new CheckBox(t.onlyTowards, false)
const radiusValue = state?.osmConnection?.GetPreference("nearby-images-radius","300").sync(s => Number(s), [], i => ""+i) ?? new UIEventSource(300); const radiusValue = state?.osmConnection?.GetPreference("nearby-images-radius", "300").sync(s => Number(s), [], i => "" + i) ?? new UIEventSource(300);
const radius = new Slider(25, 500, { const radius = new Slider(25, 500, {
value: value:
@ -285,7 +286,13 @@ export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.init() public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.init()
public static DocumentationFor(viz: SpecialVisualization): BaseUIElement { public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined {
if (typeof viz === "string") {
viz = SpecialVisualizations.specialVisualizations.find(sv => sv.funcName === viz)
}
if(viz === undefined){
return undefined;
}
return new Combine( return new Combine(
[ [
new Title(viz.funcName, 3), new Title(viz.funcName, 3),

View file

@ -1,21 +1,27 @@
{ {
"id": "shared_questions", "id": "shared_questions",
"questions": { "questions": {
"description": "Show the images block at this location",
"id": "questions" "id": "questions"
}, },
"images": { "images": {
"description": "This block shows the known images which are linked with the `image`-keys, but also via `mapillary` and `wikidata`",
"render": "{image_carousel()}{image_upload()}{nearby_images(expandable)}" "render": "{image_carousel()}{image_upload()}{nearby_images(expandable)}"
}, },
"mapillary": { "mapillary": {
"description": "Shows a button to open Mapillary on this location",
"render": "{mapillary()}" "render": "{mapillary()}"
}, },
"export_as_gpx": { "export_as_gpx": {
"description": "Shows a button to export this feature as GPX. Especially useful for route relations",
"render": "{export_as_gpx()}" "render": "{export_as_gpx()}"
}, },
"export_as_geojson": { "export_as_geojson": {
"description": "Shows a button to export this feature as geojson. Especially useful for debugging or using this in other programs",
"render": "{export_as_geojson()}" "render": "{export_as_geojson()}"
}, },
"wikipedia": { "wikipedia": {
"description": "Shows a wikipedia box with the corresponding wikipedia article",
"render": "{wikipedia():max-height:25rem}", "render": "{wikipedia():max-height:25rem}",
"question": { "question": {
"en": "What is the corresponding Wikidata entity?", "en": "What is the corresponding Wikidata entity?",
@ -93,9 +99,12 @@
} }
}, },
"reviews": { "reviews": {
"description": "Shows the reviews module (including the possibility to leave a review)",
"render": "{reviews()}" "render": "{reviews()}"
}, },
"minimap": { "minimap": {
"description": "Shows a small map with the feature. Added by default to every popup",
"render": "{minimap(18, id): width:100%; height:8rem; border-radius:2rem; overflow: hidden; pointer-events: none;}" "render": "{minimap(18, id): width:100%; height:8rem; border-radius:2rem; overflow: hidden; pointer-events: none;}"
}, },
"phone": { "phone": {
@ -855,7 +864,7 @@
"render": "<div class='subtle' style='font-size: small; margin-top: 2em; margin-bottom: 0.5em;'><a href='https://www.openStreetMap.org/changeset/{_last_edit:changeset}' target='_blank'>Last edited on {_last_edit:timestamp}</a> by <a href='https://www.openStreetMap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a></div>" "render": "<div class='subtle' style='font-size: small; margin-top: 2em; margin-bottom: 0.5em;'><a href='https://www.openStreetMap.org/changeset/{_last_edit:changeset}' target='_blank'>Last edited on {_last_edit:timestamp}</a> by <a href='https://www.openStreetMap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a></div>"
}, },
"all_tags": { "all_tags": {
"#": "Prints all the tags", "description": "Shows a table with all the tags of the feature",
"render": "{all_tags()}" "render": "{all_tags()}"
}, },
"level": { "level": {

View file

@ -819,6 +819,10 @@ video {
margin: 1.25rem; margin: 1.25rem;
} }
.m-2 {
margin: 0.5rem;
}
.m-0\.5 { .m-0\.5 {
margin: 0.125rem; margin: 0.125rem;
} }
@ -831,10 +835,6 @@ video {
margin: 0.75rem; margin: 0.75rem;
} }
.m-2 {
margin: 0.5rem;
}
.m-6 { .m-6 {
margin: 1.5rem; margin: 1.5rem;
} }
@ -843,11 +843,6 @@ video {
margin: 1px; margin: 1px;
} }
.mx-2 {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.my-2 { .my-2 {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -879,6 +874,10 @@ video {
margin-left: 0px; margin-left: 0px;
} }
.mr-8 {
margin-right: 2rem;
}
.mt-4 { .mt-4 {
margin-top: 1rem; margin-top: 1rem;
} }
@ -887,6 +886,10 @@ video {
margin-top: 1.5rem; margin-top: 1.5rem;
} }
.mr-2 {
margin-right: 0.5rem;
}
.mt-1 { .mt-1 {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
@ -903,10 +906,6 @@ video {
margin-right: 1rem; margin-right: 1rem;
} }
.mr-2 {
margin-right: 0.5rem;
}
.mb-2 { .mb-2 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -1071,14 +1070,14 @@ video {
height: 3rem; height: 3rem;
} }
.h-1\/2 {
height: 50%;
}
.h-4 { .h-4 {
height: 1rem; height: 1rem;
} }
.h-1\/2 {
height: 50%;
}
.h-screen { .h-screen {
height: 100vh; height: 100vh;
} }
@ -1163,14 +1162,14 @@ video {
width: 3rem; width: 3rem;
} }
.w-0 {
width: 0px;
}
.w-4 { .w-4 {
width: 1rem; width: 1rem;
} }
.w-0 {
width: 0px;
}
.w-screen { .w-screen {
width: 100vw; width: 100vw;
} }
@ -1361,12 +1360,12 @@ video {
overflow: scroll; overflow: scroll;
} }
.overflow-y-auto { .overflow-x-auto {
overflow-y: auto; overflow-x: auto;
} }
.overflow-x-scroll { .overflow-y-auto {
overflow-x: scroll; overflow-y: auto;
} }
.truncate { .truncate {
@ -1441,6 +1440,10 @@ video {
border-width: 4px; border-width: 4px;
} }
.border-b-4 {
border-bottom-width: 4px;
}
.border-l-4 { .border-l-4 {
border-left-width: 4px; border-left-width: 4px;
} }
@ -2447,6 +2450,15 @@ input {
box-sizing: border-box; box-sizing: border-box;
} }
.code {
display: inline-block;
background-color: lightgray;
padding: 0.5em;
word-break: break-word;
color: black;
box-sizing: border-box;
}
/** Switch layout **/ /** Switch layout **/
.small-image img { .small-image img {

View file

@ -580,6 +580,15 @@ input {
} }
.code {
display: inline-block;
background-color: lightgray;
padding: 0.5em;
word-break: break-word;
color: black;
box-sizing: border-box;
}
/** Switch layout **/ /** Switch layout **/
.small-image img { .small-image img {
height: 1em; height: 1em;

View file

@ -1,18 +1,15 @@
import Combine from "../UI/Base/Combine"; import Combine from "../UI/Base/Combine";
import BaseUIElement from "../UI/BaseUIElement"; import BaseUIElement from "../UI/BaseUIElement";
import Translations from "../UI/i18n/Translations"; import Translations from "../UI/i18n/Translations";
import {existsSync, mkdir, mkdirSync, writeFileSync} from "fs"; import {existsSync, mkdirSync, writeFileSync} from "fs";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import TableOfContents from "../UI/Base/TableOfContents"; import TableOfContents from "../UI/Base/TableOfContents";
import SimpleMetaTaggers, {SimpleMetaTagger} from "../Logic/SimpleMetaTagger"; import SimpleMetaTaggers from "../Logic/SimpleMetaTagger";
import ValidatedTextField from "../UI/Input/ValidatedTextField"; import ValidatedTextField from "../UI/Input/ValidatedTextField";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import SpecialVisualizations from "../UI/SpecialVisualizations"; import SpecialVisualizations from "../UI/SpecialVisualizations";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import {ExtraFunctions} from "../Logic/ExtraFunctions"; import {ExtraFunctions} from "../Logic/ExtraFunctions";
import Title from "../UI/Base/Title"; import Title from "../UI/Base/Title";
import Minimap from "../UI/Base/Minimap"; import Minimap from "../UI/Base/Minimap";
import {QueryParameters} from "../Logic/Web/QueryParameters";
import QueryParameterDocumentation from "../UI/QueryParameterDocumentation"; import QueryParameterDocumentation from "../UI/QueryParameterDocumentation";
import ScriptUtils from "./ScriptUtils"; import ScriptUtils from "./ScriptUtils";
import List from "../UI/Base/List"; import List from "../UI/Base/List";