forked from MapComplete/MapComplete
Merge branch 'develop' into feature/maproulette
This commit is contained in:
commit
64560b9cd2
279 changed files with 13050 additions and 4684 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;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import {Utils} from "../../Utils";
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class Img extends BaseUIElement {
|
||||
private _src: string;
|
||||
|
||||
private readonly _src: string;
|
||||
private readonly _rawSvg: boolean;
|
||||
private _options: { fallbackImage?: string };
|
||||
private readonly _options: { readonly fallbackImage?: string };
|
||||
|
||||
constructor(src: string, rawSvg = false, options?: {
|
||||
fallbackImage?: string
|
||||
|
|
@ -22,7 +23,13 @@ export default class Img extends BaseUIElement {
|
|||
if (Utils.runningFromConsole) {
|
||||
return source;
|
||||
}
|
||||
return `data:image/svg+xml;base64,${(btoa(source))}`;
|
||||
try{
|
||||
return `data:image/svg+xml;base64,${(btoa(source))}`;
|
||||
}catch (e){
|
||||
console.error("Cannot create an image for", source.slice(0, 100))
|
||||
console.trace("Cannot create an image for the given source string due to ", e)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
static AsImageElement(source: string, css_class: string = "", style = ""): string {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {Utils} from "../Utils";
|
||||
import { Utils } from "../Utils";
|
||||
|
||||
/**
|
||||
* A thin wrapper around a html element, which allows to generate a HTML-element.
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -39,9 +39,9 @@ export default abstract class BaseUIElement {
|
|||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ScrollToTop(){
|
||||
this._constructedHtmlElement?.scrollTo(0,0)
|
||||
|
||||
public ScrollToTop() {
|
||||
this._constructedHtmlElement?.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -70,10 +70,13 @@ export default abstract class BaseUIElement {
|
|||
return this;
|
||||
}
|
||||
|
||||
public RemoveClass(clss: string): BaseUIElement {
|
||||
if (this.clss.has(clss)) {
|
||||
this.clss.delete(clss);
|
||||
this._constructedHtmlElement?.classList.remove(clss)
|
||||
public RemoveClass(classes: string): BaseUIElement {
|
||||
const all = classes.split(" ").map(clsName => clsName.trim());
|
||||
for (let clss of all) {
|
||||
if (this.clss.has(clss)) {
|
||||
this.clss.delete(clss);
|
||||
this._constructedHtmlElement?.classList.remove(clss)
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
|
@ -114,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) {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,22 @@ import Toggle from "../Input/Toggle";
|
|||
import MapControlButton from "../MapControlButton";
|
||||
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler";
|
||||
import Svg from "../../Svg";
|
||||
import MapState from "../../Logic/State/MapState";
|
||||
import MapState, {GlobalFilter} from "../../Logic/State/MapState";
|
||||
import LevelSelector from "../Input/LevelSelector";
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
|
||||
import {Utils} from "../../Utils";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import {RegexTag} from "../../Logic/Tags/RegexTag";
|
||||
import {Or} from "../../Logic/Tags/Or";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
import {OsmFeature} from "../../Models/OsmFeature";
|
||||
|
||||
export default class RightControls extends Combine {
|
||||
|
||||
constructor(state: MapState) {
|
||||
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {
|
||||
|
||||
const geolocatioHandler = new GeoLocationHandler(
|
||||
state
|
||||
|
|
@ -38,7 +49,91 @@ export default class RightControls extends Combine {
|
|||
state.locationControl.ping();
|
||||
});
|
||||
|
||||
super([plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1")))
|
||||
const levelsInView = state.currentBounds.map(bbox => {
|
||||
if (bbox === undefined) {
|
||||
return []
|
||||
}
|
||||
const allElementsUnfiltered: OsmFeature[] = [].concat(... state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox).map(ff => ff.features))
|
||||
const allElements = allElementsUnfiltered.filter(f => BBox.get(f).overlapsWith(bbox))
|
||||
const allLevelsRaw: string[] = allElements.map(f => f.properties["level"])
|
||||
const allLevels = [].concat(...allLevelsRaw.map(l => TagUtils.LevelsParser(l)))
|
||||
if (allLevels.indexOf("0") < 0) {
|
||||
allLevels.push("0")
|
||||
}
|
||||
allLevels.sort((a, b) => a < b ? -1 : 1)
|
||||
return Utils.Dedup(allLevels)
|
||||
})
|
||||
state.globalFilters.data.push({
|
||||
filter: {
|
||||
currentFilter: undefined,
|
||||
state: undefined,
|
||||
|
||||
},
|
||||
id: "level",
|
||||
onNewPoint: undefined
|
||||
})
|
||||
const levelSelect = new LevelSelector(levelsInView)
|
||||
|
||||
const isShown = levelsInView.map(levelsInView => {
|
||||
if (levelsInView.length == 0) {
|
||||
return false;
|
||||
}
|
||||
if (state.locationControl.data.zoom <= 16) {
|
||||
return false;
|
||||
}
|
||||
if (levelsInView.length == 1 && levelsInView[0] == "0") {
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[state.locationControl])
|
||||
|
||||
function setLevelFilter() {
|
||||
console.log("Updating levels filter")
|
||||
const filter: GlobalFilter = state.globalFilters.data.find(gf => gf.id === "level")
|
||||
if (!isShown.data) {
|
||||
filter.filter = {
|
||||
state: "*",
|
||||
currentFilter: undefined,
|
||||
}
|
||||
filter.onNewPoint = undefined
|
||||
|
||||
} else {
|
||||
|
||||
const l = levelSelect.GetValue().data
|
||||
let neededLevel: TagsFilter = new RegexTag("level", new RegExp("(^|;)" + l + "(;|$)"));
|
||||
if (l === "0") {
|
||||
neededLevel = new Or([neededLevel, new Tag("level", "")])
|
||||
}
|
||||
filter.filter = {
|
||||
state: l,
|
||||
currentFilter: neededLevel
|
||||
}
|
||||
const t = Translations.t.general.levelSelection
|
||||
filter.onNewPoint = {
|
||||
confirmAddNew: t.confirmLevel.PartialSubs({level: l}),
|
||||
safetyCheck: t.addNewOnLevel.Subs({level: l}),
|
||||
tags: [new Tag("level", l)]
|
||||
}
|
||||
}
|
||||
state.globalFilters.ping();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
isShown.addCallbackAndRun(shown => {
|
||||
console.log("Is level selector shown?", shown)
|
||||
setLevelFilter()
|
||||
if (shown) {
|
||||
levelSelect.RemoveClass("invisible")
|
||||
} else {
|
||||
levelSelect.SetClass("invisible")
|
||||
}
|
||||
})
|
||||
|
||||
levelSelect.GetValue().addCallback(_ => setLevelFilter())
|
||||
|
||||
super([new Combine([levelSelect]).SetClass(""), plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1")))
|
||||
this.SetClass("flex flex-col items-center")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
|
|||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import Loading from "../Base/Loading";
|
||||
import Hash from "../../Logic/Web/Hash";
|
||||
import {GlobalFilter} from "../../Logic/State/MapState";
|
||||
|
||||
/*
|
||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||
|
|
@ -43,6 +44,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>,
|
||||
|
|
@ -58,8 +67,11 @@ export default class SimpleAddUI extends Toggle {
|
|||
locationControl: UIEventSource<Loc>,
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>,
|
||||
featureSwitchFilter: UIEventSource<boolean>,
|
||||
backgroundLayer: UIEventSource<BaseLayer>
|
||||
}) {
|
||||
backgroundLayer: UIEventSource<BaseLayer>,
|
||||
globalFilters: UIEventSource<GlobalFilter[]>
|
||||
},
|
||||
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 +80,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 +89,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 +133,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,
|
||||
() => {
|
||||
|
|
|
|||
51
UI/BigComponents/StatisticsPanel.ts
Normal file
51
UI/BigComponents/StatisticsPanel.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Loading from "../Base/Loading";
|
||||
import Title from "../Base/Title";
|
||||
import TagRenderingChart from "./TagRenderingChart";
|
||||
import Combine from "../Base/Combine";
|
||||
import Locale from "../i18n/Locale";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {OsmFeature} from "../../Models/OsmFeature";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
|
||||
export default class StatisticsPanel extends VariableUiElement {
|
||||
constructor(elementsInview: UIEventSource<{ element: OsmFeature, layer: LayerConfig }[]>, state: {
|
||||
layoutToUse: LayoutConfig
|
||||
}) {
|
||||
super(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.Clone(), 1).SetClass("mt-8"))
|
||||
|
||||
const layerStats = []
|
||||
for (const tagRendering of (layer?.tagRenderings ?? [])) {
|
||||
const chart = new TagRenderingChart(featuresForLayer, tagRendering, {
|
||||
chartclasses: "w-full",
|
||||
chartstyle: "height: 60rem",
|
||||
includeTitle: false
|
||||
})
|
||||
const title = new Title(tagRendering.question?.Clone() ?? tagRendering.id, 4).SetClass("mt-8")
|
||||
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]));
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
242
UI/DashboardGui.ts
Normal file
242
UI/DashboardGui.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
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 {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 Translations from "./i18n/Translations";
|
||||
import Constants from "../Models/Constants";
|
||||
import SimpleAddUI from "./BigComponents/SimpleAddUI";
|
||||
import BackToIndex from "./BigComponents/BackToIndex";
|
||||
import StatisticsPanel from "./BigComponents/StatisticsPanel";
|
||||
|
||||
|
||||
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 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() {
|
||||
const mapCenter = <[number,number]> [self.state.locationControl.data.lon, self.state.locationControl.data.lon]
|
||||
const elements = self.state.featurePipeline.getAllVisibleElementsWithmeta(self.state.currentBounds.data).map(el => {
|
||||
const distance = GeoOperations.distanceBetween(el.center, mapCenter)
|
||||
return {...el, distance }
|
||||
})
|
||||
elements.sort((e0, e1) => e0.distance - e1.distance)
|
||||
elementsInview.setData(elements)
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
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",
|
||||
new StatisticsPanel(elementsInview, this.state), "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);
|
||||
});
|
||||
|
||||
|
|
|
|||
70
UI/Input/LevelSelector.ts
Normal file
70
UI/Input/LevelSelector.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {Store, Stores, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import Slider from "./Slider";
|
||||
import {ClickableToggle} from "./Toggle";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
export default class LevelSelector extends VariableUiElement implements InputElement<string> {
|
||||
|
||||
private readonly _value: UIEventSource<string>;
|
||||
|
||||
constructor(currentLevels: Store<string[]>, options?: {
|
||||
value?: UIEventSource<string>
|
||||
}) {
|
||||
const value = options?.value ?? new UIEventSource<string>(undefined)
|
||||
super(Stores.ListStabilized(currentLevels).map(levels => {
|
||||
console.log("CUrrent levels are", levels)
|
||||
let slider = new Slider(0, levels.length - 1, {vertical: true});
|
||||
const toggleClass = "flex border-2 border-blue-500 w-10 h-10 place-content-center items-center border-box"
|
||||
slider.SetClass("flex elevator w-10").SetStyle(`height: ${2.5 * levels.length}rem; background: #00000000`)
|
||||
|
||||
const values = levels.map((data, i) => new ClickableToggle(
|
||||
new FixedUiElement(data).SetClass("font-bold active bg-subtle " + toggleClass),
|
||||
new FixedUiElement(data).SetClass("normal-background " + toggleClass),
|
||||
slider.GetValue().sync(
|
||||
(sliderVal) => {
|
||||
return sliderVal === i
|
||||
},
|
||||
[],
|
||||
(isSelected) => {
|
||||
return isSelected ? i : slider.GetValue().data
|
||||
}
|
||||
))
|
||||
.ToggleOnClick()
|
||||
.SetClass("flex w-10 h-10"))
|
||||
|
||||
values.reverse(/* This is a new list, no side-effects */)
|
||||
const combine = new Combine([new Combine(values), slider])
|
||||
combine.SetClass("flex flex-row overflow-hidden");
|
||||
|
||||
|
||||
slider.GetValue().addCallbackAndRun(i => {
|
||||
if (currentLevels?.data === undefined) {
|
||||
return
|
||||
}
|
||||
value.setData(currentLevels?.data[i]);
|
||||
})
|
||||
value.addCallback(level => {
|
||||
const i = currentLevels?.data?.findIndex(l => l === level)
|
||||
slider.GetValue().setData(i)
|
||||
})
|
||||
return combine
|
||||
}))
|
||||
|
||||
this._value = value
|
||||
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
IsValid(t: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -4,9 +4,10 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
export default class Slider extends InputElement<number> {
|
||||
|
||||
private readonly _value: UIEventSource<number>
|
||||
private min: number;
|
||||
private max: number;
|
||||
private step: number;
|
||||
private readonly min: number;
|
||||
private readonly max: number;
|
||||
private readonly step: number;
|
||||
private readonly vertical: boolean;
|
||||
|
||||
/**
|
||||
* Constructs a slider input element for natural numbers
|
||||
|
|
@ -16,13 +17,15 @@ export default class Slider extends InputElement<number> {
|
|||
*/
|
||||
constructor(min: number, max: number, options?: {
|
||||
value?: UIEventSource<number>,
|
||||
step?: 1 | number
|
||||
step?: 1 | number,
|
||||
vertical?: false | boolean
|
||||
}) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.min = min;
|
||||
this._value = options?.value ?? new UIEventSource<number>(min)
|
||||
this.step = options?.step ?? 1;
|
||||
this.vertical = options?.vertical ?? false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<number> {
|
||||
|
|
@ -39,6 +42,10 @@ export default class Slider extends InputElement<number> {
|
|||
el.oninput = () => {
|
||||
valuestore.setData(Number(el.value))
|
||||
}
|
||||
if(this.vertical){
|
||||
el.classList.add("vertical")
|
||||
el.setAttribute('orient','vertical'); // firefox only workaround...
|
||||
}
|
||||
valuestore.addCallbackAndRunD(v => el.value = ""+valuestore.data)
|
||||
return el;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export default class LanguagePicker extends Toggle {
|
|||
|
||||
|
||||
if (languages === undefined || languages.length <= 1) {
|
||||
super(undefined,undefined,undefined)
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,16 @@ import SimpleAddUI, {PresetInfo} from "../BigComponents/SimpleAddUI";
|
|||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import Img from "../Base/Img";
|
||||
import Title from "../Base/Title";
|
||||
import {GlobalFilter} from "../../Logic/State/MapState";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
|
||||
export default class ConfirmLocationOfPoint extends Combine {
|
||||
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
globalFilters: UIEventSource<GlobalFilter[]>;
|
||||
featureSwitchIsTesting: UIEventSource<boolean>;
|
||||
osmConnection: OsmConnection,
|
||||
featurePipeline: FeaturePipeline,
|
||||
|
|
@ -38,8 +42,8 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
let preciseInput: LocationInput = undefined
|
||||
if (preset.preciseInput !== undefined) {
|
||||
// Create location input
|
||||
|
||||
|
||||
|
||||
|
||||
// We uncouple the event source
|
||||
const zloc = {...loc, zoom: 19}
|
||||
const locationSrc = new UIEventSource(zloc);
|
||||
|
|
@ -106,7 +110,11 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
).SetClass("font-bold break-words")
|
||||
.onClick(() => {
|
||||
console.log("The confirmLocationPanel - precise input yielded ", preciseInput?.GetValue()?.data)
|
||||
confirm(preset.tags, preciseInput?.GetValue()?.data ?? loc, preciseInput?.snappedOnto?.data?.properties?.id);
|
||||
const globalFilterTagsToAdd: Tag[][] = state.globalFilters.data.filter(gf => gf.onNewPoint !== undefined)
|
||||
.map(gf => gf.onNewPoint.tags)
|
||||
const globalTags : Tag[] = [].concat(...globalFilterTagsToAdd)
|
||||
console.log("Global tags to add are: ", globalTags)
|
||||
confirm([...preset.tags, ...globalTags], preciseInput?.GetValue()?.data ?? loc, preciseInput?.snappedOnto?.data?.properties?.id);
|
||||
});
|
||||
|
||||
if (preciseInput !== undefined) {
|
||||
|
|
@ -126,7 +134,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
.onClick(() => filterViewIsOpened.setData(true))
|
||||
|
||||
|
||||
const openLayerOrConfirm = new Toggle(
|
||||
let openLayerOrConfirm = new Toggle(
|
||||
confirmButton,
|
||||
openLayerControl,
|
||||
preset.layerToAddTo.isDisplayed
|
||||
|
|
@ -152,6 +160,29 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
closePopup()
|
||||
})
|
||||
|
||||
|
||||
// We assume the number of global filters won't change during the run of the program
|
||||
for (let i = 0; i < state.globalFilters.data.length; i++) {
|
||||
const hasBeenCheckedOf = new UIEventSource(false);
|
||||
|
||||
const filterConfirmPanel = new VariableUiElement(
|
||||
state.globalFilters.map(gfs => {
|
||||
const gf = gfs[i]
|
||||
const confirm = gf.onNewPoint?.confirmAddNew?.Subs({preset: preset.title})
|
||||
return new Combine([
|
||||
gf.onNewPoint?.safetyCheck,
|
||||
new SubtleButton(Svg.confirm_svg(), confirm).onClick(() => hasBeenCheckedOf.setData(true))
|
||||
])
|
||||
}
|
||||
))
|
||||
|
||||
|
||||
openLayerOrConfirm = new Toggle(
|
||||
openLayerOrConfirm, filterConfirmPanel,
|
||||
state.globalFilters.map(f => hasBeenCheckedOf.data || f[i]?.onNewPoint === undefined, [hasBeenCheckedOf])
|
||||
)
|
||||
}
|
||||
|
||||
const hasActiveFilter = preset.layerToAddTo.appliedFilters
|
||||
.map(appliedFilters => {
|
||||
const activeFilters = Array.from(appliedFilters.values()).filter(f => f?.currentFilter !== undefined);
|
||||
|
|
@ -171,16 +202,16 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
Translations.t.general.cancel
|
||||
).onClick(cancel)
|
||||
|
||||
|
||||
let examples : BaseUIElement = undefined;
|
||||
if(preset.exampleImages !== undefined && preset.exampleImages.length > 0){
|
||||
|
||||
let examples: BaseUIElement = undefined;
|
||||
if (preset.exampleImages !== undefined && preset.exampleImages.length > 0) {
|
||||
examples = new Combine([
|
||||
new Title( preset.exampleImages.length == 1 ? Translations.t.general.example : Translations.t.general.examples),
|
||||
new Title(preset.exampleImages.length == 1 ? Translations.t.general.example : Translations.t.general.examples),
|
||||
new Combine(preset.exampleImages.map(img => new Img(img).SetClass("h-64 m-1 w-auto rounded-lg"))).SetClass("flex flex-wrap items-stretch")
|
||||
])
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
super([
|
||||
new Toggle(
|
||||
Translations.t.general.testing.SetClass("alert"),
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ import {MapillaryLink} from "./BigComponents/MapillaryLink";
|
|||
import {CheckBox} from "./Input/Checkboxes";
|
||||
import Slider from "./Input/Slider";
|
||||
import List from "./Base/List";
|
||||
import StatisticsPanel from "./BigComponents/StatisticsPanel";
|
||||
import { OsmFeature } from "../Models/OsmFeature";
|
||||
|
||||
export interface SpecialVisualization {
|
||||
funcName: string,
|
||||
|
|
@ -208,7 +210,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:
|
||||
|
|
@ -286,7 +288,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),
|
||||
|
|
@ -749,8 +757,8 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
{
|
||||
funcName: "canonical",
|
||||
docs: "Converts a short, canonical value into the long, translated text",
|
||||
example: "{canonical(length)} will give 42 metre (in french)",
|
||||
docs: "Converts a short, canonical value into the long, translated text including the unit. This only works if a `unit` is defined for the corresponding value. The unit specification will be included in the text. ",
|
||||
example: "If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...",
|
||||
args: [{
|
||||
name: "key",
|
||||
doc: "The key of the tag to give the canonical text for",
|
||||
|
|
@ -1125,6 +1133,35 @@ export default class SpecialVisualizations {
|
|||
return details;
|
||||
},
|
||||
docs: "Show details of a MapRoulette task"
|
||||
},
|
||||
{
|
||||
funcName: "statistics",
|
||||
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
|
||||
args: [],
|
||||
constr : (state, tagsSource, args, guiState) => {
|
||||
const elementsInview = new UIEventSource<{ distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[]>([]);
|
||||
function update() {
|
||||
const mapCenter = <[number,number]> [state.locationControl.data.lon, state.locationControl.data.lon]
|
||||
const bbox = state.currentBounds.data
|
||||
const elements = state.featurePipeline.getAllVisibleElementsWithmeta(bbox).map(el => {
|
||||
const distance = GeoOperations.distanceBetween(el.center, mapCenter)
|
||||
return {...el, distance }
|
||||
})
|
||||
elements.sort((e0, e1) => e0.distance - e1.distance)
|
||||
elementsInview.setData(elements)
|
||||
|
||||
}
|
||||
|
||||
state.currentBounds.addCallbackAndRun(update)
|
||||
state.featurePipeline.newDataLoadedSignal.addCallback(update);
|
||||
state.filteredLayers.addCallbackAndRun(fls => {
|
||||
for (const fl of fls) {
|
||||
fl.isDisplayed.addCallback(update)
|
||||
fl.appliedFilters.addCallback(update)
|
||||
}
|
||||
})
|
||||
return new StatisticsPanel(elementsInview, state)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -317,6 +317,19 @@ export class TypedTranslation<T> extends Translation {
|
|||
return Utils.SubstituteKeys(template, text, lang);
|
||||
}, context)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
PartialSubs<X extends string>(text: Partial<T> & Record<X, string>): TypedTranslation<Omit<T, X>> {
|
||||
const newTranslations : Record<string, string> = {}
|
||||
for (const lang in this.translations) {
|
||||
const template = this.translations[lang]
|
||||
if(lang === "_context"){
|
||||
newTranslations[lang] = template
|
||||
continue
|
||||
}
|
||||
newTranslations[lang] = Utils.SubstituteKeys(template, text, lang)
|
||||
}
|
||||
|
||||
return new TypedTranslation<Omit<T, X>>(newTranslations, this.context)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue