forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
f460895f60
429 changed files with 805040 additions and 44810 deletions
|
@ -5,7 +5,7 @@ import MoreScreen from "./BigComponents/MoreScreen";
|
|||
import Translations from "./i18n/Translations";
|
||||
import Constants from "../Models/Constants";
|
||||
import {Utils} from "../Utils";
|
||||
import LanguagePicker from "./LanguagePicker";
|
||||
import LanguagePicker1 from "./LanguagePicker";
|
||||
import IndexText from "./BigComponents/IndexText";
|
||||
import FeaturedMessage from "./BigComponents/FeaturedMessage";
|
||||
import Toggle from "./Input/Toggle";
|
||||
|
@ -21,7 +21,7 @@ export default class AllThemesGui {
|
|||
const state = new UserRelatedState(undefined);
|
||||
const intro = new Combine([
|
||||
|
||||
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
|
||||
new LanguagePicker1(Translations.t.index.title.SupportedLanguages(), "")
|
||||
|
||||
.SetClass("flex absolute top-2 right-3"),
|
||||
new IndexText()
|
||||
|
|
|
@ -28,7 +28,10 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
constructor(title: ((options: { mode: string }) => BaseUIElement),
|
||||
content: ((options: { mode: string, resetScrollSignal: UIEventSource<void> }) => BaseUIElement),
|
||||
hashToShow: string,
|
||||
isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false),
|
||||
options?: {
|
||||
setHash?: true | boolean
|
||||
}
|
||||
) {
|
||||
super();
|
||||
this.hashToShow = hashToShow;
|
||||
|
@ -53,16 +56,21 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
|
||||
|
||||
const self = this;
|
||||
Hash.hash.addCallback(h => {
|
||||
if (h === undefined) {
|
||||
isShown.setData(false)
|
||||
}
|
||||
})
|
||||
const setHash = options?.setHash ?? true;
|
||||
if(setHash){
|
||||
Hash.hash.addCallback(h => {
|
||||
if (h === undefined) {
|
||||
isShown.setData(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
isShown.addCallback(isShown => {
|
||||
if (isShown) {
|
||||
// We first must set the hash, then activate the panel
|
||||
// If the order is wrong, this will cause the panel to disactivate again
|
||||
Hash.hash.setData(hashToShow)
|
||||
if(setHash){
|
||||
Hash.hash.setData(hashToShow)
|
||||
}
|
||||
self.Activate();
|
||||
} else {
|
||||
// Some cleanup...
|
||||
|
|
|
@ -159,6 +159,12 @@ class SingleLayerSelectionButton extends Toggle {
|
|||
|
||||
export default class BackgroundMapSwitch extends Combine {
|
||||
|
||||
/**
|
||||
* Three buttons to easily switch map layers between OSM, aerial and some map.
|
||||
* @param state
|
||||
* @param currentBackground
|
||||
* @param options
|
||||
*/
|
||||
constructor(
|
||||
state: {
|
||||
locationControl: UIEventSource<Loc>,
|
||||
|
|
|
@ -48,7 +48,10 @@ export default class LeftControls extends Combine {
|
|||
}
|
||||
return new Lazy(() => {
|
||||
const tagsSource = state.allElements.getEventSourceById(feature.properties.id)
|
||||
return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, state, "currentview", guiState.currentViewControlIsOpened)
|
||||
return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, state, {
|
||||
hashToShow: "currentview",
|
||||
isShown: guiState.currentViewControlIsOpened
|
||||
})
|
||||
.SetClass("md:floating-element-width")
|
||||
})
|
||||
})).SetStyle("width: 40rem").SetClass("block")
|
||||
|
|
|
@ -9,7 +9,6 @@ import Svg from "../../Svg";
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs";
|
||||
import Title from "../Base/Title";
|
||||
|
||||
export default class ThemeIntroductionPanel extends Combine {
|
||||
|
||||
|
@ -17,7 +16,7 @@ export default class ThemeIntroductionPanel extends Combine {
|
|||
const t = Translations.t.general
|
||||
const layout = state.layoutToUse
|
||||
|
||||
const languagePicker = LanguagePicker.CreateLanguagePicker(layout.language, t.pickLanguage.Clone())
|
||||
const languagePicker = new LanguagePicker(layout.language, t.pickLanguage.Clone())
|
||||
|
||||
const toTheMap = new SubtleButton(
|
||||
undefined,
|
||||
|
|
|
@ -39,7 +39,7 @@ export default class UserBadge extends LoginToggle {
|
|||
});
|
||||
|
||||
const linkStyle = "flex items-baseline"
|
||||
const languagePicker = (LanguagePicker.CreateLanguagePicker(state.layoutToUse.language) ?? new FixedUiElement(""))
|
||||
const languagePicker = (new LanguagePicker(state.layoutToUse.language, "") ?? new FixedUiElement(""))
|
||||
.SetStyle("width:min-content;");
|
||||
|
||||
let messageSpan =
|
||||
|
|
|
@ -93,7 +93,7 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
|
|||
state,
|
||||
zoomToFeatures: true,
|
||||
leafletMap: comparisonMap.leafletMap,
|
||||
features: new StaticFeatureSource(partitionedImportPoints.map(p => p.hasNearby)),
|
||||
features: StaticFeatureSource.fromGeojsonStore(partitionedImportPoints.map(p => p.hasNearby)),
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state)
|
||||
})
|
||||
|
||||
|
|
|
@ -22,12 +22,17 @@ import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
|||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
|
||||
import * as currentview from "../../assets/layers/current_view/current_view.json"
|
||||
import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json"
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
||||
import {ImportUtils} from "./ImportUtils";
|
||||
import Translations from "../i18n/Translations";
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
|
||||
import {Feature, FeatureCollection} from "@turf/turf";
|
||||
import * as currentview from "../../assets/layers/current_view/current_view.json"
|
||||
import {CheckBox} from "../Input/Checkboxes";
|
||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch";
|
||||
|
||||
/**
|
||||
* Given the data to import, the bbox and the layer, will query overpass for similar items
|
||||
|
@ -36,20 +41,21 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
|
||||
public readonly IsValid
|
||||
public readonly Value: Store<{ features: any[], theme: string }>
|
||||
|
||||
|
||||
constructor(
|
||||
state,
|
||||
params: { bbox: BBox, layer: LayerConfig, theme: string, features: any[] }) {
|
||||
|
||||
const t = Translations.t.importHelper.conflationChecker
|
||||
|
||||
const bbox = params.bbox.padAbsolute(0.0001)
|
||||
const layer = params.layer;
|
||||
const toImport: {features: any[]} = params;
|
||||
|
||||
const toImport: { features: any[] } = params;
|
||||
let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached">("idle")
|
||||
const cacheAge = new UIEventSource<number>(undefined);
|
||||
|
||||
|
||||
function loadDataFromOverpass(){
|
||||
|
||||
|
||||
function loadDataFromOverpass() {
|
||||
// Load the data!
|
||||
const url = Constants.defaultOverpassUrls[1]
|
||||
const relationTracker = new RelationsTracker()
|
||||
|
@ -66,42 +72,51 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
overpassStatus.setData({error})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, {
|
||||
|
||||
|
||||
whenLoaded: (v) => {
|
||||
if (v !== undefined && v !== null) {
|
||||
console.log("Loaded from local storage:", v)
|
||||
const [geojson, date] = v;
|
||||
const timeDiff = (new Date().getTime() - date.getTime()) / 1000;
|
||||
console.log("Loaded ", geojson.features.length, " features; cache is ", timeDiff, "seconds old")
|
||||
cacheAge.setData(timeDiff)
|
||||
if (timeDiff < 24 * 60 * 60) {
|
||||
// Recently cached!
|
||||
overpassStatus.setData("cached")
|
||||
return;
|
||||
}
|
||||
cacheAge.setData(-1)
|
||||
overpassStatus.setData("cached")
|
||||
}else{
|
||||
loadDataFromOverpass()
|
||||
}
|
||||
loadDataFromOverpass()
|
||||
}
|
||||
});
|
||||
|
||||
const cacheAge = fromLocalStorage.map(d => {
|
||||
if(d === undefined || d[1] === undefined){
|
||||
return undefined
|
||||
}
|
||||
const [_, loadedDate] = d
|
||||
return (new Date().getTime() - loadedDate.getTime()) / 1000;
|
||||
})
|
||||
cacheAge.addCallbackD(timeDiff => {
|
||||
if (timeDiff < 24 * 60 * 60) {
|
||||
// Recently cached!
|
||||
overpassStatus.setData("cached")
|
||||
return;
|
||||
} else {
|
||||
loadDataFromOverpass()
|
||||
}
|
||||
})
|
||||
|
||||
const geojson: Store<any> = fromLocalStorage.map(d => {
|
||||
const geojson: Store<FeatureCollection> = fromLocalStorage.map(d => {
|
||||
if (d === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return d[0]
|
||||
})
|
||||
|
||||
|
||||
const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
|
||||
const location = new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
|
||||
const currentBounds = new UIEventSource<BBox>(undefined)
|
||||
const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement()
|
||||
const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement({
|
||||
value: LocalStorageSource.GetParsed<string>("importer-zoom-level", "0")
|
||||
})
|
||||
zoomLevel.SetClass("ml-1 border border-black")
|
||||
zoomLevel.GetValue().syncWith(LocalStorageSource.Get("importer-zoom-level", "14"), true)
|
||||
const osmLiveData = Minimap.createMiniMap({
|
||||
allowMoving: true,
|
||||
location,
|
||||
|
@ -110,18 +125,24 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
attribution: new Attribution(location, state.osmConnection.userDetails, undefined, currentBounds)
|
||||
})
|
||||
osmLiveData.SetClass("w-full").SetStyle("height: 500px")
|
||||
const preview = new StaticFeatureSource(geojson.map(geojson => {
|
||||
|
||||
const geojsonFeatures : Store<Feature[]> = geojson.map(geojson => {
|
||||
if (geojson?.features === undefined) {
|
||||
return []
|
||||
}
|
||||
const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(zoomLevel.GetValue().data)
|
||||
if (!zoomedEnough) {
|
||||
const currentZoom = zoomLevel.GetValue().data
|
||||
const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(currentZoom)
|
||||
if (currentZoom !== undefined && !zoomedEnough) {
|
||||
return []
|
||||
}
|
||||
const bounds = osmLiveData.bounds.data
|
||||
if(bounds === undefined){
|
||||
return geojson.features;
|
||||
}
|
||||
return geojson.features.filter(f => BBox.get(f).overlapsWith(bounds))
|
||||
}, [osmLiveData.bounds, zoomLevel.GetValue()]));
|
||||
|
||||
}, [osmLiveData.bounds, zoomLevel.GetValue()])
|
||||
|
||||
const preview = StaticFeatureSource.fromGeojsonStore(geojsonFeatures)
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: new LayerConfig(currentview),
|
||||
|
@ -134,12 +155,16 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
])
|
||||
})
|
||||
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: layer,
|
||||
new ShowDataMultiLayer({
|
||||
//layerToShow: layer,
|
||||
layers: new UIEventSource<FilteredLayer[]>([{
|
||||
layerDef: layer,
|
||||
isDisplayed: new UIEventSource<boolean>(true),
|
||||
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined)
|
||||
}]),
|
||||
state,
|
||||
leafletMap: osmLiveData.leafletMap,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state),
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}),
|
||||
zoomToFeatures: false,
|
||||
features: preview
|
||||
})
|
||||
|
@ -148,7 +173,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
layerToShow: new LayerConfig(import_candidate),
|
||||
state,
|
||||
leafletMap: osmLiveData.leafletMap,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state),
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}),
|
||||
zoomToFeatures: false,
|
||||
features: StaticFeatureSource.fromGeojson(toImport.features)
|
||||
})
|
||||
|
@ -164,7 +189,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
matchedFeaturesMap.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
// Featuresource showing OSM-features which are nearby a toImport-feature
|
||||
const nearbyFeatures = new StaticFeatureSource(geojson.map(osmData => {
|
||||
const geojsonMapped: Store<Feature[]> = geojson.map(osmData => {
|
||||
if (osmData?.features === undefined) {
|
||||
return []
|
||||
}
|
||||
|
@ -172,32 +197,37 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
return osmData.features.filter(f =>
|
||||
toImport.features.some(imp =>
|
||||
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))))
|
||||
}, [nearbyCutoff.GetValue().stabilized(500)]));
|
||||
}, [nearbyCutoff.GetValue().stabilized(500)])
|
||||
const nearbyFeatures = StaticFeatureSource.fromGeojsonStore(geojsonMapped);
|
||||
const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number));
|
||||
|
||||
// Featuresource showing OSM-features which are nearby a toImport-feature
|
||||
const toImportWithNearby = new StaticFeatureSource(paritionedImport.map(els => els?.hasNearby ?? []));
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: layer,
|
||||
state,
|
||||
leafletMap: matchedFeaturesMap.leafletMap,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state),
|
||||
zoomToFeatures: true,
|
||||
features: nearbyFeatures
|
||||
})
|
||||
const toImportWithNearby = StaticFeatureSource.fromGeojsonStore(paritionedImport.map(els => els?.hasNearby ?? []));
|
||||
toImportWithNearby.features.addCallback(nearby => console.log("The following features are near an already existing object:", nearby))
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: new LayerConfig(import_candidate),
|
||||
state,
|
||||
leafletMap: matchedFeaturesMap.leafletMap,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state),
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}),
|
||||
zoomToFeatures: false,
|
||||
features: toImportWithNearby
|
||||
})
|
||||
const showOsmLayer = new CheckBox(t.showOsmLayerInConflationMap, true)
|
||||
new ShowDataLayer({
|
||||
layerToShow: layer,
|
||||
state,
|
||||
leafletMap: matchedFeaturesMap.leafletMap,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}),
|
||||
zoomToFeatures: true,
|
||||
features: nearbyFeatures,
|
||||
doShowLayer: showOsmLayer.GetValue()
|
||||
})
|
||||
|
||||
const t = Translations.t.importHelper.conflationChecker
|
||||
|
||||
|
||||
|
||||
|
||||
const conflationMaps = new Combine([
|
||||
new VariableUiElement(
|
||||
geojson.map(geojson => {
|
||||
|
@ -218,34 +248,44 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
return t.cacheExpired
|
||||
}
|
||||
return new Combine([t.loadedDataAge.Subs({age: Utils.toHumanTime(age)}),
|
||||
new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache)
|
||||
.onClick(loadDataFromOverpass)
|
||||
.SetClass("h-12")
|
||||
new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache)
|
||||
.onClick(loadDataFromOverpass)
|
||||
.SetClass("h-12")
|
||||
])
|
||||
})),
|
||||
|
||||
new Title(t.titleLive),
|
||||
t.importCandidatesCount.Subs({count:toImport.features.length }),
|
||||
new VariableUiElement(geojson.map(geojson => {
|
||||
if(geojson?.features?.length === undefined || geojson?.features?.length === 0){
|
||||
t.importCandidatesCount.Subs({count: toImport.features.length}),
|
||||
new VariableUiElement(geojson.map(geojson => {
|
||||
if (geojson?.features?.length === undefined || geojson?.features?.length === 0) {
|
||||
return t.nothingLoaded.Subs(layer).SetClass("alert")
|
||||
}
|
||||
return new Combine([
|
||||
}
|
||||
return new Combine([
|
||||
t.osmLoaded.Subs({count: geojson.features.length, name: layer.name}),
|
||||
|
||||
])
|
||||
})),
|
||||
|
||||
])
|
||||
})),
|
||||
osmLiveData,
|
||||
new VariableUiElement(osmLiveData.location.map(location => {
|
||||
return t.zoomIn.Subs({needed:zoomLevel, current: location.zoom })
|
||||
} )),
|
||||
new Combine([
|
||||
t.zoomLevelSelection,
|
||||
zoomLevel,
|
||||
new VariableUiElement(osmLiveData.location.map(location => {
|
||||
return t.zoomIn.Subs(<any>{current: location.zoom})
|
||||
})),
|
||||
]).SetClass("flex"),
|
||||
new Title(t.titleNearby),
|
||||
new Combine([t.mapShowingNearbyIntro, nearbyCutoff]).SetClass("flex"),
|
||||
new VariableUiElement(toImportWithNearby.features.map(feats =>
|
||||
new VariableUiElement(toImportWithNearby.features.map(feats =>
|
||||
t.nearbyWarn.Subs({count: feats.length}).SetClass("alert"))),
|
||||
t.setRangeToZero,
|
||||
matchedFeaturesMap]).SetClass("flex flex-col")
|
||||
|
||||
matchedFeaturesMap,
|
||||
new Combine([
|
||||
new BackgroundMapSwitch({backgroundLayer: background, locationControl: matchedFeaturesMap.location}, background),
|
||||
showOsmLayer,
|
||||
|
||||
]).SetClass("flex")
|
||||
|
||||
]).SetClass("flex flex-col")
|
||||
super([
|
||||
new Title(t.title),
|
||||
new VariableUiElement(overpassStatus.map(d => {
|
||||
|
@ -270,7 +310,11 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
|
||||
])
|
||||
|
||||
this.Value = paritionedImport.map(feats => ({theme: params.theme, features: feats?.noNearby, layer: params.layer}))
|
||||
this.Value = paritionedImport.map(feats => ({
|
||||
theme: params.theme,
|
||||
features: feats?.noNearby,
|
||||
layer: params.layer
|
||||
}))
|
||||
this.IsValid = this.Value.map(v => v?.features !== undefined && v.features.length > 0)
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ export default class ImportHelperGui extends LeftIndex {
|
|||
}),
|
||||
toc,
|
||||
new Toggle(t.testMode.SetClass("block alert"), undefined, state.featureSwitchIsTesting),
|
||||
LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"),
|
||||
new LanguagePicker(Translations.t.importHelper.title.SupportedLanguages(), "")?.SetClass("mt-4 self-end flex-col"),
|
||||
].map(el => el?.SetClass("pl-4"))
|
||||
|
||||
super(
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import {Store} from "../../Logic/UIEventSource";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {Feature, Geometry} from "@turf/turf";
|
||||
|
||||
export class ImportUtils {
|
||||
public static partitionFeaturesIfNearby(toPartitionFeatureCollection: ({ features: any[] }), compareWith: Store<{ features: any[] }>, cutoffDistanceInMeters: Store<number>): Store<{ hasNearby: any[], noNearby: any[] }> {
|
||||
public static partitionFeaturesIfNearby(
|
||||
toPartitionFeatureCollection: ({ features: Feature<Geometry>[] }),
|
||||
compareWith: Store<{ features: Feature[] }>,
|
||||
cutoffDistanceInMeters: Store<number>)
|
||||
: Store<{ hasNearby: Feature[], noNearby: Feature[] }> {
|
||||
return compareWith.map(osmData => {
|
||||
if (osmData?.features === undefined) {
|
||||
return undefined
|
||||
|
@ -16,7 +21,7 @@ export class ImportUtils {
|
|||
const noNearby = []
|
||||
for (const toImportElement of toPartitionFeatureCollection.features) {
|
||||
const hasNearbyFeature = osmData.features.some(f =>
|
||||
maxDist >= GeoOperations.distanceBetween(toImportElement.geometry.coordinates, GeoOperations.centerpointCoordinates(f)))
|
||||
maxDist >= GeoOperations.distanceBetween(<any> toImportElement.geometry.coordinates, GeoOperations.centerpointCoordinates(f)))
|
||||
if (hasNearbyFeature) {
|
||||
hasNearby.push(toImportElement)
|
||||
} else {
|
||||
|
|
|
@ -24,6 +24,7 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
|||
import Title from "../Base/Title";
|
||||
import CheckBoxes from "../Input/Checkboxes";
|
||||
import {AllTagsPanel} from "../AllTagsPanel";
|
||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch";
|
||||
|
||||
class PreviewPanel extends ScrollableFullScreen {
|
||||
|
||||
|
@ -109,6 +110,10 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer:
|
|||
bounds: currentBounds,
|
||||
attribution: new Attribution(location, state.osmConnection.userDetails, undefined, currentBounds)
|
||||
})
|
||||
const layerControl = new BackgroundMapSwitch( {
|
||||
backgroundLayer: background,
|
||||
locationControl: location
|
||||
},background)
|
||||
map.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
new ShowDataMultiLayer({
|
||||
|
@ -147,6 +152,7 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer:
|
|||
|
||||
mismatchIndicator,
|
||||
map,
|
||||
layerControl,
|
||||
confirm
|
||||
]);
|
||||
|
||||
|
|
|
@ -4,13 +4,13 @@ import {Utils} from "../../Utils";
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import InputElementMap from "./InputElementMap";
|
||||
|
||||
export class CheckBox extends InputElementMap<number[], boolean> {
|
||||
export class CheckBox extends InputElementMap<number[], boolean> {
|
||||
constructor(el: BaseUIElement , defaultValue?: boolean) {
|
||||
super(
|
||||
new CheckBoxes([el]),
|
||||
(x0, x1) => x0 === x1,
|
||||
t => t.length > 0,
|
||||
x => x ? [0] : []
|
||||
x => x ? [0] : [],
|
||||
);
|
||||
if(defaultValue !== undefined){
|
||||
this.GetValue().setData(defaultValue)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
|
||||
export default class InputElementMap<T, X> extends InputElement<X> {
|
||||
|
@ -13,7 +13,7 @@ export default class InputElementMap<T, X> extends InputElement<X> {
|
|||
isSame: (x0: X, x1: X) => boolean,
|
||||
toX: (t: T) => X,
|
||||
fromX: (x: X) => T,
|
||||
extraSources: UIEventSource<any>[] = []
|
||||
extraSources: Store<any>[] = []
|
||||
) {
|
||||
super();
|
||||
this.isSame = isSame;
|
||||
|
|
283
UI/Input/SearchableMappingsSelector.ts
Normal file
283
UI/Input/SearchableMappingsSelector.ts
Normal file
|
@ -0,0 +1,283 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {InputElement} from "./InputElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Locale from "../i18n/Locale";
|
||||
import Combine from "../Base/Combine";
|
||||
import {TextField} from "./TextField";
|
||||
import Svg from "../../Svg";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
|
||||
/**
|
||||
* A single 'pill' which can hide itself if the search criteria is not met
|
||||
*/
|
||||
class SelfHidingToggle extends UIElement implements InputElement<boolean> {
|
||||
private readonly _shown: BaseUIElement;
|
||||
public readonly _selected: UIEventSource<boolean>
|
||||
public readonly isShown: Store<boolean> = new UIEventSource<boolean>(true);
|
||||
public readonly forceSelected: UIEventSource<boolean>
|
||||
private readonly _squared: boolean;
|
||||
public constructor(
|
||||
shown: string | BaseUIElement,
|
||||
mainTerm: Record<string, string>,
|
||||
search: Store<string>,
|
||||
options?: {
|
||||
searchTerms?: Record<string, string[]>,
|
||||
selected?: UIEventSource<boolean>,
|
||||
forceSelected?: UIEventSource<boolean>,
|
||||
squared?: boolean
|
||||
}
|
||||
) {
|
||||
super();
|
||||
this._shown = Translations.W(shown);
|
||||
this._squared = options?.squared ?? false;
|
||||
const searchTerms: Record<string, string[]> = {};
|
||||
for (const lng in options?.searchTerms ?? []) {
|
||||
if (lng === "_context") {
|
||||
continue
|
||||
}
|
||||
searchTerms[lng] = options?.searchTerms[lng]?.map(SelfHidingToggle.clean)
|
||||
}
|
||||
for (const lng in mainTerm) {
|
||||
if (lng === "_context") {
|
||||
continue
|
||||
}
|
||||
const main = SelfHidingToggle.clean( mainTerm[lng])
|
||||
searchTerms[lng] = [main].concat(searchTerms[lng] ?? [])
|
||||
}
|
||||
const selected = this._selected = options?.selected ?? new UIEventSource<boolean>(false);
|
||||
const forceSelected = this.forceSelected = options?.forceSelected ?? new UIEventSource<boolean>(false)
|
||||
this.isShown = search.map(s => {
|
||||
if (s === undefined || s.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (selected.data && !forceSelected.data) {
|
||||
return true
|
||||
}
|
||||
s = s?.trim()?.toLowerCase()
|
||||
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;
|
||||
this.isShown.addCallbackAndRun(shown => {
|
||||
if (shown) {
|
||||
self.RemoveClass("hidden")
|
||||
} else {
|
||||
self.SetClass("hidden")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private static clean(s: string) : string{
|
||||
return s?.trim()?.toLowerCase()?.replace(/[-]/, "")
|
||||
}
|
||||
|
||||
|
||||
GetValue(): UIEventSource<boolean> {
|
||||
return this._selected
|
||||
}
|
||||
|
||||
IsValid(t: boolean): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected InnerRender(): string | BaseUIElement {
|
||||
let el: BaseUIElement = this._shown;
|
||||
const selected = this._selected;
|
||||
|
||||
selected.addCallbackAndRun(selected => {
|
||||
if (selected) {
|
||||
el.SetClass("border-4")
|
||||
el.RemoveClass("border")
|
||||
el.SetStyle("margin: 0")
|
||||
} else {
|
||||
el.SetStyle("margin: 3px")
|
||||
el.SetClass("border")
|
||||
el.RemoveClass("border-4")
|
||||
}
|
||||
})
|
||||
|
||||
const forcedSelection = this.forceSelected
|
||||
el.onClick(() => {
|
||||
if(forcedSelection.data){
|
||||
selected.setData(true)
|
||||
}else{
|
||||
selected.setData(!selected.data);
|
||||
}
|
||||
})
|
||||
|
||||
if(!this._squared){
|
||||
el.SetClass("rounded-full")
|
||||
}
|
||||
return el.SetClass("border border-black p-1 px-4")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The searchable mappings selector is a selector which shows various pills from which one (or more) options can be chosen.
|
||||
* A searchfield can be used to filter the values
|
||||
*/
|
||||
export class SearchablePillsSelector<T> extends Combine implements InputElement<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?: {
|
||||
mode?: "select-one" | "select-many",
|
||||
selectedElements?: UIEventSource<T[]>,
|
||||
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,
|
||||
hideSearchBar?: false | boolean
|
||||
}) {
|
||||
|
||||
const search = new TextField({value: options?.searchValue})
|
||||
|
||||
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())
|
||||
const selectedElements = options?.selectedElements ?? new UIEventSource<T[]>([]);
|
||||
const mode = options?.mode ?? "select-one";
|
||||
const onEmpty = options?.onNoMatches ?? Translations.t.general.noMatchingMapping
|
||||
|
||||
const mappedValues: { show: SelfHidingToggle, mainTerm: Record<string, string>, value: T }[] = values.map(v => {
|
||||
|
||||
const vIsSelected = new UIEventSource(false);
|
||||
|
||||
selectedElements.addCallbackAndRunD(selectedElements => {
|
||||
vIsSelected.setData(selectedElements.some(t => t === v.value))
|
||||
})
|
||||
|
||||
vIsSelected.addCallback(selected => {
|
||||
if (selected) {
|
||||
if (mode === "select-one") {
|
||||
selectedElements.setData([v.value])
|
||||
} else if (!selectedElements.data.some(t => t === v.value)) {
|
||||
selectedElements.data.push(v.value);
|
||||
selectedElements.ping()
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < selectedElements.data.length; i++) {
|
||||
const t = selectedElements.data[i]
|
||||
if (t == v.value) {
|
||||
selectedElements.data.splice(i, 1)
|
||||
selectedElements.ping()
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const toggle = new SelfHidingToggle(v.show, v.mainTerm, searchValue, {
|
||||
searchTerms: v.searchTerms,
|
||||
selected: vIsSelected,
|
||||
squared: mode === "select-many"
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
...v,
|
||||
show: toggle
|
||||
};
|
||||
})
|
||||
|
||||
let totalShown: Store<number>
|
||||
if (options.selectIfSingle) {
|
||||
let forcedSelection : { value: T, show: SelfHidingToggle } = undefined
|
||||
totalShown = searchValue.map(_ => {
|
||||
let totalShown = 0;
|
||||
let lastShownValue: { value: T, show: SelfHidingToggle }
|
||||
for (const mv of mappedValues) {
|
||||
const valueIsShown = mv.show.isShown.data
|
||||
if (valueIsShown) {
|
||||
totalShown++;
|
||||
lastShownValue = mv
|
||||
}
|
||||
}
|
||||
if (totalShown == 1) {
|
||||
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;
|
||||
selectedElements.setData([])
|
||||
}
|
||||
|
||||
return totalShown
|
||||
}, mappedValues.map(mv => mv.show.GetValue()))
|
||||
} else {
|
||||
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 (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 ?? "")
|
||||
}, [totalShown, searchValue]))
|
||||
|
||||
])
|
||||
this.selectedElements = selectedElements;
|
||||
this.someMatchFound = totalShown.map(t => t > 0);
|
||||
|
||||
}
|
||||
|
||||
public GetValue(): UIEventSource<T[]> {
|
||||
return this.selectedElements;
|
||||
}
|
||||
|
||||
IsValid(t: T[]): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -8,52 +8,52 @@ import * as used_languages from "../assets/generated/used_languages.json"
|
|||
import Lazy from "./Base/Lazy";
|
||||
import Toggle from "./Input/Toggle";
|
||||
|
||||
export default class LanguagePicker {
|
||||
export default class LanguagePicker extends Toggle {
|
||||
|
||||
|
||||
public static CreateLanguagePicker(
|
||||
languages: string[],
|
||||
label: string | BaseUIElement = "") : BaseUIElement{
|
||||
constructor(languages: string[],
|
||||
label: string | BaseUIElement = "") {
|
||||
|
||||
|
||||
if (languages === undefined || languages.length <= 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const allLanguages : string[] = used_languages.languages;
|
||||
|
||||
|
||||
const allLanguages: string[] = used_languages.languages;
|
||||
|
||||
const normalPicker = LanguagePicker.dropdownFor(languages, label);
|
||||
const fullPicker = new Lazy(() => LanguagePicker.dropdownFor(allLanguages, label))
|
||||
return new Toggle(fullPicker, normalPicker, Locale.showLinkToWeblate)
|
||||
super(fullPicker, normalPicker, Locale.showLinkToWeblate);
|
||||
}
|
||||
|
||||
|
||||
private static dropdownFor(languages: string[], label: string | BaseUIElement): BaseUIElement {
|
||||
return new DropDown(label, languages
|
||||
.filter(lang => lang !== "_context")
|
||||
.map(lang => {
|
||||
return {value: lang, shown: LanguagePicker.hybrid(lang) }
|
||||
}
|
||||
), Locale.language)
|
||||
return new DropDown(label, languages
|
||||
.filter(lang => lang !== "_context")
|
||||
.map(lang => {
|
||||
return {value: lang, shown: LanguagePicker.hybrid(lang)}
|
||||
}
|
||||
), Locale.language)
|
||||
}
|
||||
|
||||
private static hybrid(lang: string): Translation {
|
||||
const nativeText = native[lang] ?? lang
|
||||
const allTranslations = (language_translations["default"] ?? language_translations)
|
||||
const allTranslations = (language_translations["default"] ?? language_translations)
|
||||
const translation = {}
|
||||
const trans = allTranslations[lang]
|
||||
if(trans === undefined){
|
||||
const trans = allTranslations[lang]
|
||||
if (trans === undefined) {
|
||||
return new Translation({"*": nativeText})
|
||||
}
|
||||
for (const key in trans) {
|
||||
const translationInKey = allTranslations[lang][key]
|
||||
if(nativeText.toLowerCase() === translationInKey.toLowerCase()){
|
||||
if (nativeText.toLowerCase() === translationInKey.toLowerCase()) {
|
||||
translation[key] = nativeText
|
||||
}else{
|
||||
translation[key] = nativeText + " ("+translationInKey+")"
|
||||
} else {
|
||||
translation[key] = nativeText + " (" + translationInKey + ")"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return new Translation(translation)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -477,9 +477,10 @@ export class OH {
|
|||
lat: tags._lat,
|
||||
lon: tags._lon,
|
||||
address: {
|
||||
country_code: tags._country.toLowerCase()
|
||||
country_code: tags._country.toLowerCase(),
|
||||
state: undefined
|
||||
},
|
||||
}, {tag_key: "opening_hours"});
|
||||
}, <any> {tag_key: "opening_hours"});
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -25,16 +25,20 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
tags: UIEventSource<any>,
|
||||
layerConfig: LayerConfig,
|
||||
state: FeaturePipelineState,
|
||||
hashToShow?: string,
|
||||
isShown?: UIEventSource<boolean>,
|
||||
options?: {
|
||||
hashToShow?: string,
|
||||
isShown?: UIEventSource<boolean>,
|
||||
setHash?: true | boolean
|
||||
}
|
||||
) {
|
||||
if (state === undefined) {
|
||||
throw "State is undefined!"
|
||||
}
|
||||
super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig, state),
|
||||
() => FeatureInfoBox.GenerateContent(tags, layerConfig, state),
|
||||
hashToShow ?? tags.data.id ?? "item",
|
||||
isShown);
|
||||
options?.hashToShow ?? tags.data.id ?? "item",
|
||||
options?.isShown,
|
||||
options);
|
||||
|
||||
if (layerConfig === undefined) {
|
||||
throw "Undefined layerconfig";
|
||||
|
@ -50,7 +54,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
const titleIcons = new Combine(
|
||||
layerConfig.titleIcons.map(icon => {
|
||||
return new TagRenderingAnswer(tags, icon, state,
|
||||
"block h-8 max-h-8 align-baseline box-content sm:p-0.5").SetClass("flex");
|
||||
"block h-8 max-h-8 align-baseline box-content sm:p-0.5 titleicon");
|
||||
}
|
||||
))
|
||||
.SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
|
||||
|
|
|
@ -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";
|
||||
|
@ -31,6 +31,8 @@ import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
|
|||
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.
|
||||
|
@ -38,7 +40,7 @@ import {GeoOperations} from "../../Logic/GeoOperations";
|
|||
*/
|
||||
export default class TagRenderingQuestion extends Combine {
|
||||
|
||||
constructor(tags: UIEventSource<any>,
|
||||
constructor(tags: UIEventSource<Record<string, string> & { id: string }>,
|
||||
configuration: TagRenderingConfig,
|
||||
state?: FeaturePipelineState,
|
||||
options?: {
|
||||
|
@ -52,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
|
||||
|
@ -81,13 +83,13 @@ export default class TagRenderingQuestion extends Combine {
|
|||
|
||||
const feedback = new UIEventSource<Translation>(undefined)
|
||||
const inputElement: ReadonlyInputElement<TagsFilter> =
|
||||
new VariableInputElement(applicableMappingsSrc.map(applicableMappings => {
|
||||
return TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags, feedback)
|
||||
new VariableInputElement(applicableMappingsSrc.map(applicableMappings => {
|
||||
return TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags, feedback)
|
||||
}
|
||||
))
|
||||
|
||||
|
||||
const save = () => {
|
||||
const selection = inputElement.GetValue().data;
|
||||
const selection = TagUtils.FlattenMultiAnswer([inputElement.GetValue().data]);
|
||||
if (selection) {
|
||||
(state?.changes)
|
||||
.applyAction(new ChangeTagAction(
|
||||
|
@ -141,20 +143,29 @@ 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[] }[],
|
||||
applicableMappings: Mapping[],
|
||||
applicableUnit: Unit,
|
||||
tagsSource: UIEventSource<any>,
|
||||
feedback: UIEventSource<Translation>
|
||||
): ReadonlyInputElement<TagsFilter> {
|
||||
|
||||
// FreeForm input will be undefined if not present; will already contain a special input element if applicable
|
||||
const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource, feedback);
|
||||
|
||||
|
||||
const hasImages = applicableMappings.findIndex(mapping => mapping.icon !== undefined) >= 0
|
||||
let inputEls: InputElement<TagsFilter>[];
|
||||
|
||||
const ifNotsPresent = applicableMappings.some(mapping => mapping.ifnot !== undefined)
|
||||
|
||||
if (applicableMappings.length > 8 &&
|
||||
(configuration.freeform?.type === undefined || configuration.freeform?.type === "string") &&
|
||||
(!configuration.multiAnswer || configuration.freeform === undefined)) {
|
||||
|
||||
return TagRenderingQuestion.GenerateSearchableSelector(state, configuration, applicableMappings, tagsSource)
|
||||
}
|
||||
|
||||
|
||||
// FreeForm input will be undefined if not present; will already contain a special input element if applicable
|
||||
const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource, feedback);
|
||||
|
||||
function allIfNotsExcept(excludeIndex: number): TagsFilter[] {
|
||||
if (configuration.mappings === undefined || configuration.mappings.length === 0) {
|
||||
return undefined
|
||||
|
@ -221,6 +232,209 @@ export default class TagRenderingQuestion extends Combine {
|
|||
|
||||
}
|
||||
|
||||
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 = <Mapping>{
|
||||
...mapping,
|
||||
iconClass: `small-height`,
|
||||
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,
|
||||
original: mapping
|
||||
})
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* // 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,
|
||||
searchValue,
|
||||
onNoMatches: onEmpty?.SetClass(classes).SetClass("flex justify-center items-center"),
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
if (x0 === undefined || x1 === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (x0.and.length !== x1.and.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < x0.and.length; i++) {
|
||||
if (x1.and[i] != x0.and[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
(selected) => {
|
||||
if (ff !== undefined && searchValue.data?.length > 0 && !presetSearch.someMatchFound.data) {
|
||||
const t = fallbackTag.data;
|
||||
if (ff.addExtraTags) {
|
||||
return new And([t, ...ff.addExtraTags])
|
||||
}
|
||||
return new And([t]);
|
||||
}
|
||||
|
||||
if (selected === undefined || selected.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tfs = Utils.NoNull(applicableMappings.map((mapping, i) => {
|
||||
if (selected.indexOf(i) >= 0) {
|
||||
return mapping.if
|
||||
} else {
|
||||
return mapping.ifnot
|
||||
}
|
||||
}))
|
||||
console.log("Got tags", tfs)
|
||||
return new And(tfs);
|
||||
},
|
||||
(tf) => {
|
||||
if (tf === undefined) {
|
||||
return []
|
||||
}
|
||||
const selected: number[] = []
|
||||
for (let i = 0; i < applicableMappings.length; i++) {
|
||||
const mapping = applicableMappings[i]
|
||||
if (tf.and.some(t => mapping.if == t)) {
|
||||
selected.push(i)
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
},
|
||||
[searchValue, presetSearch.someMatchFound]
|
||||
);
|
||||
}
|
||||
|
||||
private static GenerateMultiAnswer(
|
||||
configuration: TagRenderingConfig,
|
||||
|
@ -332,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?: string
|
||||
}, ifNot?: TagsFilter[]): InputElement<TagsFilter> {
|
||||
mapping: Mapping, ifNot?: TagsFilter[]): InputElement<TagsFilter> {
|
||||
|
||||
let tagging: TagsFilter = mapping.if;
|
||||
if (ifNot !== undefined) {
|
||||
|
@ -355,16 +563,12 @@ export default class TagRenderingQuestion extends Combine {
|
|||
(t0, t1) => t1.shadows(t0));
|
||||
}
|
||||
|
||||
private static GenerateMappingContent(mapping: {
|
||||
then: Translation,
|
||||
icon?: string,
|
||||
iconClass?: string
|
||||
}, 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;
|
||||
}
|
||||
return new Combine([new Img(mapping.icon).SetClass("mapping-icon-"+(mapping.iconClass ?? "small")), text]).SetClass("flex")
|
||||
return new Combine([new Img(mapping.icon).SetClass("mr-1 mapping-icon-" + (mapping.iconClass ?? "small")), text]).SetClass("flex items-center")
|
||||
}
|
||||
|
||||
private static GenerateFreeform(state: FeaturePipelineState, configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>, feedback: UIEventSource<Translation>)
|
||||
|
@ -412,7 +616,7 @@ export default class TagRenderingQuestion extends Combine {
|
|||
|
||||
const tagsData = tags.data;
|
||||
const feature = state?.allElements?.ContainingFeatures?.get(tagsData.id)
|
||||
const center = feature != undefined ? GeoOperations.centerpointCoordinates(feature) : [0,0]
|
||||
const center = feature != undefined ? GeoOperations.centerpointCoordinates(feature) : [0, 0]
|
||||
const input: InputElement<string> = ValidatedTextField.ForType(configuration.freeform.type)?.ConstructInputElement({
|
||||
country: () => tagsData._country,
|
||||
location: [center[1], center[0]],
|
||||
|
@ -423,13 +627,13 @@ export default class TagRenderingQuestion extends Combine {
|
|||
placeholder: configuration.freeform.placeholder,
|
||||
feedback
|
||||
});
|
||||
|
||||
|
||||
// Init with correct value
|
||||
input?.GetValue().setData(tagsData[freeform.key] ?? freeform.default);
|
||||
|
||||
|
||||
// Add a length check
|
||||
input?.GetValue().addCallbackD((v : string | undefined) => {
|
||||
if(v?.length >= 255){
|
||||
input?.GetValue().addCallbackD((v: string | undefined) => {
|
||||
if (v?.length >= 255) {
|
||||
feedback.setData(Translations.t.validation.tooLong.Subs({count: v.length}))
|
||||
}
|
||||
})
|
||||
|
@ -448,10 +652,10 @@ export default class TagRenderingQuestion extends Combine {
|
|||
return inputTagsFilter;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static CreateTagExplanation(selectedValue: Store<TagsFilter>,
|
||||
tags: Store<object>,
|
||||
state?: {osmConnection?: OsmConnection}){
|
||||
state?: { osmConnection?: OsmConnection }) {
|
||||
return new VariableUiElement(
|
||||
selectedValue.map(
|
||||
(tagsFilter: TagsFilter) => {
|
||||
|
|
|
@ -98,7 +98,7 @@ class ProfessionalGui extends LeftIndex {
|
|||
maxDepth: 2
|
||||
}).SetClass("subtle"),
|
||||
|
||||
LanguagePicker.CreateLanguagePicker(Translations.t.professional.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"),
|
||||
new LanguagePicker(Translations.t.professional.title.SupportedLanguages(), "")?.SetClass("mt-4 self-end flex-col"),
|
||||
].map(el => el?.SetClass("pl-4"))
|
||||
|
||||
super(leftContents, content)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
|
||||
import {ElementStorage} from "../../Logic/ElementStorage";
|
||||
|
@ -20,7 +20,7 @@ We don't actually import it here. It is imported in the 'MinimapImplementation'-
|
|||
export default class ShowDataLayerImplementation {
|
||||
|
||||
private static dataLayerIds = 0
|
||||
private readonly _leafletMap: UIEventSource<L.Map>;
|
||||
private readonly _leafletMap: Store<L.Map>;
|
||||
private readonly _enablePopups: boolean;
|
||||
private readonly _features: RenderingMultiPlexerFeatureSource
|
||||
private readonly _layerToShow: LayerConfig;
|
||||
|
|
|
@ -7,7 +7,7 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
|||
export interface ShowDataLayerOptions {
|
||||
features: FeatureSource,
|
||||
selectedElement?: UIEventSource<any>,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
leafletMap: Store<L.Map>,
|
||||
popup?: undefined | ((tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen),
|
||||
zoomToFeatures?: false | boolean,
|
||||
doShowLayer?: Store<boolean>,
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
/**
|
||||
* SHows geojson on the given leaflet map, but attempts to figure out the correct layer first
|
||||
*/
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Store} from "../../Logic/UIEventSource";
|
||||
import ShowDataLayer from "./ShowDataLayer";
|
||||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
|
||||
|
||||
export default class ShowDataMultiLayer {
|
||||
constructor(options: ShowDataLayerOptions & { layers: UIEventSource<FilteredLayer[]> }) {
|
||||
constructor(options: ShowDataLayerOptions & { layers: Store<FilteredLayer[]> }) {
|
||||
|
||||
new PerLayerFeatureSourceSplitter(options.layers, (perLayer => {
|
||||
const newOptions = {
|
||||
|
|
|
@ -49,6 +49,9 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
const allElements = SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map(
|
||||
proto => {
|
||||
if (proto.fixed !== undefined) {
|
||||
if(tagsSource === undefined){
|
||||
return Utils.SubstituteKeys(proto.fixed, undefined)
|
||||
}
|
||||
return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags)));
|
||||
}
|
||||
const viz = proto.special;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue