Merge branch 'feature/dashboard-layout' into develop

This commit is contained in:
pietervdvn 2022-07-20 15:23:46 +02:00
commit fb4b4e5702
23 changed files with 846 additions and 221 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")
@ -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. ') extraProps.push('This layer is not visible by default and must be enabled in the filter by the user. ')
} }
if (this.title === undefined) { 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") 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) { if (this.name === undefined) {
@ -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,
@ -38,6 +40,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[] = []
@ -56,6 +59,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;
@ -107,6 +111,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) {
@ -571,8 +576,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")
] ]
) )
] ]
@ -607,12 +612,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"),
@ -621,6 +628,6 @@ export default class TagRenderingConfig {
condition, condition,
group, group,
labels 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 { protected InnerConstructElement(): HTMLElement {
const el = document.createElement("span") const el = document.createElement("span")
try { try {
if(this.uiElements === undefined){
console.error("PANIC")
}
for (const subEl of this.uiElements) { for (const subEl of this.uiElements) {
if (subEl === undefined || subEl === null) { if (subEl === undefined || subEl === null) {
continue; continue;

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

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

View file

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

View file

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

View file

@ -43,6 +43,14 @@ export interface PresetInfo extends PresetConfig {
export default class SimpleAddUI extends Toggle { 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>, constructor(isShown: UIEventSource<boolean>,
resetScrollSignal: UIEventSource<void>, resetScrollSignal: UIEventSource<void>,
filterViewIsOpened: UIEventSource<boolean>, filterViewIsOpened: UIEventSource<boolean>,
@ -59,7 +67,9 @@ export default class SimpleAddUI extends Toggle {
filteredLayers: UIEventSource<FilteredLayer[]>, filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>, featureSwitchFilter: UIEventSource<boolean>,
backgroundLayer: UIEventSource<BaseLayer> backgroundLayer: UIEventSource<BaseLayer>
}) { },
takeLocationFrom?: UIEventSource<{lat: number, lon: number}>
) {
const loginButton = new SubtleButton(Svg.osm_logo_ui(), Translations.t.general.add.pleaseLogin.Clone()) const loginButton = new SubtleButton(Svg.osm_logo_ui(), Translations.t.general.add.pleaseLogin.Clone())
.onClick(() => state.osmConnection.AttemptLogin()); .onClick(() => state.osmConnection.AttemptLogin());
const readYourMessages = new Combine([ const readYourMessages = new Combine([
@ -69,6 +79,7 @@ export default class SimpleAddUI extends Toggle {
]); ]);
takeLocationFrom = takeLocationFrom ?? state.LastClickLocation
const selectedPreset = new UIEventSource<PresetInfo>(undefined); const selectedPreset = new UIEventSource<PresetInfo>(undefined);
selectedPreset.addCallback(_ => { selectedPreset.addCallback(_ => {
resetScrollSignal.ping(); 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 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) 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"]); const message = Translations.t.general.add.addNew.Subs({category: preset.name}, preset.name["context"]);
return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset, return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset,
message, message,
state.LastClickLocation.data, takeLocationFrom.data,
confirm, confirm,
cancel, 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")
}
}

345
UI/DashboardGui.ts Normal file
View file

@ -0,0 +1,345 @@
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?.GetRenderValue(element)?.Subs(element.properties)?.txt === "no") {
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: true
})
const full = new Lazy(() =>
new TagRenderingChart(featuresForLayer, tagRendering, {
chartstyle: "max-height: calc(100vh - 10rem)",
groupToOtherCutoff: 0
})
)
chart.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
})
}
)
layerStats.push(chart.SetClass("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(){ public setup(){
if (this.state.layoutToUse.customCss !== undefined) {
Utils.LoadCustomCss(this.state.layoutToUse.customCss);
}
this.SetupUIElements(); this.SetupUIElements();
this.SetupMap() this.SetupMap()
if (this.state.layoutToUse.customCss !== undefined && window.location.pathname.indexOf("index") >= 0) { if (this.state.layoutToUse.customCss !== undefined && window.location.pathname.indexOf("index") >= 0) {
Utils.LoadCustomCss(this.state.layoutToUse.customCss) Utils.LoadCustomCss(this.state.layoutToUse.customCss)
} }
@ -144,7 +139,7 @@ export default class DefaultGUI {
new ShowDataLayer({ new ShowDataLayer({
leafletMap: state.leafletMap, 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, features: state.homeLocation,
state state
}) })

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, layerConfig: LayerConfig,
state: {}): BaseUIElement { state: {}): BaseUIElement {
const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI"), state) 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, layerConfig: LayerConfig,
state: FeaturePipelineState): BaseUIElement { state: FeaturePipelineState): BaseUIElement {
let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>(); 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 {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

@ -282,70 +282,9 @@
}, },
"id": "bike_shop-name" "id": "bike_shop-name"
}, },
{ "website",
"question": { "phone",
"en": "What is the website of {name}?", "email",
"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"
},
"opening_hours", "opening_hours",
{ {
"render": { "render": {

View file

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

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

View file

@ -9,6 +9,8 @@ import DefaultGUI from "./UI/DefaultGUI";
import State from "./State"; import State from "./State";
import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation"; import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation";
import {DefaultGuiState} from "./UI/DefaultGuiState"; 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 // 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() MinimapImplementation.initialize()
@ -36,7 +38,13 @@ class Init {
// This 'leaks' the global state via the window object, useful for debugging // This 'leaks' the global state via the window object, useful for debugging
// @ts-ignore // @ts-ignore
window.mapcomplete_state = State.state; 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()
}
} }
} }

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";

120
test.ts
View file

@ -1,52 +1,76 @@
import * as shops from "./assets/generated/layers/shops.json" import ChartJs from "./UI/Base/ChartJs";
import Combine from "./UI/Base/Combine"; import TagRenderingChart from "./UI/BigComponents/TagRenderingChart";
import Img from "./UI/Base/Img"; import {OsmFeature} from "./Models/OsmFeature";
import BaseUIElement from "./UI/BaseUIElement"; import * as food from "./assets/generated/layers/food.json"
import {VariableUiElement} from "./UI/Base/VariableUIElement"; import TagRenderingConfig from "./Models/ThemeConfig/TagRenderingConfig";
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 {UIEventSource} from "./Logic/UIEventSource"; import {UIEventSource} from "./Logic/UIEventSource";
import Combine from "./UI/Base/Combine";
const mappingsRaw: MappingConfigJson[] = <any>shops.tagRenderings.find(tr => tr.id == "shop_types").mappings const data = new UIEventSource<OsmFeature[]>([
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)),
{ {
noMatchFound: new VariableUiElement(search.map(s => "Mark this a `"+s+"`")), properties: {
onNoSearch: new FixedUiElement("Search in "+mappingsRaw.length+" categories"), id: "node/1234",
selectIfSingle: true, cuisine:"pizza",
searchValue: search "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") new Combine(food.tagRenderings.map(tr => new TagRenderingChart(data, new TagRenderingConfig(tr, "test"), {chartclasses: "w-160 h-160"})))
.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")