Add level selector and global filters

This commit is contained in:
Pieter Vander Vennet 2023-04-26 18:04:42 +02:00
parent 5504d49d59
commit 7fd7a3722e
19 changed files with 401 additions and 253 deletions

View file

@ -1,8 +1,7 @@
import { FeatureSource, FeatureSourceForLayer } from "./FeatureSource"
import { FeatureSource, IndexedFeatureSource } from "./FeatureSource"
import FilteredLayer from "../../Models/FilteredLayer"
import SimpleFeatureSource from "./Sources/SimpleFeatureSource"
import { Feature } from "geojson"
import { Utils } from "../../Utils"
import { UIEventSource } from "../UIEventSource"
/**
@ -10,9 +9,7 @@ import { UIEventSource } from "../UIEventSource"
* If this is the case, multiple objects with a different _matching_layer_id are generated.
* In any case, this featureSource marks the objects with _matching_layer_id
*/
export default class PerLayerFeatureSourceSplitter<
T extends FeatureSourceForLayer = SimpleFeatureSource
> {
export default class PerLayerFeatureSourceSplitter<T extends FeatureSource = FeatureSource> {
public readonly perLayer: ReadonlyMap<string, T>
constructor(
layers: FilteredLayer[],
@ -23,6 +20,11 @@ export default class PerLayerFeatureSourceSplitter<
}
) {
const knownLayers = new Map<string, T>()
/**
* Keeps track of the ids that are included per layer.
* Used to know if the downstream feature source needs to be pinged
*/
let layerIndexes: ReadonlySet<string>[] = layers.map((_) => new Set<string>())
this.perLayer = knownLayers
const layerSources = new Map<string, UIEventSource<Feature[]>>()
const constructStore =
@ -41,6 +43,12 @@ export default class PerLayerFeatureSourceSplitter<
// We try to figure out (for each feature) in which feature store it should be saved.
const featuresPerLayer = new Map<string, Feature[]>()
/**
* Indexed on layer-position
* Will be true if a new id pops up
*/
const hasChanged: boolean[] = layers.map((_) => false)
const newIndices: Set<string>[] = layers.map((_) => new Set<string>())
const noLayerFound: Feature[] = []
for (const layer of layers) {
@ -49,9 +57,14 @@ export default class PerLayerFeatureSourceSplitter<
for (const f of features) {
let foundALayer = false
for (const layer of layers) {
for (let i = 0; i < layers.length; i++) {
const layer = layers[i]
if (layer.layerDef.source.osmTags.matchesProperties(f.properties)) {
const id = f.properties.id
// We have found our matching layer!
const previousIndex = layerIndexes[i]
hasChanged[i] = hasChanged[i] || !previousIndex.has(id)
newIndices[i].add(id)
featuresPerLayer.get(layer.layerDef.id).push(f)
foundALayer = true
if (!layer.layerDef.passAllFeatures) {
@ -67,7 +80,8 @@ export default class PerLayerFeatureSourceSplitter<
// At this point, we have our features per layer as a list
// We assign them to the correct featureSources
for (const layer of layers) {
for (let i = 0; i < layers.length; i++) {
const layer = layers[i]
const id = layer.layerDef.id
const features = featuresPerLayer.get(id)
if (features === undefined) {
@ -75,14 +89,17 @@ export default class PerLayerFeatureSourceSplitter<
continue
}
const src = layerSources.get(id)
if (Utils.sameList(src.data, features)) {
if (!hasChanged[i] && layerIndexes[i].size === newIndices[i].size) {
// No new id has been added and the sizes are the same (thus: nothing has been removed as well)
// We can safely assume that no changes were made
continue
}
src.setData(features)
layerSources.get(id).setData(features)
}
layerIndexes = newIndices
// AT last, the leftovers are handled
if (options?.handleLeftovers !== undefined && noLayerFound.length > 0) {
options.handleLeftovers(noLayerFound)
@ -90,7 +107,7 @@ export default class PerLayerFeatureSourceSplitter<
})
}
public forEach(f: (featureSource: FeatureSourceForLayer) => void) {
public forEach(f: (featureSource: FeatureSource) => void) {
for (const fs of this.perLayer.values()) {
f(fs)
}

View file

@ -122,7 +122,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
throw "This is an absurd high zoom level"
}
if (z < 14) {
if (z < 15) {
throw `Zoom ${z} is too much for OSM to handle! Use a higher zoom level!`
}
const index = Tiles.tile_index(z, x, y)

View file

@ -35,7 +35,18 @@ export default class MetaTagging {
continue
}
const featureSource = state.perLayer.get(layer.id)
featureSource.features?.addCallbackAndRunD((features) => {
featureSource.features?.stabilized(1000)?.addCallbackAndRunD((features) => {
if (!(features?.length > 0)) {
// No features to handle
return
}
console.trace(
"Recalculating metatags for layer ",
layer.id,
"due to a change in the upstream features. Contains ",
features.length,
"items"
)
MetaTagging.addMetatags(
features,
params,
@ -71,7 +82,6 @@ export default class MetaTagging {
return
}
console.debug("Recalculating metatags...")
const metatagsToApply: SimpleMetaTagger[] = []
for (const metatag of SimpleMetaTaggers.metatags) {
if (metatag.includesDates) {

View file

@ -3,6 +3,8 @@ import { GlobalFilter } from "../../Models/GlobalFilter"
import FilteredLayer from "../../Models/FilteredLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { OsmConnection } from "../Osm/OsmConnection"
import { Tag } from "../Tags/Tag"
import Translations from "../../UI/i18n/Translations"
/**
* The layer state keeps track of:
@ -41,6 +43,45 @@ export default class LayerState {
}
this.filteredLayers = filteredLayers
layers.forEach((l) => LayerState.linkFilterStates(l, filteredLayers))
this.globalFilters.data.push({
id: "level",
osmTags: undefined,
state: undefined,
onNewPoint: undefined,
})
}
/**
* Sets the global filter which looks to the 'level'-tag.
* Only features with the given 'level' will be shown.
*
* If undefined is passed, _all_ levels will be shown
* @param level
*/
public setLevelFilter(level?: string) {
// Remove all previous
const l = this.globalFilters.data.length
this.globalFilters.data = this.globalFilters.data.filter((f) => f.id !== "level")
if (!level) {
if (l !== this.globalFilters.data.length) {
this.globalFilters.ping()
}
return
}
const t = Translations.t.general.levelSelection
this.globalFilters.data.push({
id: "level",
state: level,
osmTags: new Tag("level", level),
onNewPoint: {
tags: [new Tag("level", level)],
icon: "./assets/svg/elevator.svg",
confirmAddNew: t.confirmLevel.PartialSubs({ level }),
safetyCheck: t.addNewOnLevel.Subs({ level }),
},
})
this.globalFilters.ping()
}
/**

View file

@ -186,6 +186,12 @@ export default class FilteredLayer {
if (properties._deleted === "yes") {
return false
}
for (const globalFilter of globalFilters ?? []) {
const neededTags = globalFilter.osmTags
if (neededTags !== undefined && !neededTags.matchesProperties(properties)) {
return false
}
}
{
const isShown: TagsFilter = this.layerDef.isShown
if (isShown !== undefined && !isShown.matchesProperties(properties)) {
@ -200,12 +206,6 @@ export default class FilteredLayer {
}
}
for (const globalFilter of globalFilters ?? []) {
const neededTags = globalFilter.osmTags
if (neededTags !== undefined && !neededTags.matchesProperties(properties)) {
return false
}
}
return true
}

View file

@ -8,6 +8,7 @@ export interface GlobalFilter {
id: string
onNewPoint: {
safetyCheck: Translation
icon: string
confirmAddNew: TypedTranslation<{ preset: Translation }>
tags: Tag[]
}

View file

@ -92,6 +92,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
string,
{ readonly isDisplayed: UIEventSource<boolean> }
>
/**
* All 'level'-tags that are available with the current features
*/
readonly floors: Store<string[]>
constructor(layout: LayoutConfig) {
this.layout = layout
@ -214,17 +218,29 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.featureProperties
)
const doShowLayer = this.mapProperties.zoom.map(
(z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed]
)
if (
!doShowLayer.data &&
(this.featureSwitches.featureSwitchFilter.data === false || !fs.layer.layerDef.name)
) {
/* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined)
*
* This means that we don't have to filter it, nor do we have to display it
* */
return
}
const filtered = new FilteringFeatureSource(
fs.layer,
fs,
(id) => this.featureProperties.getStore(id),
this.layerState.globalFilters
)
const doShowLayer = this.mapProperties.zoom.map(
(z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed]
)
new ShowDataLayer(this.map, {
layer: fs.layer.layerDef,
@ -236,6 +252,33 @@ export default class ThemeViewState implements SpecialVisualizationState {
})
})
this.floors = this.indexedFeatures.features.stabilized(500).map((features) => {
if (!features) {
return []
}
const floors = new Set<string>()
for (const feature of features) {
const level = feature.properties["level"]
if (level) {
floors.add(level)
}
}
const sorted = Array.from(floors)
// Sort alphabetically first, to deal with floor "A", "B" and "C"
sorted.sort()
sorted.sort((a, b) => {
// We use the laxer 'parseInt' to deal with floor '1A'
const na = parseInt(a)
const nb = parseInt(b)
if (isNaN(na) || isNaN(nb)) {
return 0
}
return na - nb
})
sorted.reverse(/* new list, no side-effects */)
return sorted
})
const lastClick = (this.lastClickObject = new LastClickFeatureSource(
this.mapProperties.lastClickLocation,
this.layout
@ -443,7 +486,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
const found = this.indexedFeatures.featuresById.data?.get(hash)
console.log("Found:", found)
if (!found) {
return
}
@ -451,7 +493,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.selectedElement.setData(found)
this.selectedLayer.setData(layer)
},
[this.indexedFeatures.featuresById]
[this.indexedFeatures.featuresById.stabilized(250)]
)
new MetaTagging(this)

View file

@ -0,0 +1,29 @@
<script lang="ts">
/**
* Shows a 'floorSelector' and maps the selected floor onto a global filter
*/
import LayerState from "../../Logic/State/LayerState";
import FloorSelector from "../InputElement/Helpers/FloorSelector.svelte";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
export let layerState: LayerState;
export let floors: Store<string[]>;
export let zoom: Store<number>;
const maxZoom = 16
let selectedFloor: UIEventSource<string> = new UIEventSource<string>(undefined);
selectedFloor.stabilized(5).map(floor => {
if(floors.data === undefined || floors.data.length <= 1 || zoom.data < maxZoom){
// Only a single floor is visible -> disable the 'level' global filter
// OR we might have zoomed out to much ant want to show all
layerState.setLevelFilter(undefined)
}else{
layerState.setLevelFilter(floor)
}
}, [floors, zoom])
</script>
{#if $zoom >= maxZoom}
<FloorSelector {floors} value={selectedFloor} />
{/if}

View file

@ -1,151 +0,0 @@
import FloorLevelInputElement from "../Input/FloorLevelInputElement"
import MapState from "../../Logic/State/MapState"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import { RegexTag } from "../../Logic/Tags/RegexTag"
import { Or } from "../../Logic/Tags/Or"
import { Tag } from "../../Logic/Tags/Tag"
import Translations from "../i18n/Translations"
import Combine from "../Base/Combine"
import { OsmFeature } from "../../Models/OsmFeature"
import { BBox } from "../../Logic/BBox"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import { Store } from "../../Logic/UIEventSource"
import { GlobalFilter } from "../../Logic/State/GlobalFilter"
/***
* The element responsible for the level input element and picking the right level, showing and hiding at the right time, ...
*/
export default class LevelSelector extends Combine {
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {
const levelsInView: Store<Record<string, number>> = 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 levels: Record<string, number> = { "0": 0 }
for (const levelDescription of allLevelsRaw) {
if (levelDescription === undefined) {
levels["0"]++
}
for (const level of TagUtils.LevelsParser(levelDescription)) {
levels[level] = (levels[level] ?? 0) + 1
}
}
return levels
})
const levelSelect = new FloorLevelInputElement(levelsInView)
state.globalFilters.data.push({
filter: {
currentFilter: undefined,
state: undefined,
},
id: "level",
onNewPoint: undefined,
})
const isShown = levelsInView.map(
(levelsInView) => {
if (state.locationControl.data.zoom <= 16) {
return false
}
if (Object.keys(levelsInView).length == 1) {
return false
}
return true
},
[state.locationControl]
)
function setLevelFilter() {
console.log(
"Updating levels filter to ",
levelSelect.GetValue().data,
" is shown:",
isShown.data
)
const filter: GlobalFilter = state.globalFilters.data.find((gf) => gf.id === "level")
if (!isShown.data) {
filter.filter = {
state: "*",
currentFilter: undefined,
}
filter.onNewPoint = undefined
state.globalFilters.ping()
return
}
const l = levelSelect.GetValue().data
if (l === undefined) {
return
}
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")
}
})
levelsInView.addCallbackAndRun((levels) => {
if (!isShown.data) {
return
}
const value = levelSelect.GetValue()
if (!(levels[value.data] === undefined || levels[value.data] === 0)) {
return
}
// Nothing in view. Lets switch to a different level (the level with the most features)
let mostElements = 0
let mostElementsLevel = undefined
for (const level in levels) {
const count = levels[level]
if (mostElementsLevel === undefined || mostElements < count) {
mostElementsLevel = level
mostElements = count
}
}
console.log(
"Force switching to a different level:",
mostElementsLevel,
"as it has",
mostElements,
"elements on that floor",
levels,
"(old level: " + value.data + ")"
)
value.setData(mostElementsLevel)
})
levelSelect.GetValue().addCallback((_) => setLevelFilter())
super([levelSelect])
}
}

View file

@ -1,11 +0,0 @@
import Combine from "../Base/Combine"
import MapState from "../../Logic/State/MapState"
import LevelSelector from "./LevelSelector"
export default class RightControls extends Combine {
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {
const levelSelector = new LevelSelector(state)
super([levelSelector].map((el) => el.SetClass("m-0.5 md:m-1")))
this.SetClass("flex flex-col items-center")
}
}

View file

@ -1,6 +1,5 @@
import Toggle from "./Input/Toggle"
import LeftControls from "./BigComponents/LeftControls"
import RightControls from "./BigComponents/RightControls"
import CenterMessageBox from "./CenterMessageBox"
import { DefaultGuiState } from "./DefaultGuiState"
import Combine from "./Base/Combine"
@ -42,7 +41,6 @@ export default class DefaultGUI {
const guiState = this.guiState
new LeftControls(state, guiState).AttachTo("bottom-left")
new RightControls(state, this.geolocationHandler).AttachTo("bottom-right")
new CenterMessageBox(state).AttachTo("centermessage")
document?.getElementById("centermessage")?.classList?.add("pointer-events-none")

View file

@ -0,0 +1,140 @@
<script lang="ts">
import { Store, Stores, UIEventSource } from "../../../Logic/UIEventSource";
/**
* Given the available floors, shows an elevator to pick a single one
*
* This is but the input element, the logic of handling the filter is in 'LevelSelector'
*/
export let floors: Store<string[]>;
export let value: UIEventSource<string>;
const HEIGHT = 40;
let initialIndex = Math.max(0, floors?.data?.findIndex(f => f === value?.data) ?? 0);
let index: UIEventSource<number> = new UIEventSource<number>(initialIndex);
let forceIndex: number | undefined = undefined;
let top = Math.max(0, initialIndex) * HEIGHT;
let elevator: HTMLImageElement;
let mouseDown = false;
let container: HTMLElement;
$:{
if (top > 0 || forceIndex !== undefined) {
index.setData(closestFloorIndex());
value.setData(floors.data[forceIndex ?? closestFloorIndex()]);
}
}
function unclick() {
mouseDown = false;
}
function click() {
mouseDown = true;
}
function closestFloorIndex() {
return Math.min(floors.data.length - 1, Math.max(0, Math.round(top / HEIGHT)));
}
function onMove(e: { movementY: number }) {
if (mouseDown) {
forceIndex = undefined;
const containerY = container.clientTop;
const containerMax = containerY + (floors.data.length - 1) * HEIGHT;
top = Math.min(Math.max(0, top + e.movementY), containerMax);
}
}
let momentum = 0;
function stabilize() {
// Automatically move the elevator to the closes floor
if (mouseDown) {
return;
}
const target = (forceIndex ?? index.data) * HEIGHT;
let diff = target - top;
if (diff > 1) {
diff /= 3;
}
const sign = Math.sign(diff);
momentum = momentum + sign;
let diffR = Math.min(Math.abs(momentum), forceIndex !== undefined ? 9 : 3, Math.abs(diff));
momentum = Math.sign(momentum) * Math.min(diffR, Math.abs(momentum));
top += sign * diffR;
if (index.data === forceIndex) {
forceIndex = undefined;
}
}
Stores.Chronic(50).addCallback(_ => stabilize());
let image: HTMLImageElement;
$:{
if (image) {
let lastY = 0;
image.ontouchstart = (e: TouchEvent) => {
mouseDown = true;
lastY = e.changedTouches[0].clientY;
};
image.ontouchmove = e => {
const y = e.changedTouches[0].clientY;
console.log(y)
const movementY = y - lastY;
lastY = y;
onMove({ movementY });
};
image.ontouchend = unclick;
}
}
</script>
<div bind:this={container} class="relative"
style={`height: calc(${HEIGHT}px * ${$floors.length}); width: 96px`}>
<div class="h-full absolute w-min right-0">
{#each $floors as floor, i}
<button style={`height: ${HEIGHT}px; width: ${HEIGHT}px`}
class={"border-2 border-gray-300 flex content-box justify-center items-center "+(i === (forceIndex ?? $index) ? "selected": "normal-background" )
}
on:click={() => {forceIndex = i}}
> {floor}</button>
{/each}
</div>
<div style={`width: ${HEIGHT}px`}>
<img bind:this={image} class="draggable" draggable="false" on:mousedown={click} src="./assets/svg/elevator.svg"
style={" top: "+top+"px;"} />
</div>
</div>
<svelte:window on:mousemove={onMove} on:mouseup={unclick} />
<style>
.selected {
background: var(--subtle-detail-color);
font-weight: bold;
border-color: black;
}
.draggable {
user-select: none;
cursor: move;
position: absolute;
user-drag: none;
height: 72px;
margin-top: -15px;
margin-bottom: -15px;
margin-left: -18px;
-webkit-user-drag: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
</style>

View file

@ -25,22 +25,36 @@
import { Tag } from "../../../Logic/Tags/Tag";
import type { WayId } from "../../../Models/OsmFeature";
import Loading from "../../Base/Loading.svelte";
import type { GlobalFilter } from "../../../Models/GlobalFilter";
import { onDestroy } from "svelte";
export let coordinate: { lon: number, lat: number };
export let state: SpecialVisualizationState;
let selectedPreset: { preset: PresetConfig, layer: LayerConfig, icon: string, tags: Record<string, string> } = undefined;
let selectedPreset: {
preset: PresetConfig,
layer: LayerConfig,
icon: string,
tags: Record<string, string>
} = undefined;
let checkedOfGlobalFilters : number = 0
let confirmedCategory = false;
$: if (selectedPreset === undefined) {
confirmedCategory = false;
creating = false;
checkedOfGlobalFilters = 0
}
let flayer: FilteredLayer = undefined;
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined;
let layerHasFilters: Store<boolean> | undefined = undefined;
let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters;
let _globalFilter: GlobalFilter[];
onDestroy(globalFilter.addCallbackAndRun(globalFilter => {
console.log("Global filters are", globalFilter);
_globalFilter = globalFilter ?? [];
}));
$:{
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id);
layerIsDisplayed = flayer?.isDisplayed;
@ -71,38 +85,38 @@
creating = true;
const location: { lon: number; lat: number } = preciseCoordinate.data;
const snapTo: WayId | undefined = <WayId>snappedToObject.data;
const tags: Tag[] = selectedPreset.preset.tags;
const tags: Tag[] = selectedPreset.preset.tags.concat(..._globalFilter.map(f => f.onNewPoint.tags));
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags);
let snapToWay: undefined | OsmWay = undefined
if(snapTo !== undefined){
let snapToWay: undefined | OsmWay = undefined;
if (snapTo !== undefined) {
const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0);
if(downloaded !== "deleted"){
snapToWay = downloaded
if (downloaded !== "deleted") {
snapToWay = downloaded;
}
}
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon,
{
theme: state.layout?.id ?? "unkown",
changeType: "create",
snapOnto: snapToWay
});
await state.changes.applyAction(newElementAction)
state.newFeatures.features.ping()
theme: state.layout?.id ?? "unkown",
changeType: "create",
snapOnto: snapToWay
});
await state.changes.applyAction(newElementAction);
state.newFeatures.features.ping();
// The 'changes' should have created a new point, which added this into the 'featureProperties'
const newId = newElementAction.newElementId;
console.log("Applied pending changes, fetching store for", newId)
console.log("Applied pending changes, fetching store for", newId);
const tagsStore = state.featureProperties.getStore(newId);
{
// Set some metainfo
const properties = tagsStore.data;
if (snapTo) {
// metatags (starting with underscore) are not uploaded, so we can safely mark this
delete properties["_referencing_ways"]
delete properties["_referencing_ways"];
properties["_referencing_ways"] = `["${snapTo}"]`;
}
properties["_backend"] = state.osmConnection.Backend()
properties["_backend"] = state.osmConnection.Backend();
properties["_last_edit:timestamp"] = new Date().toISOString();
const userdetails = state.osmConnection.userDetails.data;
properties["_last_edit:contributor"] = userdetails.name;
@ -113,13 +127,17 @@
abort();
state.selectedLayer.setData(selectedPreset.layer);
state.selectedElement.setData(feature);
tagsStore.ping()
tagsStore.ping();
}
</script>
<LoginToggle ignoreLoading={true} {state}>
<!-- This component is basically one big if/then/else flow checking for many conditions and edge cases that (in some cases) have to be handled;
1. the first (and outermost) is of course: are we logged in?
2. What do we want to add?
3. Are all elements of this category visible? (i.e. there are no filters possibly hiding this, is the data still loading, ...) -->
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
</LoginButton>
@ -163,7 +181,7 @@
{:else if $layerHasFilters}
<!-- Some filters are enabled. The feature to add might already be mapped, but hiddne -->
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
<div class="alert flex justify-center items-center">
<EyeOffIcon class="w-8" />
<Tr t={Translations.t.general.add.disableFiltersExplanation} />
@ -231,6 +249,16 @@
<Tr t={t.backToSelect} />
</div>
</SubtleButton>
{:else if _globalFilter.length > checkedOfGlobalFilters}
<Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} />
<SubtleButton on:click={() => {checkedOfGlobalFilters = checkedOfGlobalFilters + 1}}>
<img slot="image" src={_globalFilter[checkedOfGlobalFilters].onNewPoint?.icon ?? "./assets/svg/confirm.svg"} class="w-12 h-12">
<Tr slot="message" t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({preset: selectedPreset.preset})} />
</SubtleButton>
<SubtleButton on:click={() => {globalFilter.setData([]); abort()}}>
<img slot="image" src="./assets/svg/close.svg" class="w-8 h-8"/>
<Tr slot="message" t={Translations.t.general.cancel}/>
</SubtleButton>
{:else if !creating}
<NewPointLocationInput value={preciseCoordinate} snappedTo={snappedToObject} {state} {coordinate}
targetLayer={selectedPreset.layer}

View file

@ -13,9 +13,6 @@ import { OsmServiceState } from "../../Logic/Osm/OsmConnection"
* Generates all the questions, one by one
*/
export default class QuestionBox extends VariableUiElement {
public readonly skippedQuestions: UIEventSource<number[]>
public readonly restingQuestions: Store<BaseUIElement[]>
constructor(
state,
options: {
@ -29,10 +26,6 @@ export default class QuestionBox extends VariableUiElement {
const tagsSource = options.tagsSource
const units = options.units
options.showAllQuestionsAtOnce = options.showAllQuestionsAtOnce ?? false
const tagRenderings = options.tagRenderings
.filter((tr) => tr.question !== undefined)
.filter((tr) => tr.question !== null)
let focus: () => void = () => {}
@ -59,9 +52,6 @@ export default class QuestionBox extends VariableUiElement {
)
)
const skippedQuestionsButton = Translations.t.general.skippedQuestions.onClick(() => {
skippedQuestions.setData([])
})
tagsSource.map(
(tags) => {
if (tags === undefined) {
@ -136,18 +126,12 @@ export default class QuestionBox extends VariableUiElement {
els.push(allQuestions[0])
}
if (skippedQuestions.data.length > 0) {
els.push(skippedQuestionsButton)
}
return new Combine(els).SetClass("block mb-8")
},
[state.osmConnection.apiIsOnline]
)
)
this.skippedQuestions = skippedQuestions
this.restingQuestions = questionsToAsk
focus = () => this.ScrollIntoView()
}
}

View file

@ -72,6 +72,9 @@
let answered: number = 0;
let skipped: number = 0;
function focus(){
}
function skip(question: TagRenderingConfig, didAnswer: boolean = false) {
skippedQuestions.data.add(question.id);
skippedQuestions.ping();

View file

@ -5,7 +5,6 @@
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import MapControlButton from "./Base/MapControlButton.svelte";
import ToSvelte from "./Base/ToSvelte.svelte";
import Svg from "../Svg";
import If from "./Base/If.svelte";
import { GeolocationControl } from "./BigComponents/GeolocationControl";
import type { Feature } from "geojson";
@ -35,6 +34,7 @@
import { VariableUiElement } from "./Base/VariableUIElement";
import SvelteUIElement from "./Base/SvelteUIElement";
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
import LevelSelector from "./BigComponents/LevelSelector.svelte";
export let state: ThemeViewState;
let layout = state.layout;
@ -71,14 +71,14 @@
<div class="absolute top-0 left-0 w-full ">
<!-- Top components -->
<If condition={state.featureSwitches.featureSwitchSearch}>
<div class="sm:w-min float-right mt-1 px-1 sm:m-2 max-[320px]:w-full">
<div class="max-[480px]:w-full float-right mt-1 px-1 sm:m-2">
<Geosearch bounds={state.mapProperties.bounds} perLayer={state.perLayer} {selectedElement}
{selectedLayer}></Geosearch>
</div>
</If>
<div class="float-left m-1 sm:mt-2">
<MapControlButton on:click={() => state.guistate.themeIsOpened.setData(true)}>
<div class="flex m-0.5 mx-1 sm:mx-1 md:mx-2 items-center cursor-pointer">
<div class="flex m-0.5 mx-1 sm:mx-1 md:mx-2 items-center cursor-pointer max-[480px]:w-full">
<img class="w-4 h-4 sm:w-6 sm:h-6 md:w-8 md:h-8 block mr-0.5 sm:mr-1 md:mr-2" src={layout.icon}>
<b class="mr-1">
<Tr t={layout.title}></Tr>
@ -101,7 +101,12 @@
</div>
<div class="absolute bottom-0 right-0 mb-4 mr-4">
<div class="absolute bottom-0 right-0 mb-4 mr-4 flex flex-col items-end">
<If condition={state.floors.map(f => f.length > 1)}>
<div class="mr-0.5">
<LevelSelector floors={state.floors} layerState={state.layerState} zoom={state.mapProperties.zoom}/>
</div>
</If>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z+1)}>
<img src="./assets/svg/plus.svg" class="w-6 h-6 md:w-8 md:h-8"/>
</MapControlButton>

View file

@ -787,14 +787,14 @@ video {
margin: 0.5rem;
}
.m-4 {
margin: 1rem;
}
.m-1 {
margin: 0.25rem;
}
.m-4 {
margin: 1rem;
}
.m-6 {
margin: 1.5rem;
}
@ -857,10 +857,6 @@ video {
margin-top: 0.25rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.mr-0\.5 {
margin-right: 0.125rem;
}
@ -885,6 +881,10 @@ video {
margin-bottom: 6rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.ml-2 {
margin-left: 0.5rem;
}
@ -1146,6 +1146,11 @@ video {
width: 12rem;
}
.w-min {
width: -webkit-min-content;
width: min-content;
}
.w-auto {
width: auto;
}
@ -1260,6 +1265,10 @@ video {
align-content: flex-start;
}
.items-end {
align-items: flex-end;
}
.items-center {
align-items: center;
}
@ -2297,8 +2306,8 @@ input {
color: var(--unsubtle-detail-color-contrast);
}
@media (max-width: 320px) {
.max-\[320px\]\:w-full {
@media (max-width: 480px) {
.max-\[480px\]\:w-full {
width: 100%;
}
}
@ -2322,6 +2331,10 @@ input {
margin-right: auto;
}
.sm\:mt-2 {
margin-top: 0.5rem;
}
.sm\:mr-1 {
margin-right: 0.25rem;
}
@ -2350,11 +2363,6 @@ input {
width: 6rem;
}
.sm\:w-min {
width: -webkit-min-content;
width: min-content;
}
.sm\:w-6 {
width: 1.5rem;
}
@ -2415,10 +2423,6 @@ input {
margin: 0.25rem;
}
.md\:m-4 {
margin: 1rem;
}
.md\:m-8 {
margin: 2rem;
}

View file

@ -17,7 +17,7 @@
</head>
<body>
<div id="maindiv" class="w-full h-full">'maindiv' not attached</div>
<div id="maindiv" class="w-full">'maindiv' not attached</div>
<div id="extradiv">'extradiv' not attached</div>
<script type="module" src="./test.ts"></script>

12
test.ts
View file

@ -5,12 +5,13 @@ import Combine from "./UI/Base/Combine"
import SpecialVisualizations from "./UI/SpecialVisualizations"
import InputHelpers from "./UI/InputElement/InputHelpers"
import BaseUIElement from "./UI/BaseUIElement"
import { UIEventSource } from "./Logic/UIEventSource"
import { ImmutableStore, UIEventSource } from "./Logic/UIEventSource"
import { VariableUiElement } from "./UI/Base/VariableUIElement"
import { FixedUiElement } from "./UI/Base/FixedUiElement"
import Title from "./UI/Base/Title"
import SvelteUIElement from "./UI/Base/SvelteUIElement"
import ValidatedInput from "./UI/InputElement/ValidatedInput.svelte"
import LevelSelector from "./UI/InputElement/Helpers/LevelSelector.svelte"
function testspecial() {
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
@ -47,7 +48,14 @@ function testinput() {
new Combine(els).SetClass("flex flex-col").AttachTo("maindiv")
}
testinput()
function testElevator() {
const floors = new ImmutableStore(["0", "1", "1.5", "2"])
const value = new UIEventSource<string>(undefined)
new SvelteUIElement(LevelSelector, { floors, value }).AttachTo("maindiv")
new VariableUiElement(value).AttachTo("extradiv")
}
testElevator()
//testinput()
/*/
testspecial()
//*/