Merge develop

This commit is contained in:
pietervdvn 2022-07-25 09:48:56 +02:00
commit 118ffe9e6a
50 changed files with 1152 additions and 264 deletions

View file

@ -4,13 +4,36 @@ Tags format
When creating the `json` file describing your layer or theme, you'll have to add a few tags to describe what you want.
This document gives an overview of what every expression means and how it behaves in edge cases.
If the schema-files note a type `string | AndOrTagConfigJson`, you can use one of these values.
If the schema-files note a type [`TagConfigJson`](https://github.com/pietervdvn/MapComplete/blob/develop/Models/ThemeConfig/Json/TagConfigJson.ts), you can use one of these values.
In some cases, not every type of tags-filter can be used. For example, _rendering_ an option with a regex is
fine (`"if": "brand~[Bb]randname", "then":" The brand is Brandname"`); but this regex can not be used to write a value
into the database. The theme loader will however refuse to work with such inconsistencies and notify you of this while
you are building your theme.
Example
-------
This example shows the most common options on how to specify tags:
```json
{
"and": [
"key=value",
{
"or": [
"other_key=value",
"other_key=some_other_value"
]
},
"key_which_should_be_missing=",
"key_which_should_have_a_value~*",
"key~.*some_regex_a*_b+_[a-z]?",
"height<1"
]
}
```
Strict equality
---------------

View file

@ -521,8 +521,13 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
}
}
if(json.title === undefined && json.tagRenderings !== undefined){
warnings.push(context + ": this layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element.")
if(json.tagRenderings !== undefined && json.tagRenderings.length > 0){
if(json.title === undefined){
errors.push(context + ": this layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error.")
}
if(json.title === null){
information.push(context + ": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set.")
}
}
if (json["builtin"] !== undefined) {

View file

@ -25,6 +25,11 @@ export interface TagRenderingConfigJson {
*/
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.
* 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 Constants from "../Constants";
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 {
@ -191,8 +194,8 @@ export default class LayerConfig extends WithContextLoader {
this.doNotDownload = json.doNotDownload ?? false;
this.passAllFeatures = json.passAllFeatures ?? false;
this.minzoom = json.minzoom ?? 0;
if(json["minZoom"] !== undefined){
throw "At "+context+": minzoom is written all lowercase"
if (json["minZoom"] !== undefined) {
throw "At " + context + ": minzoom is written all lowercase"
}
this.minzoomVisible = json.minzoomVisible ?? this.minzoom;
this.shownByDefault = json.shownByDefault ?? true;
@ -352,7 +355,7 @@ export default class LayerConfig extends WithContextLoader {
neededLayer: string;
}[] = []
, addedByDefault = false, canBeIncluded = true): BaseUIElement {
const extraProps = []
const extraProps : (string | BaseUIElement)[] = []
extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher")
@ -364,9 +367,9 @@ export default class LayerConfig extends WithContextLoader {
extraProps.push('This layer is not visible by default and must be enabled in the filter by the user. ')
}
if (this.title === undefined) {
extraProps.push("This layer cannot be toggled in the filter view. If you import this layer in your theme, override `title` to make this toggleable.")
extraProps.push("Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable.")
}
if (this.title === undefined && this.shownByDefault === false) {
if (this.name === undefined && this.shownByDefault === false) {
extraProps.push("This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true")
}
if (this.name === undefined) {
@ -377,7 +380,11 @@ export default class LayerConfig extends WithContextLoader {
}
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 {
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) {
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 [
new Combine([
new Link(
"<img src='https://mapcomplete.osm.be/assets/svg/statistics.svg' height='18px'>",
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values"
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", true
), Link.OsmWiki(values.key)
]),
]).SetClass("flex"),
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([
new FixedUiElement("Warning: ").SetClass("bold"),
"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")
}
const icon = this.mapRendering
.filter(mr => mr.location.has("point"))
.map(mr => mr.icon?.render?.txt)
.find(i => i !== undefined)
let iconImg = ""
if (icon !== undefined) {
// This is for the documentation, so we have to use raw HTML
iconImg = `<img src='https://mapcomplete.osm.be/${icon}' height="100px"> `
let iconImg: BaseUIElement = new FixedUiElement("")
if (Utils.runningFromConsole) {
const icon = this.mapRendering
.filter(mr => mr.location.has("point"))
.map(mr => mr.icon?.render?.txt)
.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;
@ -467,7 +483,7 @@ export default class LayerConfig extends WithContextLoader {
new Title("Supported attributes", 2),
quickOverview,
...this.tagRenderings.map(tr => tr.GenerateDocumentation())
]).SetClass("flex-col")
]).SetClass("flex-col").SetClass("link-underline")
}
public CustomCodeSnippets(): string[] {

View file

@ -14,6 +14,8 @@ import List from "../../UI/Base/List";
import {MappingConfigJson, QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import {Paragraph} from "../../UI/Base/Paragraph";
import spec = Mocha.reporters.spec;
import SpecialVisualizations from "../../UI/SpecialVisualizations";
export interface Mapping {
readonly if: TagsFilter,
@ -38,6 +40,7 @@ export default class TagRenderingConfig {
public readonly render?: TypedTranslation<object>;
public readonly question?: TypedTranslation<object>;
public readonly condition?: TagsFilter;
public readonly description?: Translation;
public readonly configuration_warnings: string[] = []
@ -56,6 +59,7 @@ export default class TagRenderingConfig {
public readonly mappings?: Mapping[]
public readonly labels: string[]
constructor(json: string | QuestionableTagRenderingConfigJson, context?: string) {
if (json === undefined) {
throw "Initing a TagRenderingConfig with undefined in " + context;
@ -107,6 +111,7 @@ export default class TagRenderingConfig {
this.labels = json.labels ?? []
this.render = Translations.T(json.render, translationKey + ".render");
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`);
if (json.freeform) {
@ -571,8 +576,8 @@ export default class TagRenderingConfig {
new Combine(
[
new FixedUiElement(m.then.txt).SetClass("bold"),
"corresponds with ",
m.if.asHumanString(true, false, {})
" corresponds with ",
new FixedUiElement( m.if.asHumanString(true, false, {})).SetClass("code")
]
)
]
@ -607,12 +612,14 @@ export default class TagRenderingConfig {
labels = new Combine([
"This tagrendering has labels ",
...this.labels.map(label => new FixedUiElement(label).SetClass("code"))
])
]).SetClass("flex")
}
return new Combine([
new Title(this.id, 3),
this.description,
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(
"This tagrendering has no question and is thus read-only"
).SetClass("italic"),
@ -621,6 +628,6 @@ export default class TagRenderingConfig {
condition,
group,
labels
]).SetClass("flex-col");
]).SetClass("flex flex-col");
}
}

24
UI/Base/ChartJs.ts Normal file
View file

@ -0,0 +1,24 @@
import BaseUIElement from "../BaseUIElement";
import {Chart, ChartConfiguration, ChartType, DefaultDataPoint, registerables} from 'chart.js';
Chart.register(...registerables);
export default class ChartJs<
TType extends ChartType = ChartType,
TData = DefaultDataPoint<TType>,
TLabel = unknown
> extends BaseUIElement{
private readonly _config: ChartConfiguration<TType, TData, TLabel>;
constructor(config: ChartConfiguration<TType, TData, TLabel>) {
super();
this._config = config;
}
protected InnerConstructElement(): HTMLElement {
const canvas = document.createElement("canvas");
new Chart(canvas, this._config);
return canvas;
}
}

View file

@ -38,6 +38,9 @@ export default class Combine extends BaseUIElement {
protected InnerConstructElement(): HTMLElement {
const el = document.createElement("span")
try {
if(this.uiElements === undefined){
console.error("PANIC")
}
for (const subEl of this.uiElements) {
if (subEl === undefined || subEl === null) {
continue;

View file

@ -26,9 +26,9 @@ export default class Link extends BaseUIElement {
if (!hideKey) {
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 {

View file

@ -26,7 +26,8 @@ export default class Toggleable extends Combine {
public readonly isVisible = new UIEventSource(false)
constructor(title: Title | Combine | BaseUIElement, content: BaseUIElement, options?: {
closeOnClick: true | boolean
closeOnClick?: true | boolean,
height?: "100vh" | string
}) {
super([title, content])
content.SetClass("animate-height border-l-4 pl-2 block")
@ -72,7 +73,7 @@ export default class Toggleable extends Combine {
this.isVisible.addCallbackAndRun(isVisible => {
if (isVisible) {
contentElement.style.maxHeight = "100vh"
contentElement.style.maxHeight = options?.height ?? "100vh"
contentElement.style.overflowY = "auto"
contentElement.style["-webkit-mask-image"] = "unset"
} else {

View file

@ -33,6 +33,7 @@ export class VariableUiElement extends BaseUIElement {
if (self.isDestroyed) {
return true;
}
while (el.firstChild) {
el.removeChild(el.lastChild);
}

View file

@ -9,7 +9,7 @@ export default abstract class BaseUIElement {
protected _constructedHtmlElement: HTMLElement;
protected isDestroyed = false;
private clss: Set<string> = new Set<string>();
private readonly clss: Set<string> = new Set<string>();
private style: string;
private _onClick: () => void;
@ -114,7 +114,7 @@ export default abstract class BaseUIElement {
if (style !== undefined && style !== "") {
el.style.cssText = style
}
if (this.clss.size > 0) {
if (this.clss?.size > 0) {
try {
el.classList.add(...Array.from(this.clss))
} catch (e) {

View file

@ -43,6 +43,14 @@ export interface PresetInfo extends PresetConfig {
export default class SimpleAddUI extends Toggle {
/**
*
* @param isShown
* @param resetScrollSignal
* @param filterViewIsOpened
* @param state
* @param takeLocationFrom: defaults to state.lastClickLocation. Take this location to add the new point around
*/
constructor(isShown: UIEventSource<boolean>,
resetScrollSignal: UIEventSource<void>,
filterViewIsOpened: UIEventSource<boolean>,
@ -59,7 +67,9 @@ export default class SimpleAddUI extends Toggle {
filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>,
backgroundLayer: UIEventSource<BaseLayer>
}) {
},
takeLocationFrom?: UIEventSource<{lat: number, lon: number}>
) {
const loginButton = new SubtleButton(Svg.osm_logo_ui(), Translations.t.general.add.pleaseLogin.Clone())
.onClick(() => state.osmConnection.AttemptLogin());
const readYourMessages = new Combine([
@ -68,7 +78,8 @@ export default class SimpleAddUI extends Toggle {
Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false})
]);
takeLocationFrom = takeLocationFrom ?? state.LastClickLocation
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
selectedPreset.addCallback(_ => {
resetScrollSignal.ping();
@ -76,7 +87,7 @@ export default class SimpleAddUI extends Toggle {
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
state.LastClickLocation.addCallback(_ => selectedPreset.setData(undefined))
takeLocationFrom.addCallback(_ => selectedPreset.setData(undefined))
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset, state)
@ -120,7 +131,7 @@ export default class SimpleAddUI extends Toggle {
const message = Translations.t.general.add.addNew.Subs({category: preset.name}, preset.name["context"]);
return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset,
message,
state.LastClickLocation.data,
takeLocationFrom.data,
confirm,
cancel,
() => {

View file

@ -0,0 +1,179 @@
import ChartJs from "../Base/ChartJs";
import {OsmFeature} from "../../Models/OsmFeature";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import {ChartConfiguration} from 'chart.js';
import Combine from "../Base/Combine";
import {TagUtils} from "../../Logic/Tags/TagUtils";
export default class TagRenderingChart extends Combine {
private static readonly unkownColor = 'rgba(128, 128, 128, 0.2)'
private static readonly unkownBorderColor = 'rgba(128, 128, 128, 0.2)'
private static readonly otherColor = 'rgba(128, 128, 128, 0.2)'
private static readonly otherBorderColor = 'rgba(128, 128, 255)'
private static readonly notApplicableColor = 'rgba(128, 128, 128, 0.2)'
private static readonly notApplicableBorderColor = 'rgba(255, 0, 0)'
private static readonly backgroundColors = [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
]
private static readonly borderColors = [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
]
/**
* Creates a chart about this tagRendering for the given data
*/
constructor(features: OsmFeature[], tagRendering: TagRenderingConfig, options?: {
chartclasses?: string,
chartstyle?: string,
includeTitle?: boolean,
groupToOtherCutoff?: 3 | number
}) {
const mappings = tagRendering.mappings ?? []
if (mappings.length === 0 && tagRendering.freeform?.key === undefined) {
super([])
this.SetClass("hidden")
return;
}
let unknownCount = 0;
const categoryCounts = mappings.map(_ => 0)
const otherCounts: Record<string, number> = {}
let notApplicable = 0;
let barchartMode = tagRendering.multiAnswer;
for (const feature of features) {
const props = feature.properties
if (tagRendering.condition !== undefined && !tagRendering.condition.matchesProperties(props)) {
notApplicable++;
continue;
}
if (!tagRendering.IsKnown(props)) {
unknownCount++;
continue;
}
let foundMatchingMapping = false;
if (!tagRendering.multiAnswer) {
for (let i = 0; i < mappings.length; i++) {
const mapping = mappings[i];
if (mapping.if.matchesProperties(props)) {
categoryCounts[i]++
foundMatchingMapping = true
break;
}
}
} else {
for (let i = 0; i < mappings.length; i++) {
const mapping = mappings[i];
if (TagUtils.MatchesMultiAnswer( mapping.if, props)) {
categoryCounts[i]++
foundMatchingMapping = true
}
}
}
if (!foundMatchingMapping) {
if (tagRendering.freeform?.key !== undefined && props[tagRendering.freeform.key] !== undefined) {
const otherValue = props[tagRendering.freeform.key]
otherCounts[otherValue] = (otherCounts[otherValue] ?? 0) + 1
} else {
unknownCount++
}
}
}
if (unknownCount + notApplicable === features.length) {
super([])
this.SetClass("hidden")
return
}
let otherGrouped = 0;
const otherLabels: string[] = []
const otherData : number[] = []
for (const v in otherCounts) {
const count = otherCounts[v]
if(count >= (options.groupToOtherCutoff ?? 3)){
otherLabels.push(v)
otherData.push(otherCounts[v])
}else{
otherGrouped++;
}
}
const labels = ["Unknown", "Other", "Not applicable", ...mappings?.map(m => m.then.txt) ?? [], ...otherLabels]
const data = [unknownCount, otherGrouped, notApplicable, ...categoryCounts, ... otherData]
const borderColor = [TagRenderingChart.unkownBorderColor, TagRenderingChart.otherBorderColor, TagRenderingChart.notApplicableBorderColor]
const backgroundColor = [TagRenderingChart.unkownColor, TagRenderingChart.otherColor, TagRenderingChart.notApplicableColor]
while (borderColor.length < data.length) {
borderColor.push(...TagRenderingChart.borderColors)
backgroundColor.push(...TagRenderingChart.backgroundColors)
}
for (let i = data.length; i >= 0; i--) {
if (data[i] === 0) {
labels.splice(i, 1)
data.splice(i, 1)
borderColor.splice(i, 1)
backgroundColor.splice(i, 1)
}
}
if(labels.length > 9){
barchartMode = true;
}
const config = <ChartConfiguration>{
type: barchartMode ? 'bar' : 'doughnut',
data: {
labels,
datasets: [{
data,
backgroundColor,
borderColor,
borderWidth: 1,
label: undefined
}]
},
options: {
plugins: {
legend: {
display: !barchartMode
}
}
}
}
const chart = new ChartJs(config).SetClass(options?.chartclasses ?? "w-32 h-32");
if (options.chartstyle !== undefined) {
chart.SetStyle(options.chartstyle)
}
super([
options?.includeTitle ? (tagRendering.question.Clone() ?? tagRendering.id) : undefined,
chart])
this.SetClass("block")
}
}

348
UI/DashboardGui.ts Normal file
View file

@ -0,0 +1,348 @@
import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
import {DefaultGuiState} from "./DefaultGuiState";
import {FixedUiElement} from "./Base/FixedUiElement";
import {Utils} from "../Utils";
import Combine from "./Base/Combine";
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import * as home_location_json from "../assets/layers/home_location/home_location.json";
import State from "../State";
import Title from "./Base/Title";
import {MinimapObj} from "./Base/Minimap";
import BaseUIElement from "./BaseUIElement";
import {VariableUiElement} from "./Base/VariableUIElement";
import {GeoOperations} from "../Logic/GeoOperations";
import {BBox} from "../Logic/BBox";
import {OsmFeature} from "../Models/OsmFeature";
import SearchAndGo from "./BigComponents/SearchAndGo";
import FeatureInfoBox from "./Popup/FeatureInfoBox";
import {UIEventSource} from "../Logic/UIEventSource";
import LanguagePicker from "./LanguagePicker";
import Lazy from "./Base/Lazy";
import TagRenderingAnswer from "./Popup/TagRenderingAnswer";
import Hash from "../Logic/Web/Hash";
import FilterView from "./BigComponents/FilterView";
import {FilterState} from "../Models/FilteredLayer";
import Translations from "./i18n/Translations";
import Constants from "../Models/Constants";
import SimpleAddUI from "./BigComponents/SimpleAddUI";
import TagRenderingChart from "./BigComponents/TagRenderingChart";
import Loading from "./Base/Loading";
import BackToIndex from "./BigComponents/BackToIndex";
import Locale from "./i18n/Locale";
export default class DashboardGui {
private readonly state: FeaturePipelineState;
private readonly currentView: UIEventSource<{ title: string | BaseUIElement, contents: string | BaseUIElement }> = new UIEventSource(undefined)
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
this.state = state;
}
private viewSelector(shown: BaseUIElement, title: string | BaseUIElement, contents: string | BaseUIElement, hash?: string): BaseUIElement {
const currentView = this.currentView
const v = {title, contents}
shown.SetClass("pl-1 pr-1 rounded-md")
shown.onClick(() => {
currentView.setData(v)
})
Hash.hash.addCallbackAndRunD(h => {
if (h === hash) {
currentView.setData(v)
}
})
currentView.addCallbackAndRunD(cv => {
if (cv == v) {
shown.SetClass("bg-unsubtle")
Hash.hash.setData(hash)
} else {
shown.RemoveClass("bg-unsubtle")
}
})
return shown;
}
private singleElementCache: Record<string, BaseUIElement> = {}
private singleElementView(element: OsmFeature, layer: LayerConfig, distance: number): BaseUIElement {
if (this.singleElementCache[element.properties.id] !== undefined) {
return this.singleElementCache[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),
distance < 900 ? Math.floor(distance) + "m away" :
Utils.Round(distance / 1000) + "km away"
]).SetClass("flex justify-between");
return this.singleElementCache[element.properties.id] = this.viewSelector(title,
new Lazy(() => FeatureInfoBox.GenerateTitleBar(tags, layer, this.state)),
new Lazy(() => FeatureInfoBox.GenerateContent(tags, layer, this.state)),
// element.properties.id
);
}
private mainElementsView(elements: { element: OsmFeature, layer: LayerConfig, distance: number }[]): BaseUIElement {
const self = this
if (elements === undefined) {
return new FixedUiElement("Initializing")
}
if (elements.length == 0) {
return new FixedUiElement("No elements in view")
}
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 }[] {
const bbox = map.bounds.data
if (bbox === undefined) {
console.warn("No bbox")
return undefined
}
const location = map.location.data;
const loc: [number, number] = [location.lon, location.lat]
const elementsWithMeta: { features: OsmFeature[], layer: string }[] = this.state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox)
let elements: { distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[] = []
let seenElements = new Set<string>()
for (const elementsWithMetaElement of elementsWithMeta) {
const layer = layers[elementsWithMetaElement.layer]
if(layer.title === undefined){
continue
}
const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer);
for (let i = 0; i < elementsWithMetaElement.features.length; i++) {
const element = elementsWithMetaElement.features[i];
if (!filtered.isDisplayed.data) {
continue
}
if (seenElements.has(element.properties.id)) {
continue
}
seenElements.add(element.properties.id)
if (!bbox.overlapsWith(BBox.get(element))) {
continue
}
if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) {
continue
}
const activeFilters: FilterState[] = Array.from(filtered.appliedFilters.data.values());
if (!activeFilters.every(filter => filter?.currentFilter === undefined || filter?.currentFilter?.matchesProperties(element.properties))) {
continue
}
const center = GeoOperations.centerpointCoordinates(element);
elements.push({
element,
center,
layer: layers[elementsWithMetaElement.layer],
distance: GeoOperations.distanceBetween(loc, center)
})
}
}
elements.sort((e0, e1) => e0.distance - e1.distance)
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 {
const state = this.state;
if (this.state.layoutToUse.customCss !== undefined) {
if (window.location.pathname.indexOf("index") >= 0) {
Utils.LoadCustomCss(this.state.layoutToUse.customCss)
}
}
const map = this.SetupMap();
Utils.downloadJson("./service-worker-version").then(data => console.log("Service worker", data)).catch(_ => console.log("Service worker not active"))
document.getElementById("centermessage").classList.add("hidden")
const layers: Record<string, LayerConfig> = {}
for (const layer of state.layoutToUse.layers) {
layers[layer.id] = layer;
}
const self = this;
const elementsInview = new UIEventSource<{ distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[]>([]);
function update() {
elementsInview.setData(self.visibleElements(map, layers))
}
map.bounds.addCallbackAndRun(update)
state.featurePipeline.newDataLoadedSignal.addCallback(update);
state.filteredLayers.addCallbackAndRun(fls => {
for (const fl of fls) {
fl.isDisplayed.addCallback(update)
fl.appliedFilters.addCallback(update)
}
})
const filterView = new Lazy(() => {
return new FilterView(state.filteredLayers, state.overlayToggles)
});
const welcome = new Combine([state.layoutToUse.description, state.layoutToUse.descriptionTail])
self.currentView.setData({title: state.layoutToUse.title, contents: welcome})
const filterViewIsOpened = new UIEventSource(false)
filterViewIsOpened.addCallback(_ => self.currentView.setData({title: "filters", contents: filterView}))
const newPointIsShown = new UIEventSource(false);
const addNewPoint = new SimpleAddUI(
new UIEventSource(true),
new UIEventSource(undefined),
filterViewIsOpened,
state,
state.locationControl
);
const addNewPointTitle = "Add a missing point"
this.currentView.addCallbackAndRunD(cv => {
newPointIsShown.setData(cv.contents === addNewPoint)
})
newPointIsShown.addCallbackAndRun(isShown => {
if (isShown) {
if (self.currentView.data.contents !== addNewPoint) {
self.currentView.setData({title: addNewPointTitle, contents: addNewPoint})
}
} else {
if (self.currentView.data.contents === addNewPoint) {
self.currentView.setData(undefined)
}
}
})
const statistics =
new VariableUiElement(elementsInview.stabilized(1000).map(features => {
if (features === undefined) {
return new Loading("Loading data")
}
if (features.length === 0) {
return "No elements in view"
}
const els = []
for (const layer of state.layoutToUse.layers) {
if(layer.name === undefined){
continue
}
const featuresForLayer = features.filter(f => f.layer === layer).map(f => f.element)
if(featuresForLayer.length === 0){
continue
}
els.push(new Title(layer.name))
const layerStats = []
for (const tagRendering of (layer?.tagRenderings ?? [])) {
const chart = new TagRenderingChart(featuresForLayer, tagRendering, {
chartclasses: "w-full",
chartstyle: "height: 60rem",
includeTitle: false
})
const full = new Lazy(() =>
new TagRenderingChart(featuresForLayer, tagRendering, {
chartstyle: "max-height: calc(100vh - 10rem)",
groupToOtherCutoff: 0
})
)
const title = new Title(tagRendering.question?.Clone() ?? tagRendering.id)
title.onClick(() => {
const current = self.currentView.data
full.onClick(() => {
self.currentView.setData(current)
})
self.currentView.setData(
{
title: new Title(tagRendering.question.Clone() ?? tagRendering.id),
contents: full
})
}
)
if(!chart.HasClass("hidden")){
layerStats.push(new Combine([title, chart]).SetClass("flex flex-col w-full lg:w-1/3"))
}
}
els.push(new Combine(layerStats).SetClass("flex flex-wrap"))
}
return new Combine(els)
}, [Locale.language]))
new Combine([
new Combine([
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"),
new SearchAndGo(state),
this.viewSelector(new Title(
new VariableUiElement(elementsInview.map(elements => "There are " + elements?.length + " elements in view"))),
"Statistics",
statistics, "statistics"),
this.viewSelector(new FixedUiElement("Filter"),
"Filters", filterView, "filters"),
this.viewSelector(new Combine(["Add a missing point"]), addNewPointTitle,
addNewPoint
),
new VariableUiElement(elementsInview.map(elements => this.mainElementsView(elements).SetClass("block m-2")))
.SetClass("block shrink-2 overflow-x-auto h-full border-2 border-subtle rounded-lg"),
this.allDocumentationButtons(),
new LanguagePicker(Object.keys(state.layoutToUse.title.translations)).SetClass("mt-2"),
new BackToIndex()
]).SetClass("w-1/2 lg:w-1/4 m-4 flex flex-col shrink-0 grow-0"),
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 lg:w-3/4 m-4 p-2 border-2 border-subtle rounded-xl m-4 ml-0 mr-8 shrink-0 grow-0"),
]).SetClass("flex h-full")
.AttachTo("leafletDiv")
}
private SetupMap(): MinimapObj & BaseUIElement {
const state = this.state;
new ShowDataLayer({
leafletMap: state.leafletMap,
layerToShow: new LayerConfig(home_location_json, "home_location", true),
features: state.homeLocation,
state
})
state.leafletMap.addCallbackAndRunD(_ => {
// Lets assume that all showDataLayers are initialized at this point
state.selectedElement.ping()
State.state.locationControl.ping();
return true;
})
return state.mainMapObject
}
}

View file

@ -44,14 +44,9 @@ export default class DefaultGUI {
}
public setup(){
if (this.state.layoutToUse.customCss !== undefined) {
Utils.LoadCustomCss(this.state.layoutToUse.customCss);
}
this.SetupUIElements();
this.SetupMap()
if (this.state.layoutToUse.customCss !== undefined && window.location.pathname.indexOf("index") >= 0) {
Utils.LoadCustomCss(this.state.layoutToUse.customCss)
}
@ -144,7 +139,7 @@ export default class DefaultGUI {
new ShowDataLayer({
leafletMap: state.leafletMap,
layerToShow: new LayerConfig(home_location_json, "all_known_layers", true),
layerToShow: new LayerConfig(home_location_json, "home_location", true),
features: state.homeLocation,
state
})

View file

@ -13,6 +13,11 @@ export class DropDown<T> extends InputElement<T> {
private readonly _value: UIEventSource<T>;
private readonly _values: { value: T; shown: string | BaseUIElement }[];
/**
*
* const dropdown = new DropDown<number>("test",[{value: 42, shown: "the answer"}])
* dropdown.GetValue().data // => 42
*/
constructor(label: string | BaseUIElement,
values: { value: T, shown: string | BaseUIElement }[],
value: UIEventSource<T> = undefined,
@ -21,7 +26,7 @@ export class DropDown<T> extends InputElement<T> {
}
) {
super();
value = value ?? new UIEventSource<T>(undefined)
value = value ?? new UIEventSource<T>(values[0].value)
this._value = value
this._values = values;
if (values.length <= 1) {
@ -63,7 +68,7 @@ export class DropDown<T> extends InputElement<T> {
select.onchange = (() => {
var index = select.selectedIndex;
const index = select.selectedIndex;
value.setData(values[index].value);
});

View file

@ -46,7 +46,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
}
private static GenerateTitleBar(tags: UIEventSource<any>,
public static GenerateTitleBar(tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: {}): BaseUIElement {
const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI"), state)
@ -64,7 +64,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
])
}
private static GenerateContent(tags: UIEventSource<any>,
public static GenerateContent(tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: FeaturePipelineState): BaseUIElement {
let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>();

View file

@ -57,6 +57,7 @@ import {SaveButton} from "./Popup/SaveButton";
import {MapillaryLink} from "./BigComponents/MapillaryLink";
import {CheckBox} from "./Input/Checkboxes";
import Slider from "./Input/Slider";
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
export interface SpecialVisualization {
funcName: string,
@ -207,7 +208,7 @@ class NearbyImageVis implements SpecialVisualization {
const nearby = new Lazy(() => {
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, {
value:
@ -285,7 +286,13 @@ export default class SpecialVisualizations {
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(
[
new Title(viz.funcName, 3),

View file

@ -282,70 +282,9 @@
},
"id": "bike_shop-name"
},
{
"question": {
"en": "What is the website of {name}?",
"nl": "Wat is de website van {name}?",
"fr": "Quel est le site web de {name} ?",
"gl": "Cal é a páxina web de {name}?",
"it": "Qual è il sito web di {name}?",
"ru": "Какой сайт у {name}?",
"id": "URL {name} apa?",
"de": "Wie lautet die Webseite von {name}?",
"pt_BR": "Qual o website de {name}?",
"pt": "Qual o website de {name}?",
"es": "¿Cual es el sitio web de {name}?",
"da": "Hvad er webstedet for {name}?"
},
"render": "<a href='{website}' target='_blank'>{website}</a>",
"freeform": {
"key": "website",
"type": "url"
},
"id": "bike_shop-website"
},
{
"question": {
"en": "What is the phone number of {name}?",
"nl": "Wat is het telefoonnummer van {name}?",
"fr": "Quel est le numéro de téléphone de {name} ?",
"gl": "Cal é o número de teléfono de {name}?",
"it": "Qual è il numero di telefono di {name}?",
"ru": "Какой номер телефона у {name}?",
"de": "Wie lautet die Telefonnummer von {name}?",
"pt_BR": "Qual o número de telefone de {name}?",
"pt": "Qual é o número de telefone de {name}?",
"es": "¿Cual es el número de teléfono de {name}?",
"da": "Hvad er telefonnummeret på {name}?"
},
"render": "<a href='tel:{phone}'>{phone}</a>",
"freeform": {
"key": "phone",
"type": "phone"
},
"id": "bike_shop-phone"
},
{
"question": {
"en": "What is the email address of {name}?",
"nl": "Wat is het email-adres van {name}?",
"fr": "Quelle est l'adresse électronique de {name}?",
"gl": "Cal é o enderezo de correo electrónico de {name}?",
"it": "Qual è lindirizzo email di {name}?",
"ru": "Какой адрес электронной почты у {name}?",
"de": "Wie lautet die E-Mail-Adresse von {name}?",
"pt_BR": "Qual o endereço de email de {name}?",
"pt": "Qual o endereço de email de {name}?",
"es": "¿Cual es la dirección de correo electrónico de {name}?",
"da": "Hvad er e-mailadressen på {name}?"
},
"render": "<a href='mailto:{email}' target='_blank'>{email}</a>",
"freeform": {
"key": "email",
"type": "email"
},
"id": "bike_shop-email"
},
"website",
"phone",
"email",
"opening_hours",
{
"render": {

View file

@ -4,9 +4,12 @@
"en": "elevator"
},
"source": {
"osmTags": "elevator=yes"
"osmTags": "highway=elevator"
},
"minzoom": 13,
"description": {
"en": "This layer show elevators and asks for operational status and elevator dimensions. Useful for wheelchair accessibility information"
},
"title": {
"en": "Elevator"
},
@ -14,12 +17,23 @@
"images",
{
"id": "operational_status",
"question": {
"en": "Does this elevator work?"
},
"mappings": [
{
"if": "operational_status=broken",
"then": {
"en": "This elevator is broken"
}
},
"icon": "close:red"
},
{
"if": "operational_status=closed",
"then": {
"en": "This elevator is closed <span class='subtle'>e.g. because renovation works are going on</span>"
},
"icon": "invalid:red"
},
{
"if": "operational_status=ok",
@ -86,6 +100,17 @@
"location": [
"point",
"centroid"
],
"iconBadges": [
{
"if": {
"or": [
"operational_status=broken",
"operational_status=closed"
]
},
"then": "close:#c33"
}
]
}
],
@ -96,7 +121,7 @@
"nl": "een lift"
},
"tags": [
"elevator=yes"
"highway=elevator"
]
}
],
@ -118,6 +143,7 @@
}
},
{
"default": true,
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",
@ -130,4 +156,4 @@
]
}
]
}
}

View file

@ -337,8 +337,7 @@
"es": "¿Cual es el ancho de esta puerta/entrada?"
},
"freeform": {
"key": "width",
"type": "distance"
"key": "width"
}
},
{
@ -417,6 +416,7 @@
}
},
{
"default": true,
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="3" fill="#4863A0"/>
</svg>

After

Width:  |  Height:  |  Size: 136 B

View file

@ -0,0 +1,62 @@
{
"id": "governments",
"name": {
"en": "governments"
},
"description": {
"en": "This layer show governmental buildings. It was setup as commissioned layer for the client of OSOC '22"
},
"source": {
"osmTags": {
"or": [
"office=government"
]
}
},
"title": {
"render": {
"en": "Governmental Office {name}"
}
},
"minzoom": 13,
"tagRenderings": [
"images",
"phone",
"email",
"website",
{
"question": {
"en": "What is the name of this Governmental Office?"
},
"render": {
"en": "This Governmental Office is called {name}"
},
"freeform": {
"key": "name"
},
"id": "name"
}
],
"presets": [
{
"title": {
"en": "a Governmental Office"
},
"tags": [
"office=government"
]
}
],
"mapRendering": [
{
"icon": {
"render": "circle:white;./assets/layers/governments/government.svg"
},
"iconSize": "40,40,center",
"location": [
"point",
"centroid"
]
}
]
}

View file

@ -0,0 +1,12 @@
[
{
"path": "government.svg",
"license": "CC0",
"authors": [
"OSM Carto"
],
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Office-16.svg"
]
}
]

View file

@ -8,6 +8,9 @@
"en": "Hospital"
}
},
"description": {
"en": "A layer showing hospital grounds"
},
"minzoom": 12,
"source": {
"osmTags": "amenity=hospital"
@ -39,6 +42,10 @@
"point",
"centroid"
]
},
{
"color": "#fcd862",
"width": 1
}
]
}

View file

@ -1,10 +1,13 @@
{
"id": "id_presets",
"description": "Layer containing various presets and questions generated by ID. These are meant to be reused in other layers by importing the tagRenderings with `id_preset.<tagrendering>",
"description": {
"en": "Layer containing various presets and questions generated by ID. These are meant to be reused in other layers by importing the tagRenderings with `id_preset.<tagrendering>"
},
"#dont-translate": "*",
"source": {
"osmTags": "id~*"
},
"title": null,
"mapRendering": null,
"tagRenderings": [
{

View file

@ -3,6 +3,9 @@
"name": {
"en": "indoors"
},
"description": {
"en": "Basic indoor mapping: shows room outlines"
},
"source": {
"osmTags": {
"or": [

View file

@ -16,7 +16,6 @@
]
}
},
"title": {},
"description": {
"en": "Pedestrian footpaths, especially used for indoor navigation and snapping entrances to this layer",
"nl": "Pad voor voetgangers, in het bijzonder gebruikt voor navigatie binnen gebouwen en om aan toegangen vast te klikken in deze laag",

View file

@ -3,6 +3,9 @@
"name": {
"en": "pharmacy"
},
"description": {
"en": "A layer showing pharmacies, which (probably) dispense prescription drugs"
},
"title": {
"render": {
"en": "{name}"
@ -26,6 +29,22 @@
"minzoom": 13,
"tagRenderings": [
"images",
{
"id": "name",
"freeform": {
"key": "name",
"type": "string",
"placeholder": {
"en": "Name of the pharmacy"
}
},
"question": {
"en": "What is the name of the pharmacy?"
},
"render": {
"en": "This pharmacy is called {name}"
}
},
"opening_hours",
"phone",
"email",
@ -66,31 +85,45 @@
"location": [
"point",
"centroid"
]
],
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
}
],
"label": {
"mappings": [
{
"if": "name~*",
"then": "<div style='background: white; padding: 0.25em; border-radius:0.5em'>{name}</div>"
}
]
}
}
],
"filter": [
"filter": [
{
"id": "drive-through",
"options": [
{
"id": "drive-through",
"options": [
{
"question": {
"en": "Has drive through"
},
"osmTags": "drive_through=yes"
}
]
},
{
"id": "dispensing",
"options": [
{
"question": {
"en": "Pharmacy able to provide prescription drugs"
},
"osmTags": "dispensing=yes"
}
]
"question": {
"en": "Has drive through"
},
"osmTags": "drive_through=yes"
}
]
]
},
{
"id": "dispensing",
"options": [
{
"question": {
"en": "Pharmacy able to provide prescription drugs"
},
"osmTags": "dispensing=yes"
}
]
}
]
}

View file

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="500" height="500" viewBox="0 0 14 14">
<rect width="14" height="14" x="0" y="0" id="canvas" style="fill:none;stroke:none;visibility:hidden"/>
<path d="m 4,4 -2,2 0,8 10,0 0,-8 -2,-2.0000003 z m 2,2 2,0 0,2 2,0 0,2 -2,0 0,2 -2,0 0,-2 -2,0 0,-2 2,0 z M 4,0 4,3 10,3 10,0 z" id="pharmacy" style="fill:#BF0000"/>
</svg>

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 330 B

View file

@ -3,6 +3,9 @@
"name": {
"en": "Reception desks"
},
"description": {
"en": "A layer showing where the reception desks are and which asks some accessibility information"
},
"title": {
"render": {
"en": "Reception desk"

View file

@ -5,6 +5,7 @@
"source": {
"osmTags": "id~*"
},
"title": null,
"mapRendering": null,
"tagRenderings": [
{

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 228.57 239.29"><defs><style>.cls-1{fill:#1d1d1b;}</style></defs><path class="cls-1" d="M208.18,119.08V11.93c0-5.24,0-5.24,5.15-5.24,2.43,0,4.87,0,7.31,0,2.05,0,3.12.76,3,2.92-.07,1.13,0,2.27,0,3.4q0,106.5,0,213c0,.69-.06,1.39,0,2.09.13,1.9-.73,2.68-2.62,2.64-3.39-.05-6.79,0-10.19,0-1.84,0-2.73-.76-2.66-2.67s0-4,0-6Z"/><path class="cls-1" d="M20,119.06V226.75c0,3.9,0,3.92-3.91,4-3,0-5.93-.05-8.89,0-1.76,0-2.58-.7-2.55-2.47s0-3.31,0-5q0-106.12,0-212.25c0-4.17.22-4.39,4.45-4.36,2.52,0,5.05.08,7.58,0,2.26-.09,3.47.67,3.32,3.13-.1,1.47,0,3,0,4.44q0,52.41,0,104.82Z"/><path class="cls-1" d="M135.46,156.52a4.35,4.35,0,0,0-4.86-3.21,12.64,12.64,0,0,0-2.44.73c-2.07.72-4.13,1.47-6.22,2.21l-.26-.54q-1.94-4.52-3.88-9c-1.77-4.11-3.53-8.22-5.32-12.31a4.21,4.21,0,0,0-4.06-2.71c-4.84-.06-9.68-.07-14.52-.11L81,131.46c-.18,0-.5-.09-.52-.19-.16-.78-.27-1.58-.4-2.42H99.2c.35,0,.69,0,1,0a4.41,4.41,0,0,0,3.41-6.75,4.58,4.58,0,0,0-4.22-2.1q-10,0-20,0h-.72c-.52-3.24-1-6.39-1.54-9.56h-9c.55,3.45,1.11,6.9,1.66,10.35.83,5.11,1.71,10.2,2.47,15.32a4.76,4.76,0,0,0,4.92,4.29c9.22,0,18.45.11,27.68.15a.86.86,0,0,1,.91.61q4.72,11.05,9.48,22.08a4.49,4.49,0,0,0,6.12,2.69c3.69-1.31,7.39-2.61,11.07-3.95a6.74,6.74,0,0,0,1.67-1,4.71,4.71,0,0,0,1.49-2.56v-1.48C135.55,156.8,135.5,156.66,135.46,156.52Z"/><path class="cls-1" d="M53.61,145.09c.2-1.23.33-2.47.59-3.69a26.58,26.58,0,0,1,6.42-12.56,4.42,4.42,0,0,1,6.63,5.85,18.7,18.7,0,0,0-4.46,8.95,18.32,18.32,0,0,0,34.75,10.84,4.62,4.62,0,0,1,3.67-3,4.41,4.41,0,0,1,4.76,5.84,23.53,23.53,0,0,1-3.45,6.12,26.77,26.77,0,0,1-14.83,10c-1.46.38-3,.53-4.47.79l-.56.11H78.9c-.14,0-.29-.08-.43-.1a34.79,34.79,0,0,1-3.7-.6c-11-2.89-17.86-9.91-20.58-20.95-.29-1.19-.39-2.43-.58-3.64Z"/><ellipse class="cls-1" cx="72.62" cy="109.74" rx="4.51" ry="3.94"/><circle class="cls-1" cx="71.76" cy="95.49" r="8.93"/><path class="cls-1" d="M157.33,131.79H154v1.75q0,18,0,36c0,2.4-1,3.89-2.94,4.52a4.17,4.17,0,0,1-5.43-3.17,10.86,10.86,0,0,1-.13-2q0-31,0-61.93v-2.49h-2c0,.48-.07,1-.07,1.47,0,7.32,0,14.65,0,22,0,2.28-1.42,3.66-3.4,3.39a3,3,0,0,1-2.63-3.33c0-5.24,0-10.47,0-15.7,0-4.38,0-8.75,0-13.13,0-5.09,2.76-7.89,7.83-7.92q10.48-.06,21,0c4.89,0,7.67,2.88,7.67,7.77q0,14.49,0,29c0,2.3-1.56,3.7-3.6,3.31-1.57-.3-2.41-1.44-2.41-3.35q0-10.84,0-21.69v-1.76h-2v1.76q0,31.32,0,62.64a10.81,10.81,0,0,1-.17,2.13,4.23,4.23,0,0,1-4.41,3.27,4.18,4.18,0,0,1-3.9-4c0-2.95,0-5.9,0-8.84V131.79Z"/><path class="cls-1" d="M155.74,89.56a9,9,0,0,1-9.1-9,9,9,0,0,1,18,0A9,9,0,0,1,155.74,89.56Z"/><path class="cls-1" d="M191.93,36.94h-71V24.12h10l-17-17-17,17h10V36.94h-71a5.83,5.83,0,0,0-5.83,5.83V197.05a5.83,5.83,0,0,0,5.83,5.83h71V215.7h-10l17,17,17-17h-10V202.88h71a5.83,5.83,0,0,0,5.83-5.83V42.77A5.83,5.83,0,0,0,191.93,36.94Zm-7.5,147a5.83,5.83,0,0,1-5.83,5.84H49.22a5.83,5.83,0,0,1-5.83-5.84V56a5.83,5.83,0,0,1,5.83-5.83H178.6A5.83,5.83,0,0,1,184.43,56Z"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -387,6 +387,16 @@
],
"sources": []
},
{
"path": "elevator_wheelchair.svg",
"license": "CC-BY_SA",
"authors": [
"Robin Julien"
],
"sources": [
"https://www.ctsteward.com/"
]
},
{
"path": "envelope.svg",
"license": "CC0; trivial",

View file

@ -1,21 +1,27 @@
{
"id": "shared_questions",
"questions": {
"description": "Show the images block at this location",
"id": "questions"
},
"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)}"
},
"mapillary": {
"description": "Shows a button to open Mapillary on this location",
"render": "{mapillary()}"
},
"export_as_gpx": {
"description": "Shows a button to export this feature as GPX. Especially useful for route relations",
"render": "{export_as_gpx()}"
},
"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()}"
},
"wikipedia": {
"description": "Shows a wikipedia box with the corresponding wikipedia article",
"render": "{wikipedia():max-height:25rem}",
"question": {
"en": "What is the corresponding Wikidata entity?",
@ -93,9 +99,12 @@
}
},
"reviews": {
"description": "Shows the reviews module (including the possibility to leave a review)",
"render": "{reviews()}"
},
"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;}"
},
"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>"
},
"all_tags": {
"#": "Prints all the tags",
"description": "Shows a table with all the tags of the feature",
"render": "{all_tags()}"
},
"level": {

View file

@ -0,0 +1,11 @@
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M397 200C397 308.8 308.8 397 200 397C91.1999 397 3 308.8 3 200C3 91.1999 91.1999 3 200 3C308.8 3 397 91.1999 397 200Z" fill="#22CA60" stroke="#E62222" stroke-width="6"/>
<path d="M242.105 240.241L137.282 291.429L155.338 312.97L167.374 327.329L185.428 348.868L254.141 254.6L242.105 240.241Z" fill="#ECF0F1"/>
<path d="M138.93 283.603L122.535 283.627L190.077 364.206L192.964 348.067L138.93 283.603Z" fill="#FE5757"/>
<path d="M242.105 240.241L224.633 248.772L242.689 270.312L254.141 254.6L242.105 240.241ZM201.309 260.11L177.983 271.447L212.214 312.285L227.45 291.296L201.309 260.11ZM154.752 282.898L137.282 291.429L155.338 312.97L167.374 327.329L185.428 348.868L196.88 333.157L154.754 282.9L154.752 282.898Z" fill="#E62222"/>
<path d="M122.535 283.627L190.077 364.206L185.257 368.246L117.714 287.667L122.535 283.627Z" fill="#E62222"/>
<path d="M243.871 187.187C241.2 193.219 236.746 198.533 230.685 202.228C214.87 211.87 194.229 206.866 184.586 191.05C180.892 184.991 179.363 178.227 179.755 171.642L162.087 167.358C160.538 178.79 162.721 190.803 169.195 201.422C184.17 225.982 216.219 233.753 240.777 218.78C251.396 212.306 258.87 202.636 262.73 191.761L243.871 187.187Z" fill="#FCFCFF"/>
<path d="M220.03 69.1601C228.136 64.2175 230.701 53.639 225.759 45.5323C220.816 37.4256 210.237 34.8606 202.131 39.8032C194.024 44.7458 191.459 55.3243 196.402 63.431C201.344 71.5377 211.923 74.1027 220.03 69.1601Z" fill="#FCFCFF"/>
<path d="M250.824 98.9454L218.518 118.642L225.708 88.9832C225.995 88.2028 226.192 87.3988 226.294 86.5735L226.353 86.3324L226.329 86.3268C226.556 84.0714 226.115 81.7309 224.844 79.6478C222.814 76.3168 219.268 74.5145 215.635 74.5114L215.635 74.5081L159.854 75.8435L159.857 75.9264C158.38 76.1088 156.92 76.5767 155.568 77.4013C153.203 78.8428 151.63 81.0604 150.891 83.5185L150.871 83.5137L150.823 83.7119C150.755 83.956 150.696 84.1972 150.645 84.4453L143.544 113.732L143.57 113.739C143.012 116.342 143.389 119.153 144.885 121.608C147.987 126.696 154.623 128.303 159.709 125.202C162.164 123.706 163.786 121.379 164.482 118.809L164.509 118.816L164.574 118.548L164.58 118.522L169.753 97.19L185.053 96.8249L174.632 139.811C170.442 144.547 167.246 149.967 165.048 155.739L182.58 159.99C185.251 153.959 189.705 148.644 195.764 144.951C211.581 135.307 232.221 140.313 241.863 156.128C245.557 162.187 247.086 168.952 246.694 175.536L265.414 180.077C266.655 169.003 264.383 157.449 258.132 147.197C253.744 140 247.883 134.257 241.182 130.094L252.845 122.983L275.833 160.687C278.935 165.775 285.572 167.384 290.66 164.282C295.748 161.18 297.356 154.541 294.255 149.455L265.65 102.538C262.549 97.4527 255.91 95.8446 250.824 98.9454Z" fill="#FCFCFF"/>
<path d="M104.007 155.511C105.121 154.899 105.528 153.5 104.917 152.386C104.305 151.272 102.905 150.865 101.791 151.476L95.7395 154.8C91.283 157.248 89.6546 162.845 92.1022 167.301C94.5498 171.758 100.147 173.386 104.603 170.938L130.828 156.535C131.942 155.923 132.349 154.524 131.737 153.41C131.125 152.296 129.726 151.889 128.612 152.501L102.387 166.904C100.159 168.128 97.3605 167.313 96.1367 165.085C94.9129 162.857 95.7272 160.059 97.9554 158.835L104.007 155.511ZM79.7999 168.806C80.914 168.194 81.3211 166.795 80.7092 165.681C80.0973 164.567 78.6981 164.16 77.584 164.772L73.5495 166.988C65.7507 171.271 62.9009 181.066 67.1842 188.864C71.4675 196.663 81.262 199.513 89.0608 195.23L145.544 164.207C146.659 163.595 147.066 162.196 146.454 161.082C145.842 159.968 144.443 159.56 143.329 160.172L86.8449 191.195C81.2743 194.255 74.2783 192.219 71.2187 186.648C68.1592 181.078 70.1948 174.082 75.7654 171.022L79.7999 168.806ZM136.054 185.174C137.168 184.562 137.575 183.163 136.963 182.049C136.351 180.935 134.952 180.528 133.838 181.14L103.579 197.759C99.1225 200.207 97.494 205.803 99.9417 210.26C102.389 214.716 107.986 216.345 112.443 213.897L118.494 210.573C119.608 209.961 120.016 208.562 119.404 207.448C118.792 206.334 117.393 205.927 116.278 206.539L110.227 209.863C107.998 211.086 105.2 210.272 103.976 208.044C102.752 205.816 103.567 203.017 105.795 201.793L136.054 185.174Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -0,0 +1,20 @@
{
"id": "governments",
"title": {
"en": "Governmental Offices"
},
"description": {
"en": "On this map, Governmental offices are shown and can be easily added"
},
"maintainer": "MapComplete",
"icon": "./assets/themes/onwheels/crest.svg",
"version": "0",
"startLat": 50.8465573,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 4.351697,
"startZoom": 16,
"widenFactor": 2,
"layers": [
"governments"
]
}

View file

@ -0,0 +1,10 @@
[
{
"path": "crest.svg",
"license": "CC0",
"authors": [
"Free Wheelies"
],
"sources": []
}
]

View file

@ -187,6 +187,10 @@
"if": "theme=ghostbikes",
"then": "./assets/themes/ghostbikes/logo.svg"
},
{
"if": "theme=governments",
"then": "./assets/themes/onwheels/crest.svg"
},
{
"if": "theme=grb",
"then": "./assets/themes/grb_import/logo.svg"

View file

@ -125,7 +125,11 @@
},
{
"if": "sidewalk:left|right=no",
"then": "No, there is no seperated sidewalk to walk on"
"then": "No, there is no sidewalk to walk on"
},
{
"if": "sidewalk:left|right=separate",
"then": "There is a separately mapped sidewalk to walk on"
}
]
},
@ -224,4 +228,4 @@
"allowSplit": true
}
]
}
}

View file

@ -33,6 +33,7 @@
"layers": [
{
"id": "shadow",
"title": null,
"source": {
"geoJson": "https://raw.githubusercontent.com/pietervdvn/MapComplete/master/assets/themes/speelplekken/shadow.geojson",
"osmTags": "shadow=yes",

View file

@ -819,6 +819,10 @@ video {
margin: 1.25rem;
}
.m-2 {
margin: 0.5rem;
}
.m-0\.5 {
margin: 0.125rem;
}
@ -831,10 +835,6 @@ video {
margin: 0.75rem;
}
.m-2 {
margin: 0.5rem;
}
.m-6 {
margin: 1.5rem;
}
@ -866,6 +866,18 @@ video {
margin-bottom: 1rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.ml-0 {
margin-left: 0px;
}
.mr-8 {
margin-right: 2rem;
}
.mt-4 {
margin-top: 1rem;
}
@ -874,6 +886,10 @@ video {
margin-top: 1.5rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.mt-1 {
margin-top: 0.25rem;
}
@ -890,14 +906,6 @@ video {
margin-right: 1rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
@ -1042,8 +1050,8 @@ video {
height: 6rem;
}
.h-8 {
height: 2rem;
.h-64 {
height: 16rem;
}
.h-full {
@ -1058,14 +1066,18 @@ video {
height: 3rem;
}
.h-1\/2 {
height: 50%;
.h-8 {
height: 2rem;
}
.h-4 {
height: 1rem;
}
.h-1\/2 {
height: 50%;
}
.h-screen {
height: 100vh;
}
@ -1090,10 +1102,6 @@ video {
height: 0px;
}
.h-64 {
height: 16rem;
}
.h-3 {
height: 0.75rem;
}
@ -1102,6 +1110,10 @@ video {
height: 12rem;
}
.max-h-screen {
max-height: 100vh;
}
.max-h-7 {
max-height: 1.75rem;
}
@ -1126,18 +1138,14 @@ video {
width: 100%;
}
.w-8 {
width: 2rem;
}
.w-1 {
width: 0.25rem;
}
.w-24 {
width: 6rem;
}
.w-1\/2 {
width: 50%;
}
.w-6 {
width: 1.5rem;
}
@ -1150,14 +1158,18 @@ video {
width: 3rem;
}
.w-0 {
width: 0px;
.w-8 {
width: 2rem;
}
.w-4 {
width: 1rem;
}
.w-0 {
width: 0px;
}
.w-screen {
width: 100vw;
}
@ -1171,15 +1183,15 @@ video {
width: min-content;
}
.w-1\/2 {
width: 50%;
}
.w-max {
width: -webkit-max-content;
width: max-content;
}
.w-32 {
width: 8rem;
}
.w-16 {
width: 4rem;
}
@ -1352,6 +1364,10 @@ video {
overflow: scroll;
}
.overflow-x-auto {
overflow-x: auto;
}
.overflow-y-auto {
overflow-y: auto;
}
@ -1395,14 +1411,18 @@ video {
border-radius: 0.25rem;
}
.rounded-xl {
border-radius: 0.75rem;
.rounded-md {
border-radius: 0.375rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-xl {
border-radius: 0.75rem;
}
.rounded-sm {
border-radius: 0.125rem;
}
@ -1424,6 +1444,10 @@ video {
border-width: 4px;
}
.border-b-4 {
border-bottom-width: 4px;
}
.border-l-4 {
border-left-width: 4px;
}
@ -1524,14 +1548,14 @@ video {
padding: 1rem;
}
.p-1 {
padding: 0.25rem;
}
.p-2 {
padding: 0.5rem;
}
.p-1 {
padding: 0.25rem;
}
.p-0 {
padding: 0px;
}
@ -1550,8 +1574,12 @@ video {
padding-right: 1rem;
}
.pr-2 {
padding-right: 0.5rem;
.pl-1 {
padding-left: 0.25rem;
}
.pr-1 {
padding-right: 0.25rem;
}
.pb-12 {
@ -1578,14 +1606,6 @@ video {
padding-bottom: 0.25rem;
}
.pl-1 {
padding-left: 0.25rem;
}
.pr-1 {
padding-right: 0.25rem;
}
.pt-2 {
padding-top: 0.5rem;
}
@ -1622,6 +1642,10 @@ video {
padding-top: 0.125rem;
}
.pr-2 {
padding-right: 0.5rem;
}
.pl-6 {
padding-left: 1.5rem;
}
@ -1693,10 +1717,6 @@ video {
text-transform: lowercase;
}
.capitalize {
text-transform: capitalize;
}
.italic {
font-style: italic;
}
@ -1844,11 +1864,20 @@ video {
z-index: 10001
}
.w-160 {
width: 40rem;
}
.bg-subtle {
background-color: var(--subtle-detail-color);
color: var(--subtle-detail-color-contrast);
}
.bg-unsubtle {
background-color: var(--unsubtle-detail-color);
color: var(--unsubtle-detail-color-contrast);
}
:root {
/* The main colour scheme of mapcomplete is configured here.
* For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these.
@ -2429,6 +2458,15 @@ input {
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 **/
.small-image img {
@ -2477,7 +2515,7 @@ input {
.mapping-icon-small-height {
/* A mapping icon type */
height: 2rem;
height: 1.5rem;
margin-right: 0.5rem;
width: unset;
}
@ -2791,6 +2829,14 @@ input {
width: 75%;
}
.lg\:w-1\/3 {
width: 33.333333%;
}
.lg\:w-1\/4 {
width: 25%;
}
.lg\:w-1\/6 {
width: 16.666667%;
}

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 **/
.small-image img {
height: 1em;

View file

@ -9,6 +9,8 @@ import DefaultGUI from "./UI/DefaultGUI";
import State from "./State";
import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation";
import {DefaultGuiState} from "./UI/DefaultGuiState";
import {QueryParameters} from "./Logic/Web/QueryParameters";
import DashboardGui from "./UI/DashboardGui";
// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts running from console
MinimapImplementation.initialize()
@ -36,7 +38,13 @@ class Init {
// This 'leaks' the global state via the window object, useful for debugging
// @ts-ignore
window.mapcomplete_state = State.state;
new DefaultGUI(State.state, guiState).setup()
const mode = QueryParameters.GetQueryParameter("mode", "map", "The mode the application starts in, e.g. 'map' or 'dashboard'")
if(mode.data === "dashboard"){
new DashboardGui(State.state, guiState).setup()
}else{
new DefaultGUI(State.state, guiState).setup()
}
}
}

11
package-lock.json generated
View file

@ -27,6 +27,7 @@
"@types/prompt-sync": "^4.1.0",
"@types/wikidata-sdk": "^6.1.0",
"@types/xml2js": "^0.4.9",
"chart.js": "^3.8.0",
"country-language": "^0.1.7",
"csv-parse": "^5.1.0",
"doctest-ts-improved": "^0.8.8",
@ -4525,6 +4526,11 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
"node_modules/chart.js": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz",
"integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg=="
},
"node_modules/check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
@ -20309,6 +20315,11 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
"chart.js": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz",
"integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg=="
},
"check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",

View file

@ -86,6 +86,7 @@
"@types/prompt-sync": "^4.1.0",
"@types/wikidata-sdk": "^6.1.0",
"@types/xml2js": "^0.4.9",
"chart.js": "^3.8.0",
"country-language": "^0.1.7",
"csv-parse": "^5.1.0",
"doctest-ts-improved": "^0.8.8",

View file

@ -1,18 +1,15 @@
import Combine from "../UI/Base/Combine";
import BaseUIElement from "../UI/BaseUIElement";
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 TableOfContents from "../UI/Base/TableOfContents";
import SimpleMetaTaggers, {SimpleMetaTagger} from "../Logic/SimpleMetaTagger";
import SimpleMetaTaggers from "../Logic/SimpleMetaTagger";
import ValidatedTextField from "../UI/Input/ValidatedTextField";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import SpecialVisualizations from "../UI/SpecialVisualizations";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import {ExtraFunctions} from "../Logic/ExtraFunctions";
import Title from "../UI/Base/Title";
import Minimap from "../UI/Base/Minimap";
import {QueryParameters} from "../Logic/Web/QueryParameters";
import QueryParameterDocumentation from "../UI/QueryParameterDocumentation";
import ScriptUtils from "./ScriptUtils";
import List from "../UI/Base/List";

View file

@ -296,8 +296,8 @@ const iconThief = new AggregateIconThief(
const thief = new IdThief("../id-tagging-schema/", iconThief)
const shopLayerPath = targetDir + "id_presets.json"
const idPresets = <LayerConfigJson>JSON.parse(readFileSync(shopLayerPath, 'utf8'))
const id_presets_path = targetDir + "id_presets.json"
const idPresets = <LayerConfigJson>JSON.parse(readFileSync(id_presets_path, 'utf8'))
idPresets.tagRenderings = [
{
id: "shop_types",
@ -309,4 +309,4 @@ idPresets.tagRenderings = [
}
]
writeFileSync(shopLayerPath, JSON.stringify(idPresets, null, " "), 'utf8')
writeFileSync(id_presets_path, JSON.stringify(idPresets, null, " "), 'utf8')

View file

@ -54,11 +54,14 @@ function main(){
const wikidataLayer = <LayerConfigJson>{
id: "wikidata",
description: "Various tagrenderings which are generated from Wikidata. Automatically generated with a script, don't edit manually",
description: {
en: "Various tagrenderings which are generated from Wikidata. Automatically generated with a script, don't edit manually"
},
"#dont-translate": "*",
"source": {
"osmTags": "id~*"
},
title: null,
"mapRendering": null,
tagRenderings: [
{

120
test.ts
View file

@ -1,52 +1,76 @@
import * as shops from "./assets/generated/layers/shops.json"
import Combine from "./UI/Base/Combine";
import Img from "./UI/Base/Img";
import BaseUIElement from "./UI/BaseUIElement";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import LanguagePicker from "./UI/LanguagePicker";
import TagRenderingConfig, {Mapping} from "./Models/ThemeConfig/TagRenderingConfig";
import {MappingConfigJson} from "./Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
import {FixedUiElement} from "./UI/Base/FixedUiElement";
import {TagsFilter} from "./Logic/Tags/TagsFilter";
import {SearchablePillsSelector} from "./UI/Input/SearchableMappingsSelector";
import ChartJs from "./UI/Base/ChartJs";
import TagRenderingChart from "./UI/BigComponents/TagRenderingChart";
import {OsmFeature} from "./Models/OsmFeature";
import * as food from "./assets/generated/layers/food.json"
import TagRenderingConfig from "./Models/ThemeConfig/TagRenderingConfig";
import {UIEventSource} from "./Logic/UIEventSource";
const mappingsRaw: MappingConfigJson[] = <any>shops.tagRenderings.find(tr => tr.id == "shop_types").mappings
const mappings = mappingsRaw.map((m, i) => TagRenderingConfig.ExtractMapping(m, i, "test", "test"))
function fromMapping(m: Mapping): { show: BaseUIElement, value: TagsFilter, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]> } {
const el: BaseUIElement = m.then
let icon: BaseUIElement
if (m.icon !== undefined) {
icon = new Img(m.icon).SetClass("h-8 w-8 pr-2")
} else {
icon = new FixedUiElement("").SetClass("h-8 w-1")
}
const show = new Combine([
icon,
el.SetClass("block-ruby")
]).SetClass("flex items-center")
return {show, mainTerm: m.then.translations, searchTerms: m.searchTerms, value: m.if};
}
const search = new UIEventSource("")
const sp = new SearchablePillsSelector(
mappings.map(m => fromMapping(m)),
import Combine from "./UI/Base/Combine";
const data = new UIEventSource<OsmFeature[]>([
{
noMatchFound: new VariableUiElement(search.map(s => "Mark this a `"+s+"`")),
onNoSearch: new FixedUiElement("Search in "+mappingsRaw.length+" categories"),
selectIfSingle: true,
searchValue: search
properties: {
id: "node/1234",
cuisine:"pizza",
"payment:cash":"yes"
},
geometry:{
type: "Point",
coordinates: [0,0]
},
id: "node/1234",
type: "Feature"
},
{
properties: {
id: "node/42",
cuisine:"pizza",
"payment:cash":"yes"
},
geometry:{
type: "Point",
coordinates: [1,0]
},
id: "node/42",
type: "Feature"
},
{
properties: {
id: "node/452",
cuisine:"pasta",
"payment:cash":"yes",
"payment:cards":"yes"
},
geometry:{
type: "Point",
coordinates: [2,0]
},
id: "node/452",
type: "Feature"
},
{
properties: {
id: "node/4542",
cuisine:"something_comletely_invented",
"payment:cards":"yes"
},
geometry:{
type: "Point",
coordinates: [3,0]
},
id: "node/4542",
type: "Feature"
},
{
properties: {
id: "node/45425",
},
geometry:{
type: "Point",
coordinates: [3,0]
},
id: "node/45425",
type: "Feature"
}
)
]);
sp.AttachTo("maindiv")
const lp = new LanguagePicker(["en", "nl"], "")
new Combine([
new VariableUiElement(sp.GetValue().map(tf => new FixedUiElement("Selected tags: " + tf.map(tf => tf.asHumanString(false, false, {})).join(", ")))),
lp
]).SetClass("flex flex-col")
.AttachTo("extradiv")
new Combine(food.tagRenderings.map(tr => new TagRenderingChart(data, new TagRenderingConfig(tr, "test"), {chartclasses: "w-160 h-160"})))
.AttachTo("maindiv")