forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
15e6fde194
275 changed files with 786405 additions and 39150 deletions
24
UI/Base/ChartJs.ts
Normal file
24
UI/Base/ChartJs.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -33,6 +33,7 @@ export class VariableUiElement extends BaseUIElement {
|
|||
if (self.isDestroyed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
while (el.firstChild) {
|
||||
el.removeChild(el.lastChild);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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,
|
||||
() => {
|
||||
|
|
179
UI/BigComponents/TagRenderingChart.ts
Normal file
179
UI/BigComponents/TagRenderingChart.ts
Normal 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
348
UI/DashboardGui.ts
Normal 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
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue