Merge develop

This commit is contained in:
Pieter Vander Vennet 2022-07-21 19:23:05 +02:00
commit 15e6fde194
275 changed files with 786405 additions and 39150 deletions

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;
@ -117,7 +117,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

@ -42,7 +42,7 @@ export default class ShareScreen extends Combine {
} else {
return null;
}
}, [currentLocation]));
@ -94,7 +94,7 @@ export default class ShareScreen extends Combine {
for (const swtch of switches) {
const checkbox =new CheckBox(Translations.W(swtch.human), !swtch.reverse)
const checkbox = new CheckBox(Translations.W(swtch.human), !swtch.reverse)
optionCheckboxes.push(checkbox);
optionParts.push(checkbox.GetValue().map((isEn) => {
if (isEn) {
@ -113,8 +113,8 @@ export default class ShareScreen extends Combine {
}
if(layout.definitionRaw !== undefined){
optionParts.push(new UIEventSource("userlayout="+(layout.definedAtUrl ?? layout.id)))
if (layout.definitionRaw !== undefined) {
optionParts.push(new UIEventSource("userlayout=" + (layout.definedAtUrl ?? layout.id)))
}
const options = new Combine(optionCheckboxes).SetClass("flex flex-col")
@ -124,14 +124,14 @@ export default class ShareScreen extends Combine {
let path = window.location.pathname;
path = path.substr(0, path.lastIndexOf("/"));
let id = layout.id.toLowerCase()
if(layout.definitionRaw !== undefined){
id="theme.html"
if (layout.definitionRaw !== undefined) {
id = "theme.html"
}
let literalText = `https://${host}${path}/${id}`
let hash = ""
if(layout.definedAtUrl === undefined && layout.definitionRaw !== undefined){
hash = "#"+ LZString.compressToBase64( Utils.MinifyJSON(layout.definitionRaw))
if (layout.definedAtUrl === undefined && layout.definitionRaw !== undefined) {
hash = "#" + LZString.compressToBase64(Utils.MinifyJSON(layout.definitionRaw))
}
const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data)));
if (parts.length === 0) {
@ -189,18 +189,30 @@ export default class ShareScreen extends Combine {
});
let downloadThemeConfig: BaseUIElement = undefined;
if(layout.definitionRaw !== undefined){
downloadThemeConfig = new SubtleButton(Svg.download_svg(), new Combine([
if (layout.definitionRaw !== undefined) {
const downloadThemeConfigAsJson = new SubtleButton(Svg.download_svg(), new Combine([
tr.downloadCustomTheme,
tr.downloadCustomThemeHelp.SetClass("subtle")
]).onClick(() => {
Utils.offerContentsAsDownloadableFile(layout.definitionRaw, layout.id+".mapcomplete-theme-definition.json", {
mimetype:"application/json"
Utils.offerContentsAsDownloadableFile(layout.definitionRaw, layout.id + ".mapcomplete-theme-definition.json", {
mimetype: "application/json"
})
})
.SetClass("flex flex-col"))
let editThemeConfig: BaseUIElement = undefined
if (layout.definedAtUrl === undefined) {
const patchedDefinition = JSON.parse(layout.definitionRaw)
patchedDefinition["language"] = Object.keys(patchedDefinition.title)
editThemeConfig = new SubtleButton(Svg.pencil_svg(), "Edit this theme on the custom theme generator",
{
url: `https://pietervdvn.github.io/mc/legacy/070/customGenerator.html#${btoa(JSON.stringify(patchedDefinition))}`
}
)
}
downloadThemeConfig = new Combine([downloadThemeConfigAsJson, editThemeConfig]).SetClass("flex flex-col")
}
super([

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

@ -57,7 +57,13 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
return true
}
s = s?.trim()?.toLowerCase()
return searchTerms[Locale.language.data].some(t => t.indexOf(s) >= 0);
if(searchTerms[Locale.language.data]?.some(t => t.indexOf(s) >= 0)){
return true
}
if(searchTerms["*"]?.some(t => t.indexOf(s) >= 0)){
return true
}
return false;
}, [selected, Locale.language])
const self = this;
@ -121,10 +127,15 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
* A searchfield can be used to filter the values
*/
export class SearchablePillsSelector<T> extends Combine implements InputElement<T[]> {
private selectedElements: UIEventSource<T[]>;
private readonly selectedElements: UIEventSource<T[]>;
public readonly someMatchFound: Store<boolean>;
/**
*
* @param values
* @param options
*/
constructor(
values: { show: BaseUIElement, value: T, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]> }[],
options?: {
@ -133,13 +144,19 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
searchValue?: UIEventSource<string>,
onNoMatches?: BaseUIElement,
onNoSearchMade?: BaseUIElement,
/**
* Shows this if there are many (>200) possible mappings
*/
onManyElements?: BaseUIElement,
onManyElementsValue?: UIEventSource<T[]>,
selectIfSingle?: false | boolean,
searchAreaClass?: string
searchAreaClass?: string,
hideSearchBar?: false | boolean
}) {
const search = new TextField({value: options?.searchValue})
const searchBar = new Combine([Svg.search_svg().SetClass("w-8 normal-background"), search.SetClass("w-full")])
const searchBar = options?.hideSearchBar ? undefined : new Combine([Svg.search_svg().SetClass("w-8 normal-background"), search.SetClass("w-full")])
.SetClass("flex items-center border-2 border-black m-2")
const searchValue = search.GetValue().map(s => s?.trim()?.toLowerCase())
@ -188,11 +205,10 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
};
})
let somethingShown: Store<boolean>
let totalShown: Store<number>
if (options.selectIfSingle) {
let forcedSelection : { value: T, show: SelfHidingToggle } = undefined
somethingShown = searchValue.map(_ => {
totalShown = searchValue.map(_ => {
let totalShown = 0;
let lastShownValue: { value: T, show: SelfHidingToggle }
for (const mv of mappedValues) {
@ -203,42 +219,54 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
}
}
if (totalShown == 1) {
if (this.selectedElements.data.indexOf(lastShownValue.value) < 0) {
this.selectedElements.setData([lastShownValue.value])
if (selectedElements.data?.indexOf(lastShownValue.value) < 0) {
selectedElements.setData([lastShownValue.value])
lastShownValue.show.forceSelected.setData(true)
forcedSelection = lastShownValue
}
} else if (forcedSelection != undefined) {
forcedSelection?.show?.forceSelected?.setData(false)
forcedSelection = undefined;
this.selectedElements.setData([])
selectedElements.setData([])
}
return totalShown > 0
return totalShown
}, mappedValues.map(mv => mv.show.GetValue()))
} else {
somethingShown = searchValue.map(_ => mappedValues.some(mv => mv.show.isShown.data), mappedValues.map(mv => mv.show.GetValue()))
totalShown = searchValue.map(_ => mappedValues.filter(mv => mv.show.isShown.data).length, mappedValues.map(mv => mv.show.GetValue()))
}
const tooMuchElementsCutoff = 200;
options?.onManyElementsValue?.map(value => {
console.log("Installing toMuchElementsValue", value)
if(tooMuchElementsCutoff <= totalShown.data){
selectedElements.setData(value)
selectedElements.ping()
}
}, [totalShown])
super([
searchBar,
new VariableUiElement(Locale.language.map(lng => {
if(totalShown.data >= 200){
return options?.onManyElements ?? Translations.t.general.useSearch;
}
if (options?.onNoSearchMade !== undefined && (searchValue.data === undefined || searchValue.data.length === 0)) {
return options?.onNoSearchMade
}
if (!somethingShown.data) {
if (totalShown.data == 0) {
return onEmpty
}
mappedValues.sort((a, b) => a.mainTerm[lng] < b.mainTerm[lng] ? -1 : 1)
return new Combine(mappedValues.map(e => e.show))
.SetClass("flex flex-wrap w-full content-start")
.SetClass(options?.searchAreaClass ?? "")
}, [somethingShown, searchValue]))
}, [totalShown, searchValue]))
])
this.selectedElements = selectedElements;
this.someMatchFound = somethingShown;
this.someMatchFound = totalShown.map(t => t > 0);
}

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

@ -11,7 +11,7 @@ import {SaveButton} from "./SaveButton";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import {FixedUiElement} from "../Base/FixedUiElement";
import {Translation, TypedTranslation} from "../i18n/Translation";
import {Translation} from "../i18n/Translation";
import Constants from "../../Models/Constants";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
@ -22,7 +22,7 @@ import BaseUIElement from "../BaseUIElement";
import {DropDown} from "../Input/DropDown";
import InputElementWrapper from "../Input/InputElementWrapper";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import TagRenderingConfig, {Mapping} from "../../Models/ThemeConfig/TagRenderingConfig";
import {Unit} from "../../Models/Unit";
import VariableInputElement from "../Input/VariableInputElement";
import Toggle from "../Input/Toggle";
@ -32,6 +32,7 @@ import Title from "../Base/Title";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {GeoOperations} from "../../Logic/GeoOperations";
import {SearchablePillsSelector} from "../Input/SearchableMappingsSelector";
import {OsmTags} from "../../Models/OsmFeature";
/**
* Shows the question element.
@ -39,7 +40,7 @@ import {SearchablePillsSelector} from "../Input/SearchableMappingsSelector";
*/
export default class TagRenderingQuestion extends Combine {
constructor(tags: UIEventSource<Record<string, string> & {id: string}>,
constructor(tags: UIEventSource<Record<string, string> & { id: string }>,
configuration: TagRenderingConfig,
state?: FeaturePipelineState,
options?: {
@ -53,7 +54,7 @@ export default class TagRenderingQuestion extends Combine {
const applicableMappingsSrc =
Stores.ListStabilized(tags.map(tags => {
const applicableMappings: { if: TagsFilter, icon?: string, then: TypedTranslation<object>, ifnot?: TagsFilter, addExtraTags: Tag[] }[] = []
const applicableMappings: Mapping[] = []
for (const mapping of configuration.mappings ?? []) {
if (mapping.hideInAnswer === true) {
continue
@ -142,7 +143,7 @@ export default class TagRenderingQuestion extends Combine {
private static GenerateInputElement(
state: FeaturePipelineState,
configuration: TagRenderingConfig,
applicableMappings: { if: TagsFilter, then: TypedTranslation<object>, icon?: string, ifnot?: TagsFilter, addExtraTags: Tag[], searchTerms?: Record<string, string[]> }[],
applicableMappings: Mapping[],
applicableUnit: Unit,
tagsSource: UIEventSource<any>,
feedback: UIEventSource<Translation>
@ -231,43 +232,152 @@ export default class TagRenderingQuestion extends Combine {
}
private static GenerateSearchableSelector(
state: FeaturePipelineState,
configuration: TagRenderingConfig,
applicableMappings: { if: TagsFilter; ifnot?: TagsFilter, then: TypedTranslation<object>; icon?: string; iconClass?: string, addExtraTags: Tag[], searchTerms?: Record<string, string[]> }[], tagsSource: UIEventSource<any>): InputElement<TagsFilter> {
const values: { show: BaseUIElement, value: number, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]> }[] = []
private static MappingToPillValue(applicableMappings: Mapping[], tagsSource: UIEventSource<OsmTags>, state: FeaturePipelineState): { show: BaseUIElement, value: number, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]>, original: Mapping }[] {
const values: { show: BaseUIElement, value: number, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]>, original: Mapping }[] = []
const addIcons = applicableMappings.some(m => m.icon !== undefined)
for (let i = 0; i < applicableMappings.length; i++) {
const mapping = applicableMappings[i];
const tr = mapping.then.Subs(tagsSource.data)
const patchedMapping = <{ iconClass: "small-height", then: TypedTranslation<object> }>{
const patchedMapping = <Mapping>{
...mapping,
iconClass: `small-height`,
icon: mapping.icon ?? "./assets/svg/none.svg"
icon: mapping.icon ?? (addIcons ? "./assets/svg/none.svg" : undefined)
}
const fancy = TagRenderingQuestion.GenerateMappingContent(patchedMapping, tagsSource, state).SetClass("normal-background")
values.push({
show: fancy,
value: i,
mainTerm: tr.translations,
searchTerms: mapping.searchTerms
searchTerms: mapping.searchTerms,
original: mapping
})
}
return values
}
const searchValue: UIEventSource<string> = new UIEventSource<string>(undefined)
/**
*
* // Should return the search as freeform value
* const source = new UIEventSource({id: "1234"})
* const tr = new TagRenderingConfig({
* id:"test",
* render:"The value is {key}",
* freeform: {
* key:"key"
* },
*
* mappings: [
* {
* if:"x=y",
* then:"z",
* searchTerms: {
* "en" : ["z"]
* }
* }
* ]
* }, "test");
* const selector = TagRenderingQuestion.GenerateSearchableSelector(
* undefined,
* tr,
* tr.mappings,
* source,
* {
* search: new UIEventSource<string>("value")
* }
* );
* selector.GetValue().data // => new And([new Tag("key","value")])
*
* // Should return the search as freeform value, even if a previous search matched
* const source = new UIEventSource({id: "1234"})
* const search = new UIEventSource<string>("")
* const tr = new TagRenderingConfig({
* id:"test",
* render:"The value is {key}",
* freeform: {
* key:"key"
* },
*
* mappings: [
* {
* if:"x=y",
* then:"z",
* searchTerms: {
* "en" : ["z"]
* }
* }
* ]
* }, "test");
* const selector = TagRenderingQuestion.GenerateSearchableSelector(
* undefined,
* tr,
* tr.mappings,
* source,
* {
* search
* }
* );
* search.setData("z")
* search.setData("zx")
* selector.GetValue().data // => new And([new Tag("key","zx")])
*/
private static GenerateSearchableSelector(
state: FeaturePipelineState,
configuration: TagRenderingConfig,
applicableMappings: Mapping[],
tagsSource: UIEventSource<OsmTags>,
options?: {
search: UIEventSource<string>
}): InputElement<TagsFilter> {
const values = TagRenderingQuestion.MappingToPillValue(applicableMappings, tagsSource, state)
const searchValue: UIEventSource<string> = options?.search ?? new UIEventSource<string>(undefined)
const ff = configuration.freeform
let onEmpty: BaseUIElement = undefined
if (ff !== undefined) {
onEmpty = new VariableUiElement(searchValue.map(search => configuration.render.Subs({[ff.key]: search})))
}
const mode = configuration.multiAnswer ? "select-many" : "select-one";
const tooMuchElementsValue = new UIEventSource<number[]>([]);
let priorityPresets: BaseUIElement = undefined;
const classes = "h-64 overflow-scroll"
if (applicableMappings.some(m => m.priorityIf !== undefined)) {
const priorityValues = tagsSource.map(tags =>
TagRenderingQuestion.MappingToPillValue(applicableMappings, tagsSource, state)
.filter(v => v.original.priorityIf?.matchesProperties(tags)))
priorityPresets = new VariableUiElement(priorityValues.map(priority => {
if (priority.length === 0) {
return Translations.t.general.useSearch;
}
return new Combine([
Translations.t.general.useSearchForMore.Subs({total: applicableMappings.length}),
new SearchablePillsSelector(priority, {
selectedElements: tooMuchElementsValue,
hideSearchBar: true,
mode
})]).SetClass("flex flex-col items-center ").SetClass(classes);
}));
}
const presetSearch = new SearchablePillsSelector<number>(values, {
selectIfSingle: true,
mode: configuration.multiAnswer ? "select-many" : "select-one",
mode,
searchValue,
onNoMatches: onEmpty?.SetClass(classes).SetClass("flex justify-center items-center"),
searchAreaClass: classes
searchAreaClass: classes,
onManyElementsValue: tooMuchElementsValue,
onManyElements: priorityPresets
})
const fallbackTag = searchValue.map(s => {
if (s === undefined || ff?.key === undefined) {
return undefined
}
return new Tag(ff.key, s)
});
return new InputElementMap<number[], And>(presetSearch,
(x0, x1) => {
if (x0 == x1) {
@ -288,7 +398,7 @@ export default class TagRenderingQuestion extends Combine {
},
(selected) => {
if (ff !== undefined && searchValue.data?.length > 0 && !presetSearch.someMatchFound.data) {
const t = new Tag(ff.key, searchValue.data)
const t = fallbackTag.data;
if (ff.addExtraTags) {
return new And([t, ...ff.addExtraTags])
}
@ -436,13 +546,7 @@ export default class TagRenderingQuestion extends Combine {
private static GenerateMappingElement(
state,
tagsSource: UIEventSource<any>,
mapping: {
if: TagsFilter,
then: Translation,
addExtraTags: Tag[],
icon?: string,
iconClass?: "small" | "medium" | "large" | "small-height"
}, ifNot?: TagsFilter[]): InputElement<TagsFilter> {
mapping: Mapping, ifNot?: TagsFilter[]): InputElement<TagsFilter> {
let tagging: TagsFilter = mapping.if;
if (ifNot !== undefined) {
@ -459,11 +563,7 @@ export default class TagRenderingQuestion extends Combine {
(t0, t1) => t1.shadows(t0));
}
private static GenerateMappingContent(mapping: {
then: Translation,
icon?: string,
iconClass?: "small" | "medium" | "large" | "small-height" | "medium-height" | "large-height"
}, tagsSource: UIEventSource<any>, state: FeaturePipelineState): BaseUIElement {
private static GenerateMappingContent(mapping: Mapping, tagsSource: UIEventSource<any>, state: FeaturePipelineState): BaseUIElement {
const text = new SubstitutedTranslation(mapping.then, tagsSource, state)
if (mapping.icon === undefined) {
return text;

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),