forked from MapComplete/MapComplete
refactoring
This commit is contained in:
parent
b94a8f5745
commit
5d0fe31c41
114 changed files with 2412 additions and 2958 deletions
13
UI/Base/Checkbox.svelte
Normal file
13
UI/Base/Checkbox.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource.js";
|
||||
|
||||
/**
|
||||
* For some stupid reason, it is very hard to bind inputs
|
||||
*/
|
||||
export let selected: UIEventSource<boolean>;
|
||||
let _c: boolean = selected.data ?? true;
|
||||
$: selected.setData(_c)
|
||||
|
||||
</script>
|
||||
|
||||
<input type="checkbox" bind:checked={_c} />
|
15
UI/Base/Dropdown.svelte
Normal file
15
UI/Base/Dropdown.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource.js";
|
||||
|
||||
/**
|
||||
* For some stupid reason, it is very hard to bind inputs
|
||||
*/
|
||||
export let value: UIEventSource<number>;
|
||||
let i: number = value.data;
|
||||
$: value.setData(i)
|
||||
|
||||
</script>
|
||||
|
||||
<select bind:value={i} >
|
||||
<slot></slot>
|
||||
</select>
|
|
@ -1,14 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
/**
|
||||
* For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here
|
||||
*/
|
||||
export let condition: UIEventSource<boolean>;
|
||||
let _c = condition.data;
|
||||
condition.addCallback(c => _c = c)
|
||||
onDestroy(condition.addCallback(c => {
|
||||
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`,
|
||||
which will _unregister_ the callback if `c = true`! */
|
||||
_c = c;
|
||||
return false
|
||||
}))
|
||||
|
||||
</script>
|
||||
|
||||
{#if _c}
|
||||
<slot></slot>
|
||||
{:else}
|
||||
<slot name="else"></slot>
|
||||
{/if}
|
||||
|
|
18
UI/Base/IfNot.svelte
Normal file
18
UI/Base/IfNot.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
/**
|
||||
* For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here
|
||||
*/
|
||||
export let condition: UIEventSource<boolean>;
|
||||
let _c = !condition.data;
|
||||
onDestroy(condition.addCallback(c => {
|
||||
_c = !c;
|
||||
return false
|
||||
}))
|
||||
</script>
|
||||
|
||||
{#if _c}
|
||||
<slot></slot>
|
||||
{/if}
|
13
UI/Base/Loading.svelte
Normal file
13
UI/Base/Loading.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
import ToSvelte from "./ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
</script>
|
||||
|
||||
<div class="pl-2 p-1 flex">
|
||||
<div class="animate-spin self-center w-6 h-6 min-w-6">
|
||||
<ToSvelte construct={Svg.loading_ui}></ToSvelte>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
|
@ -8,6 +8,6 @@
|
|||
</script>
|
||||
|
||||
|
||||
<div on:click={e => dispatch("click", e)} class="subtle-background block rounded-full min-w-10 h-10 pointer-events-auto m-0.5 md:m-1 p-1">
|
||||
<div on:click={e => dispatch("click", e)} class="subtle-background rounded-full min-w-10 w-fit h-10 m-0.5 md:m-1 p-1">
|
||||
<slot class="m-4"></slot>
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
<script lang="ts">
|
||||
import BaseUIElement from "../BaseUIElement.js"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
export let construct: BaseUIElement | (() => BaseUIElement)
|
||||
let elem: HTMLElement
|
||||
import BaseUIElement from "../BaseUIElement.js";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
|
||||
export let construct: BaseUIElement | (() => BaseUIElement);
|
||||
let elem: HTMLElement;
|
||||
let html: HTMLElement;
|
||||
onMount(() => {
|
||||
let html =
|
||||
typeof construct === "function"
|
||||
? construct().ConstructElement()
|
||||
: construct.ConstructElement()
|
||||
const uiElem = typeof construct === "function"
|
||||
? construct() : construct;
|
||||
html =uiElem?.ConstructElement();
|
||||
if (html !== undefined) {
|
||||
elem.replaceWith(html);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
html?.remove();
|
||||
});
|
||||
|
||||
elem.replaceWith(html)
|
||||
})
|
||||
</script>
|
||||
|
||||
<span bind:this={elem} />
|
||||
|
|
|
@ -13,6 +13,7 @@ import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
|
|||
import Toggle from "../Input/Toggle"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
import DefaultGUI from "../DefaultGUI"
|
||||
|
||||
export class BackToThemeOverview extends Toggle {
|
||||
constructor(
|
||||
|
@ -42,6 +43,7 @@ export class ActionButtons extends Combine {
|
|||
readonly locationControl: Store<Loc>
|
||||
readonly osmConnection: OsmConnection
|
||||
readonly featureSwitchMoreQuests: Store<boolean>
|
||||
readonly defaultGuiState: DefaultGuiState
|
||||
}) {
|
||||
const imgSize = "h-6 w-6"
|
||||
const iconStyle = "height: 1.5rem; width: 1.5rem"
|
||||
|
@ -82,8 +84,8 @@ export class ActionButtons extends Combine {
|
|||
Translations.t.translations.activateButton
|
||||
).onClick(() => {
|
||||
ScrollableFullScreen.collapse()
|
||||
DefaultGuiState.state.userInfoIsOpened.setData(true)
|
||||
DefaultGuiState.state.userInfoFocusedQuestion.setData("translation-mode")
|
||||
state.defaultGuiState.userInfoIsOpened.setData(true)
|
||||
state.defaultGuiState.userInfoFocusedQuestion.setData("translation-mode")
|
||||
}),
|
||||
])
|
||||
this.SetClass("block w-full link-no-underline")
|
||||
|
|
|
@ -14,54 +14,53 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
|||
import Title from "../Base/Title"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import Loc from "../../Models/Loc"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Constants from "../../Models/Constants"
|
||||
import ContributorCount from "../../Logic/ContributorCount"
|
||||
import Img from "../Base/Img"
|
||||
import { TypedTranslation } from "../i18n/Translation"
|
||||
import GeoIndexedStore from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
|
||||
export class OpenIdEditor extends VariableUiElement {
|
||||
constructor(
|
||||
state: { readonly locationControl: Store<Loc> },
|
||||
mapProperties: { location: Store<{ lon: number; lat: number }>; zoom: Store<number> },
|
||||
iconStyle?: string,
|
||||
objectId?: string
|
||||
) {
|
||||
const t = Translations.t.general.attribution
|
||||
super(
|
||||
state.locationControl.map((location) => {
|
||||
let elementSelect = ""
|
||||
if (objectId !== undefined) {
|
||||
const parts = objectId.split("/")
|
||||
const tp = parts[0]
|
||||
if (
|
||||
parts.length === 2 &&
|
||||
!isNaN(Number(parts[1])) &&
|
||||
(tp === "node" || tp === "way" || tp === "relation")
|
||||
) {
|
||||
elementSelect = "&" + tp + "=" + parts[1]
|
||||
mapProperties.location.map(
|
||||
(location) => {
|
||||
let elementSelect = ""
|
||||
if (objectId !== undefined) {
|
||||
const parts = objectId.split("/")
|
||||
const tp = parts[0]
|
||||
if (
|
||||
parts.length === 2 &&
|
||||
!isNaN(Number(parts[1])) &&
|
||||
(tp === "node" || tp === "way" || tp === "relation")
|
||||
) {
|
||||
elementSelect = "&" + tp + "=" + parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${
|
||||
location?.zoom ?? 0
|
||||
}/${location?.lat ?? 0}/${location?.lon ?? 0}`
|
||||
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {
|
||||
url: idLink,
|
||||
newTab: true,
|
||||
})
|
||||
})
|
||||
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${
|
||||
mapProperties.zoom?.data ?? 0
|
||||
}/${location?.lat ?? 0}/${location?.lon ?? 0}`
|
||||
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {
|
||||
url: idLink,
|
||||
newTab: true,
|
||||
})
|
||||
},
|
||||
[mapProperties.zoom]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenJosm extends Combine {
|
||||
constructor(
|
||||
state: { osmConnection: OsmConnection; currentBounds: Store<BBox> },
|
||||
iconStyle?: string
|
||||
) {
|
||||
constructor(osmConnection: OsmConnection, bounds: Store<BBox>, iconStyle?: string) {
|
||||
const t = Translations.t.general.attribution
|
||||
|
||||
const josmState = new UIEventSource<string>(undefined)
|
||||
|
@ -83,21 +82,21 @@ export class OpenJosm extends Combine {
|
|||
|
||||
const toggle = new Toggle(
|
||||
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => {
|
||||
const bounds: any = state.currentBounds.data
|
||||
if (bounds === undefined) {
|
||||
return undefined
|
||||
const bbox = bounds.data
|
||||
if (bbox === undefined) {
|
||||
return
|
||||
}
|
||||
const top = bounds.getNorth()
|
||||
const bottom = bounds.getSouth()
|
||||
const right = bounds.getEast()
|
||||
const left = bounds.getWest()
|
||||
const top = bbox.getNorth()
|
||||
const bottom = bbox.getSouth()
|
||||
const right = bbox.getEast()
|
||||
const left = bbox.getWest()
|
||||
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
|
||||
Utils.download(josmLink)
|
||||
.then((answer) => josmState.setData(answer.replace(/\n/g, "").trim()))
|
||||
.catch((_) => josmState.setData("ERROR"))
|
||||
}),
|
||||
undefined,
|
||||
state.osmConnection.userDetails.map(
|
||||
osmConnection.userDetails.map(
|
||||
(ud) => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible
|
||||
)
|
||||
)
|
||||
|
@ -113,14 +112,14 @@ export default class CopyrightPanel extends Combine {
|
|||
private static LicenseObject = CopyrightPanel.GenerateLicenses()
|
||||
|
||||
constructor(state: {
|
||||
layoutToUse: LayoutConfig
|
||||
featurePipeline: FeaturePipeline
|
||||
currentBounds: Store<BBox>
|
||||
locationControl: UIEventSource<Loc>
|
||||
layout: LayoutConfig
|
||||
bounds: Store<BBox>
|
||||
osmConnection: OsmConnection
|
||||
dataIsLoading: Store<boolean>
|
||||
perLayer: ReadonlyMap<string, GeoIndexedStore>
|
||||
}) {
|
||||
const t = Translations.t.general.attribution
|
||||
const layoutToUse = state.layoutToUse
|
||||
const layoutToUse = state.layout
|
||||
|
||||
const iconAttributions: BaseUIElement[] = layoutToUse.usedImages.map(
|
||||
CopyrightPanel.IconAttribution
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import Translations from "../i18n/Translations"
|
||||
import State from "../../State"
|
||||
import { Utils } from "../../Utils"
|
||||
import Combine from "../Base/Combine"
|
||||
import CheckBoxes from "../Input/Checkboxes"
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import { FixedInputElement } from "../Input/FixedInputElement"
|
||||
import { RadioButton } from "../Input/RadioButton"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Toggle, { ClickableToggle } from "../Input/Toggle"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Svg from "../../Svg"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
|
||||
import BackgroundSelector from "./BackgroundSelector"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
|
||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
|
||||
import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
||||
|
@ -18,9 +15,7 @@ import ValidatedTextField from "../Input/ValidatedTextField"
|
|||
import { QueryParameters } from "../../Logic/Web/QueryParameters"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import { InputElement } from "../Input/InputElement"
|
||||
import { DropDown } from "../Input/DropDown"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { BackToThemeOverview } from "./ActionButtons"
|
||||
|
||||
|
@ -272,102 +267,6 @@ export class LayerFilterPanel extends Combine {
|
|||
return [tr, settableFilter]
|
||||
}
|
||||
|
||||
private static createCheckboxFilter(
|
||||
filterConfig: FilterConfig
|
||||
): [BaseUIElement, UIEventSource<FilterState>] {
|
||||
let option = filterConfig.options[0]
|
||||
|
||||
const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6")
|
||||
const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6")
|
||||
const qp = QueryParameters.GetBooleanQueryParameter(
|
||||
"filter-" + filterConfig.id,
|
||||
false,
|
||||
"Is filter '" + filterConfig.options[0].question.textFor("en") + " enabled?"
|
||||
)
|
||||
const toggle = new ClickableToggle(
|
||||
new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"),
|
||||
new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass(
|
||||
"flex"
|
||||
),
|
||||
qp
|
||||
)
|
||||
.ToggleOnClick()
|
||||
.SetClass("block m-1")
|
||||
|
||||
return [
|
||||
toggle,
|
||||
toggle.isEnabled.sync(
|
||||
(enabled) =>
|
||||
enabled
|
||||
? {
|
||||
currentFilter: option.osmTags,
|
||||
state: "true",
|
||||
}
|
||||
: undefined,
|
||||
[],
|
||||
(f) => f !== undefined
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
private static createMultiFilter(
|
||||
filterConfig: FilterConfig
|
||||
): [BaseUIElement, UIEventSource<FilterState>] {
|
||||
let options = filterConfig.options
|
||||
|
||||
const values: FilterState[] = options.map((f, i) => ({
|
||||
currentFilter: f.osmTags,
|
||||
state: i,
|
||||
}))
|
||||
let filterPicker: InputElement<number>
|
||||
const value = QueryParameters.GetQueryParameter(
|
||||
"filter-" + filterConfig.id,
|
||||
"0",
|
||||
"Value for filter " + filterConfig.id
|
||||
).sync(
|
||||
(str) => Number(str),
|
||||
[],
|
||||
(n) => "" + n
|
||||
)
|
||||
|
||||
if (options.length <= 6) {
|
||||
filterPicker = new RadioButton(
|
||||
options.map(
|
||||
(option, i) =>
|
||||
new FixedInputElement(option.question.Clone().SetClass("block"), i)
|
||||
),
|
||||
{
|
||||
value,
|
||||
dontStyle: true,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
filterPicker = new DropDown(
|
||||
"",
|
||||
options.map((option, i) => ({
|
||||
value: i,
|
||||
shown: option.question.Clone(),
|
||||
})),
|
||||
value
|
||||
)
|
||||
}
|
||||
|
||||
return [
|
||||
filterPicker,
|
||||
filterPicker.GetValue().sync(
|
||||
(i) => values[i],
|
||||
[],
|
||||
(selected) => {
|
||||
const v = selected?.state
|
||||
if (v === undefined || typeof v === "string") {
|
||||
return undefined
|
||||
}
|
||||
return v
|
||||
}
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
private static createFilter(
|
||||
state: {},
|
||||
filterConfig: FilterConfig
|
||||
|
@ -376,12 +275,6 @@ export class LayerFilterPanel extends Combine {
|
|||
return LayerFilterPanel.createFilterWithFields(state, filterConfig)
|
||||
}
|
||||
|
||||
if (filterConfig.options.length === 1) {
|
||||
return LayerFilterPanel.createCheckboxFilter(filterConfig)
|
||||
}
|
||||
|
||||
const filter = LayerFilterPanel.createMultiFilter(filterConfig)
|
||||
filter[0].SetClass("pl-2")
|
||||
return filter
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
79
UI/BigComponents/Filterview.svelte
Normal file
79
UI/BigComponents/Filterview.svelte
Normal file
|
@ -0,0 +1,79 @@
|
|||
<script lang="ts">/**
|
||||
* The FilterView shows the various options to enable/disable a single layer.
|
||||
*/
|
||||
import type FilteredLayer from "../../Models/FilteredLayer";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import Checkbox from "../Base/Checkbox.svelte";
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
|
||||
import type { Writable } from "svelte/store";
|
||||
import If from "../Base/If.svelte";
|
||||
import Dropdown from "../Base/Dropdown.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let filteredLayer: FilteredLayer;
|
||||
export let zoomlevel: number;
|
||||
let layer: LayerConfig = filteredLayer.layerDef;
|
||||
let isDisplayed: boolean = filteredLayer.isDisplayed.data;
|
||||
onDestroy(filteredLayer.isDisplayed.addCallbackAndRunD(d => {
|
||||
isDisplayed = d;
|
||||
return false
|
||||
}));
|
||||
|
||||
/**
|
||||
* Gets a UIEventSource as boolean for the given option, to be used with a checkbox
|
||||
*/
|
||||
function getBooleanStateFor(option: FilterConfig): Writable<boolean> {
|
||||
const state = filteredLayer.appliedFilters.get(option.id);
|
||||
return state.sync(f => f === 0, [], (b) => b ? 0 : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a UIEventSource as number for the given option, to be used with a dropdown or radiobutton
|
||||
*/
|
||||
function getStateFor(option: FilterConfig): Writable<number> {
|
||||
return filteredLayer.appliedFilters.get(option.id);
|
||||
}
|
||||
</script>
|
||||
{#if filteredLayer.layerDef.name}
|
||||
<div>
|
||||
<label class="flex gap-1">
|
||||
<Checkbox selected={filteredLayer.isDisplayed} />
|
||||
<If condition={filteredLayer.isDisplayed}>
|
||||
<ToSvelte construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6")}></ToSvelte>
|
||||
<ToSvelte slot="else" construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 opacity-50")}></ToSvelte>
|
||||
</If>
|
||||
|
||||
{filteredLayer.layerDef.name}
|
||||
</label>
|
||||
<If condition={filteredLayer.isDisplayed}>
|
||||
<div id="subfilters" class="flex flex-col gap-y-1 mb-4 ml-4">
|
||||
{#each filteredLayer.layerDef.filters as filter}
|
||||
<div>
|
||||
|
||||
<!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with fields -->
|
||||
{#if filter.options.length === 1 && filter.options[0].fields.length === 0}
|
||||
<label>
|
||||
<Checkbox selected={getBooleanStateFor(filter)} />
|
||||
{filter.options[0].question}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
{#if filter.options.length > 1}
|
||||
<Dropdown value={getStateFor(filter)}>
|
||||
{#each filter.options as option, i}
|
||||
<option value={i}>
|
||||
{ option.question}
|
||||
</option>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</If>
|
||||
|
||||
</div>
|
||||
{/if}
|
|
@ -1,8 +1,7 @@
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Svg from "../../Svg"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
@ -94,14 +93,13 @@ export class GeolocationControl extends VariableUiElement {
|
|||
return
|
||||
}
|
||||
|
||||
if (geolocationState.currentGPSLocation.data === undefined) {
|
||||
// A location _is_ known! Let's move to this location
|
||||
const currentLocation = geolocationState.currentGPSLocation.data
|
||||
if (currentLocation === undefined) {
|
||||
// No location is known yet, not much we can do
|
||||
lastClick.setData(new Date())
|
||||
return
|
||||
}
|
||||
|
||||
// A location _is_ known! Let's move to this location
|
||||
const currentLocation = geolocationState.currentGPSLocation.data
|
||||
const inBounds = state.bounds.data.contains([
|
||||
currentLocation.longitude,
|
||||
currentLocation.latitude,
|
||||
|
|
94
UI/BigComponents/Geosearch.svelte
Normal file
94
UI/BigComponents/Geosearch.svelte
Normal file
|
@ -0,0 +1,94 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import type { Feature } from "geojson";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import Svg from "../../Svg.js";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
import Hotkeys from "../Base/Hotkeys";
|
||||
import { Geocoding } from "../../Logic/Osm/Geocoding";
|
||||
import { BBox } from "../../Logic/BBox";
|
||||
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore";
|
||||
|
||||
Translations.t;
|
||||
export let bounds: UIEventSource<BBox>
|
||||
export let layout: LayoutConfig;
|
||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||
export let selectedElement: UIEventSource<Feature>;
|
||||
export let selectedLayer: UIEventSource<LayerConfig>;
|
||||
|
||||
let searchContents: string = undefined;
|
||||
|
||||
let isRunning: boolean = false;
|
||||
|
||||
let inputElement: HTMLInputElement;
|
||||
|
||||
let feedback: string = undefined
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ ctrl: "F" },
|
||||
Translations.t.hotkeyDocumentation.selectSearch,
|
||||
() => {
|
||||
inputElement?.focus()
|
||||
inputElement?.select()
|
||||
}
|
||||
)
|
||||
async function performSearch() {
|
||||
try {
|
||||
isRunning = true;
|
||||
searchContents = searchContents?.trim() ?? ""
|
||||
if (searchContents === "") {
|
||||
return
|
||||
}
|
||||
const result = await Geocoding.Search(searchContents, bounds.data)
|
||||
if (result.length == 0) {
|
||||
feedback = Translations.t.search.nothing.txt
|
||||
return
|
||||
}
|
||||
const poi = result[0]
|
||||
const [lat0, lat1, lon0, lon1] = poi.boundingbox
|
||||
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01))
|
||||
const id = poi.osm_type + "/" + poi.osm_id
|
||||
const layers = Array.from(perLayer.values())
|
||||
for (const layer of layers) {
|
||||
const found = layer.features.data.find(f => f.properties.id === id)
|
||||
selectedElement.setData(found)
|
||||
selectedLayer.setData(layer.layer.layerDef)
|
||||
|
||||
}
|
||||
}catch (e) {
|
||||
console.error(e)
|
||||
feedback = Translations.t.search.error.txt
|
||||
} finally {
|
||||
isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex normal-background rounded-full pl-2">
|
||||
<form>
|
||||
|
||||
{#if isRunning}
|
||||
<Loading>{Translations.t.general.search.searching}</Loading>
|
||||
{:else if feedback !== undefined}
|
||||
<div class="alert" on:click={() => feedback = undefined}>
|
||||
{feedback}
|
||||
</div>
|
||||
{:else }
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
on:keypress={keypr => keypr.key === "Enter" ? performSearch() : undefined}
|
||||
|
||||
bind:value={searchContents}
|
||||
placeholder={Translations.t.general.search.search}>
|
||||
{/if}
|
||||
|
||||
</form>
|
||||
<div class="w-6 h-6" on:click={performSearch}>
|
||||
<ToSvelte construct={Svg.search_ui}></ToSvelte>
|
||||
</div>
|
||||
</div>
|
|
@ -1,12 +1,6 @@
|
|||
import Combine from "../Base/Combine"
|
||||
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 FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import LevelSelector from "./LevelSelector"
|
||||
import { GeolocationControl } from "./GeolocationControl"
|
||||
|
||||
export default class RightControls extends Combine {
|
||||
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {
|
||||
|
|
75
UI/BigComponents/SelectedElementView.svelte
Normal file
75
UI/BigComponents/SelectedElementView.svelte
Normal file
|
@ -0,0 +1,75 @@
|
|||
<script lang="ts">
|
||||
import type { Feature } from "geojson";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import TagRenderingAnswer from "../Popup/TagRenderingAnswer";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import { VariableUiElement } from "../Base/VariableUIElement.js";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let selectedElement: UIEventSource<Feature>;
|
||||
export let layer: UIEventSource<LayerConfig>;
|
||||
export let tags: Store<UIEventSource<Record<string, string>>>;
|
||||
let _tags: UIEventSource<Record<string, string>>;
|
||||
onDestroy(tags.subscribe(tags => {
|
||||
_tags = tags;
|
||||
return false
|
||||
}));
|
||||
|
||||
export let specialVisState: SpecialVisualizationState;
|
||||
|
||||
/**
|
||||
* const title = new TagRenderingAnswer(
|
||||
* tags,
|
||||
* layerConfig.title ?? new TagRenderingConfig("POI"),
|
||||
* state
|
||||
* ).SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2 text-2xl")
|
||||
* 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 titleicon"
|
||||
* )
|
||||
* })
|
||||
* ).SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
|
||||
*
|
||||
* return new Combine([
|
||||
* new Combine([title, titleIcons]).SetClass(
|
||||
* "flex flex-col sm:flex-row flex-grow justify-between"
|
||||
* ),
|
||||
* ])
|
||||
*/
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div on:click={() =>selectedElement.setData(undefined)}>close</div>
|
||||
<div class="flex flex-col sm:flex-row flex-grow justify-between">
|
||||
<!-- Title element-->
|
||||
<ToSvelte
|
||||
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, layer.data.title, specialVisState), [layer]))}></ToSvelte>
|
||||
|
||||
<div class="flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2">
|
||||
|
||||
{#each $layer.titleIcons as titleIconConfig (titleIconConfig.id)}
|
||||
<div class="w-8 h-8">
|
||||
<ToSvelte
|
||||
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, titleIconConfig, specialVisState)))}></ToSvelte>
|
||||
</div>
|
||||
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
|
||||
{#each Object.keys($_tags) as key}
|
||||
<li><b>{key}</b>=<b>{$_tags[key]}</b></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
|
@ -4,20 +4,14 @@ 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"
|
||||
import { FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
|
||||
export default class StatisticsPanel extends VariableUiElement {
|
||||
constructor(
|
||||
elementsInview: UIEventSource<{ element: OsmFeature; layer: LayerConfig }[]>,
|
||||
state: {
|
||||
layoutToUse: LayoutConfig
|
||||
}
|
||||
) {
|
||||
export default class StatisticsForLayerPanel extends VariableUiElement {
|
||||
constructor(elementsInview: FeatureSourceForLayer) {
|
||||
const layer = elementsInview.layer.layerDef
|
||||
super(
|
||||
elementsInview.stabilized(1000).map(
|
||||
elementsInview.features.stabilized(1000).map(
|
||||
(features) => {
|
||||
if (features === undefined) {
|
||||
return new Loading("Loading data")
|
||||
|
@ -25,40 +19,33 @@ export default class StatisticsPanel extends VariableUiElement {
|
|||
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"))
|
||||
const els: BaseUIElement[] = []
|
||||
const featuresForLayer = features
|
||||
if (featuresForLayer.length === 0) {
|
||||
return
|
||||
}
|
||||
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]
|
||||
|
|
|
@ -11,6 +11,7 @@ import LoggedInUserIndicator from "../LoggedInUserIndicator"
|
|||
import { ActionButtons } from "./ActionButtons"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
|
||||
export default class ThemeIntroductionPanel extends Combine {
|
||||
constructor(
|
||||
|
@ -24,6 +25,7 @@ export default class ThemeIntroductionPanel extends Combine {
|
|||
osmConnection: OsmConnection
|
||||
currentBounds: Store<BBox>
|
||||
locationControl: UIEventSource<Loc>
|
||||
defaultGuiState: DefaultGuiState
|
||||
},
|
||||
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
|
||||
) {
|
||||
|
|
|
@ -17,7 +17,7 @@ export default class UploadTraceToOsmUI extends LoginToggle {
|
|||
constructor(
|
||||
trace: (title: string) => string,
|
||||
state: {
|
||||
layoutToUse: LayoutConfig
|
||||
layout: LayoutConfig
|
||||
osmConnection: OsmConnection
|
||||
readonly featureSwitchUserbadge: Store<boolean>
|
||||
},
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
|
||||
import State from "../State"
|
||||
import { Utils } from "../Utils"
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs"
|
||||
|
@ -11,14 +10,11 @@ import BaseUIElement from "./BaseUIElement"
|
|||
import LeftControls from "./BigComponents/LeftControls"
|
||||
import RightControls from "./BigComponents/RightControls"
|
||||
import CenterMessageBox from "./CenterMessageBox"
|
||||
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"
|
||||
import ScrollableFullScreen from "./Base/ScrollableFullScreen"
|
||||
import Translations from "./i18n/Translations"
|
||||
import SimpleAddUI from "./BigComponents/SimpleAddUI"
|
||||
import StrayClickHandler from "../Logic/Actors/StrayClickHandler"
|
||||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import home_location_json from "../assets/layers/home_location/home_location.json"
|
||||
import NewNoteUi from "./Popup/NewNoteUi"
|
||||
import Combine from "./Base/Combine"
|
||||
import AddNewMarker from "./BigComponents/AddNewMarker"
|
||||
|
@ -32,7 +28,6 @@ import { FixedUiElement } from "./Base/FixedUiElement"
|
|||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
import { GeoLocationState } from "../Logic/State/GeoLocationState"
|
||||
import Hotkeys from "./Base/Hotkeys"
|
||||
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
|
||||
|
@ -50,9 +45,6 @@ export default class DefaultGUI {
|
|||
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
|
||||
this.state = state
|
||||
this.guiState = guiState
|
||||
if (this.state.featureSwitchGeolocation.data) {
|
||||
this.geolocationHandler = new GeoLocationHandler(new GeoLocationState(), state)
|
||||
}
|
||||
}
|
||||
|
||||
public setup() {
|
||||
|
@ -74,10 +66,6 @@ export default class DefaultGUI {
|
|||
this.state.backgroundLayer.setData(AvailableBaseLayers.osmCarto)
|
||||
}
|
||||
)
|
||||
|
||||
Utils.downloadJson("./service-worker-version")
|
||||
.then((data) => console.log("Service worker", data))
|
||||
.catch((_) => console.log("Service worker not active"))
|
||||
}
|
||||
|
||||
public setupClickDialogOnMap(
|
||||
|
@ -173,13 +161,6 @@ export default class DefaultGUI {
|
|||
|
||||
this.setupClickDialogOnMap(guiState.filterViewIsOpened, state)
|
||||
|
||||
new ShowDataLayer({
|
||||
leafletMap: state.leafletMap,
|
||||
layerToShow: new LayerConfig(home_location_json, "home_location", true),
|
||||
features: state.homeLocation,
|
||||
state,
|
||||
})
|
||||
|
||||
const selectedElement: FilteredLayer = state.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === "selected_element"
|
||||
)[0]
|
||||
|
@ -285,23 +266,6 @@ export default class DefaultGUI {
|
|||
.SetClass("flex items-center justify-center normal-background h-full")
|
||||
.AttachTo("on-small-screen")
|
||||
|
||||
new Combine([
|
||||
Toggle.If(state.featureSwitchSearch, () => {
|
||||
const search = new SearchAndGo(state).SetClass(
|
||||
"shadow rounded-full h-min w-full overflow-hidden sm:max-w-sm pointer-events-auto"
|
||||
)
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ ctrl: "F" },
|
||||
Translations.t.hotkeyDocumentation.selectSearch,
|
||||
() => {
|
||||
search.focus()
|
||||
}
|
||||
)
|
||||
|
||||
return search
|
||||
}),
|
||||
]).AttachTo("top-right")
|
||||
|
||||
new LeftControls(state, guiState).AttachTo("bottom-left")
|
||||
new RightControls(state, this.geolocationHandler).AttachTo("bottom-right")
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
||||
import Hash from "../Logic/Web/Hash"
|
||||
|
||||
export class DefaultGuiState {
|
||||
static state: DefaultGuiState
|
||||
|
||||
public readonly welcomeMessageIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(
|
||||
false
|
||||
)
|
||||
|
||||
public readonly menuIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
|
||||
public readonly downloadControlIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(
|
||||
false
|
||||
)
|
||||
|
@ -22,25 +22,17 @@ export class DefaultGuiState {
|
|||
public readonly userInfoFocusedQuestion: UIEventSource<string> = new UIEventSource<string>(
|
||||
undefined
|
||||
)
|
||||
public readonly welcomeMessageOpenedTab: UIEventSource<number>
|
||||
|
||||
private readonly sources: Record<string, UIEventSource<boolean>> = {
|
||||
welcome: this.welcomeMessageIsOpened,
|
||||
download: this.downloadControlIsOpened,
|
||||
filters: this.filterViewIsOpened,
|
||||
copyright: this.copyrightViewIsOpened,
|
||||
currentview: this.currentViewControlIsOpened,
|
||||
userinfo: this.userInfoIsOpened,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.welcomeMessageOpenedTab = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
"tab",
|
||||
"0",
|
||||
`The tab that is shown in the welcome-message.`
|
||||
)
|
||||
)
|
||||
const sources = {
|
||||
welcome: this.welcomeMessageIsOpened,
|
||||
download: this.downloadControlIsOpened,
|
||||
filters: this.filterViewIsOpened,
|
||||
copyright: this.copyrightViewIsOpened,
|
||||
currentview: this.currentViewControlIsOpened,
|
||||
userinfo: this.userInfoIsOpened,
|
||||
}
|
||||
|
||||
const self = this
|
||||
this.userInfoIsOpened.addCallback((isOpen) => {
|
||||
if (!isOpen) {
|
||||
|
@ -49,10 +41,16 @@ export class DefaultGuiState {
|
|||
}
|
||||
})
|
||||
|
||||
sources[Hash.hash.data?.toLowerCase()]?.setData(true)
|
||||
this.sources[Hash.hash.data?.toLowerCase()]?.setData(true)
|
||||
|
||||
if (Hash.hash.data === "" || Hash.hash.data === undefined) {
|
||||
this.welcomeMessageIsOpened.setData(true)
|
||||
}
|
||||
}
|
||||
|
||||
public closeAll() {
|
||||
for (const sourceKey in this.sources) {
|
||||
this.sources[sourceKey].setData(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export default class DeleteImage extends Toggle {
|
|||
constructor(
|
||||
key: string,
|
||||
tags: Store<any>,
|
||||
state: { layoutToUse: LayoutConfig; changes?: Changes; osmConnection?: OsmConnection }
|
||||
state: { layout: LayoutConfig; changes?: Changes; osmConnection?: OsmConnection }
|
||||
) {
|
||||
const oldValue = tags.data[key]
|
||||
const isDeletedBadge = Translations.t.image.isDeleted
|
||||
|
@ -24,7 +24,7 @@ export default class DeleteImage extends Toggle {
|
|||
await state?.changes?.applyAction(
|
||||
new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, {
|
||||
changeType: "delete-image",
|
||||
theme: state.layoutToUse.id,
|
||||
theme: state.layout.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
@ -39,7 +39,7 @@ export default class DeleteImage extends Toggle {
|
|||
await state?.changes?.applyAction(
|
||||
new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, {
|
||||
changeType: "answer",
|
||||
theme: state.layoutToUse.id,
|
||||
theme: state.layout.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
|
|
@ -14,7 +14,7 @@ export class ImageCarousel extends Toggle {
|
|||
constructor(
|
||||
images: Store<{ key: string; url: string; provider: ImageProvider }[]>,
|
||||
tags: Store<any>,
|
||||
state: { osmConnection?: OsmConnection; changes?: Changes; layoutToUse: LayoutConfig }
|
||||
state: { osmConnection?: OsmConnection; changes?: Changes; layout: LayoutConfig }
|
||||
) {
|
||||
const uiElements = images.map(
|
||||
(imageURLS: { key: string; url: string; provider: ImageProvider }[]) => {
|
||||
|
|
|
@ -11,26 +11,19 @@ import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
|||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import Loading from "../Base/Loading"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
|
||||
|
||||
constructor(
|
||||
tagsSource: Store<any>,
|
||||
state: {
|
||||
osmConnection: OsmConnection
|
||||
layoutToUse: LayoutConfig
|
||||
changes: Changes
|
||||
featureSwitchUserbadge: Store<boolean>
|
||||
},
|
||||
state: SpecialVisualizationState,
|
||||
imagePrefix: string = "image",
|
||||
text: string = undefined
|
||||
) {
|
||||
|
@ -56,7 +49,7 @@ export class ImageUploadFlow extends Toggle {
|
|||
await state.changes.applyAction(
|
||||
new ChangeTagAction(tags.id, new Tag(key, url), tagsSource.data, {
|
||||
changeType: "add-image",
|
||||
theme: state.layoutToUse.id,
|
||||
theme: state.layout.id,
|
||||
})
|
||||
)
|
||||
console.log("Adding image:" + key, url)
|
||||
|
@ -111,7 +104,7 @@ export class ImageUploadFlow extends Toggle {
|
|||
|
||||
const tags = tagsSource.data
|
||||
|
||||
const layout = state?.layoutToUse
|
||||
const layout = state?.layout
|
||||
let matchingLayer: LayerConfig = undefined
|
||||
for (const layer of layout?.layers ?? []) {
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
|
|
|
@ -1,31 +1,30 @@
|
|||
import { InputElement } from "./InputElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import Svg from "../../Svg"
|
||||
import { Utils } from "../../Utils"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import Minimap, { MinimapObj } from "../Base/Minimap"
|
||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
|
||||
/**
|
||||
* Selects a length after clicking on the minimap, in meters
|
||||
*/
|
||||
export default class LengthInput extends InputElement<string> {
|
||||
private readonly _location: UIEventSource<Loc>
|
||||
private readonly _location: Store<Loc>
|
||||
private readonly value: UIEventSource<string>
|
||||
private readonly background: UIEventSource<any>
|
||||
private readonly background: Store<RasterLayerPolygon>
|
||||
|
||||
constructor(
|
||||
mapBackground: UIEventSource<any>,
|
||||
location: UIEventSource<Loc>,
|
||||
mapBackground?: UIEventSource<RasterLayerPolygon>,
|
||||
value?: UIEventSource<string>
|
||||
) {
|
||||
super()
|
||||
this._location = location
|
||||
this.value = value ?? new UIEventSource<string>(undefined)
|
||||
this.background = mapBackground
|
||||
this.background = mapBackground ?? new ImmutableStore(AvailableRasterLayers.osmCarto)
|
||||
this.SetClass("block")
|
||||
}
|
||||
|
||||
|
@ -41,28 +40,26 @@ export default class LengthInput extends InputElement<string> {
|
|||
protected InnerConstructElement(): HTMLElement {
|
||||
let map: BaseUIElement & MinimapObj = undefined
|
||||
let layerControl: BaseUIElement = undefined
|
||||
if (!Utils.runningFromConsole) {
|
||||
map = Minimap.createMiniMap({
|
||||
background: this.background,
|
||||
allowMoving: false,
|
||||
location: this._location,
|
||||
attribution: true,
|
||||
leafletOptions: {
|
||||
tap: true,
|
||||
},
|
||||
})
|
||||
map = Minimap.createMiniMap({
|
||||
background: this.background,
|
||||
allowMoving: false,
|
||||
location: this._location,
|
||||
attribution: true,
|
||||
leafletOptions: {
|
||||
tap: true,
|
||||
},
|
||||
})
|
||||
|
||||
layerControl = new BackgroundMapSwitch(
|
||||
{
|
||||
locationControl: this._location,
|
||||
backgroundLayer: this.background,
|
||||
},
|
||||
this.background,
|
||||
{
|
||||
allowedCategories: ["map", "photo"],
|
||||
}
|
||||
)
|
||||
}
|
||||
layerControl = new BackgroundMapSwitch(
|
||||
{
|
||||
locationControl: this._location,
|
||||
backgroundLayer: this.background,
|
||||
},
|
||||
this.background,
|
||||
{
|
||||
allowedCategories: ["map", "photo"],
|
||||
}
|
||||
)
|
||||
const crosshair = new Combine([
|
||||
Svg.length_crosshair_svg().SetStyle(
|
||||
`position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as EmailValidator from "email-validator"
|
|||
import { parsePhoneNumberFromString } from "libphonenumber-js"
|
||||
import { InputElement } from "./InputElement"
|
||||
import { TextField } from "./TextField"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import CombinedInputElement from "./CombinedInputElement"
|
||||
import SimpleDatePicker from "./SimpleDatePicker"
|
||||
import OpeningHoursInput from "../OpeningHours/OpeningHoursInput"
|
||||
|
@ -25,6 +25,7 @@ import InputElementMap from "./InputElementMap"
|
|||
import Translations from "../i18n/Translations"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
|
||||
export class TextFieldDef {
|
||||
public readonly name: string
|
||||
|
@ -638,7 +639,7 @@ class LengthTextField extends TextFieldDef {
|
|||
location?: [number, number]
|
||||
args?: string[]
|
||||
feature?: any
|
||||
mapBackgroundLayer?: Store<BaseLayer>
|
||||
mapBackgroundLayer?: Store<RasterLayerPolygon>
|
||||
}
|
||||
) => {
|
||||
options = options ?? {}
|
||||
|
@ -674,14 +675,18 @@ class LengthTextField extends TextFieldDef {
|
|||
zoom: zoom,
|
||||
})
|
||||
if (args[1]) {
|
||||
// We have a prefered map!
|
||||
// The arguments indicate the preferred background type
|
||||
options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(
|
||||
location,
|
||||
new UIEventSource<string[]>(args[1].split(","))
|
||||
new ImmutableStore<string[]>(args[1].split(","))
|
||||
)
|
||||
}
|
||||
const background = options?.mapBackgroundLayer
|
||||
const li = new LengthInput(new UIEventSource<BaseLayer>(background.data), location, value)
|
||||
const li = new LengthInput(
|
||||
new UIEventSource<RasterLayerPolygon>(background.data),
|
||||
location,
|
||||
value
|
||||
)
|
||||
li.SetStyle("height: 20rem;")
|
||||
return li
|
||||
}
|
||||
|
|
|
@ -21,12 +21,20 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
"keyboard",
|
||||
"touchZoomRotate",
|
||||
]
|
||||
private static maplibre_zoom_handlers = [
|
||||
"scrollZoom",
|
||||
"boxZoom",
|
||||
"doubleClickZoom",
|
||||
"touchZoomRotate",
|
||||
]
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
readonly zoom: UIEventSource<number>
|
||||
readonly bounds: Store<BBox>
|
||||
readonly bounds: UIEventSource<BBox>
|
||||
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
|
||||
readonly maxbounds: UIEventSource<BBox | undefined>
|
||||
readonly allowMoving: UIEventSource<true | boolean | undefined>
|
||||
readonly allowZooming: UIEventSource<true | boolean | undefined>
|
||||
readonly lastClickLocation: Store<undefined | { lon: number; lat: number }>
|
||||
private readonly _maplibreMap: Store<MLMap>
|
||||
private readonly _bounds: UIEventSource<BBox>
|
||||
/**
|
||||
|
@ -50,11 +58,14 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
})
|
||||
this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined)
|
||||
this.allowMoving = state?.allowMoving ?? new UIEventSource(true)
|
||||
this.allowZooming = state?.allowZooming ?? new UIEventSource(true)
|
||||
this._bounds = new UIEventSource(undefined)
|
||||
this.bounds = this._bounds
|
||||
this.rasterLayer =
|
||||
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
|
||||
|
||||
const lastClickLocation = new UIEventSource<{ lon: number; lat: number }>(undefined)
|
||||
this.lastClickLocation = lastClickLocation
|
||||
const self = this
|
||||
maplibreMap.addCallbackAndRunD((map) => {
|
||||
map.on("load", () => {
|
||||
|
@ -63,11 +74,13 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
self.SetZoom(self.zoom.data)
|
||||
self.setMaxBounds(self.maxbounds.data)
|
||||
self.setAllowMoving(self.allowMoving.data)
|
||||
self.setAllowZooming(self.allowZooming.data)
|
||||
})
|
||||
self.MoveMapToCurrentLoc(self.location.data)
|
||||
self.SetZoom(self.zoom.data)
|
||||
self.setMaxBounds(self.maxbounds.data)
|
||||
self.setAllowMoving(self.allowMoving.data)
|
||||
self.setAllowZooming(self.allowZooming.data)
|
||||
map.on("moveend", () => {
|
||||
const dt = this.location.data
|
||||
dt.lon = map.getCenter().lng
|
||||
|
@ -81,6 +94,11 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
])
|
||||
self._bounds.setData(bbox)
|
||||
})
|
||||
map.on("click", (e) => {
|
||||
const lon = e.lngLat.lng
|
||||
const lat = e.lngLat.lat
|
||||
lastClickLocation.setData({ lon, lat })
|
||||
})
|
||||
})
|
||||
|
||||
this.rasterLayer.addCallback((_) =>
|
||||
|
@ -95,6 +113,8 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
this.zoom.addCallbackAndRunD((z) => self.SetZoom(z))
|
||||
this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox))
|
||||
this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving))
|
||||
this.allowZooming.addCallbackAndRun((allowZooming) => self.setAllowZooming(allowZooming))
|
||||
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -205,7 +225,7 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
// already the correct background layer, nothing to do
|
||||
return
|
||||
}
|
||||
if (background === undefined) {
|
||||
if (!background?.url) {
|
||||
// no background to set
|
||||
this.removeCurrentLayer(map)
|
||||
this._currentRasterLayer = undefined
|
||||
|
@ -266,4 +286,38 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setAllowZooming(allow: true | boolean | undefined) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
return
|
||||
}
|
||||
if (allow === false) {
|
||||
for (const id of MapLibreAdaptor.maplibre_zoom_handlers) {
|
||||
map[id].disable()
|
||||
}
|
||||
} else {
|
||||
for (const id of MapLibreAdaptor.maplibre_zoom_handlers) {
|
||||
map[id].enable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setBounds(bounds: BBox) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
return
|
||||
}
|
||||
const oldBounds = map.getBounds()
|
||||
const e = 0.0000001
|
||||
const hasDiff =
|
||||
Math.abs(oldBounds.getWest() - bounds.getWest()) > e &&
|
||||
Math.abs(oldBounds.getEast() - bounds.getEast()) > e &&
|
||||
Math.abs(oldBounds.getNorth() - bounds.getNorth()) > e &&
|
||||
Math.abs(oldBounds.getSouth() - bounds.getSouth()) > e
|
||||
if (!hasDiff) {
|
||||
return
|
||||
}
|
||||
map.fitBounds(bounds.toLngLat())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Map as MlMap } from "maplibre-gl"
|
||||
import { GeoJSONSource, Marker } from "maplibre-gl"
|
||||
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
|
||||
|
@ -14,10 +14,13 @@ import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
|
|||
import { Utils } from "../../Utils"
|
||||
import * as range_layer from "../../assets/layers/range/range.json"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
|
||||
class PointRenderingLayer {
|
||||
private readonly _config: PointRenderingConfig
|
||||
private readonly _fetchStore?: (id: string) => Store<OsmTags>
|
||||
private readonly _fetchStore?: (id: string) => Store<Record<string, string>>
|
||||
private readonly _map: MlMap
|
||||
private readonly _onClick: (feature: Feature) => void
|
||||
private readonly _allMarkers: Map<string, Marker> = new Map<string, Marker>()
|
||||
|
@ -27,7 +30,7 @@ class PointRenderingLayer {
|
|||
features: FeatureSource,
|
||||
config: PointRenderingConfig,
|
||||
visibility?: Store<boolean>,
|
||||
fetchStore?: (id: string) => Store<OsmTags>,
|
||||
fetchStore?: (id: string) => Store<Record<string, string>>,
|
||||
onClick?: (feature: Feature) => void
|
||||
) {
|
||||
this._config = config
|
||||
|
@ -96,7 +99,7 @@ class PointRenderingLayer {
|
|||
}
|
||||
|
||||
private addPoint(feature: Feature, loc: [number, number]): Marker {
|
||||
let store: Store<OsmTags>
|
||||
let store: Store<Record<string, string>>
|
||||
if (this._fetchStore) {
|
||||
store = this._fetchStore(feature.properties.id)
|
||||
} else {
|
||||
|
@ -143,7 +146,7 @@ class LineRenderingLayer {
|
|||
private readonly _map: MlMap
|
||||
private readonly _config: LineRenderingConfig
|
||||
private readonly _visibility?: Store<boolean>
|
||||
private readonly _fetchStore?: (id: string) => Store<OsmTags>
|
||||
private readonly _fetchStore?: (id: string) => Store<Record<string, string>>
|
||||
private readonly _onClick?: (feature: Feature) => void
|
||||
private readonly _layername: string
|
||||
private readonly _listenerInstalledOn: Set<string> = new Set<string>()
|
||||
|
@ -154,7 +157,7 @@ class LineRenderingLayer {
|
|||
layername: string,
|
||||
config: LineRenderingConfig,
|
||||
visibility?: Store<boolean>,
|
||||
fetchStore?: (id: string) => Store<OsmTags>,
|
||||
fetchStore?: (id: string) => Store<Record<string, string>>,
|
||||
onClick?: (feature: Feature) => void
|
||||
) {
|
||||
this._layername = layername
|
||||
|
@ -212,9 +215,10 @@ class LineRenderingLayer {
|
|||
promoteId: "id",
|
||||
})
|
||||
// @ts-ignore
|
||||
const linelayer = this._layername + "_line"
|
||||
map.addLayer({
|
||||
source: this._layername,
|
||||
id: this._layername + "_line",
|
||||
id: linelayer,
|
||||
type: "line",
|
||||
paint: {
|
||||
"line-color": ["feature-state", "color"],
|
||||
|
@ -227,9 +231,10 @@ class LineRenderingLayer {
|
|||
},
|
||||
})
|
||||
|
||||
const polylayer = this._layername + "_polygon"
|
||||
map.addLayer({
|
||||
source: this._layername,
|
||||
id: this._layername + "_polygon",
|
||||
id: polylayer,
|
||||
type: "fill",
|
||||
filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]],
|
||||
layout: {},
|
||||
|
@ -238,6 +243,11 @@ class LineRenderingLayer {
|
|||
"fill-opacity": 0.1,
|
||||
},
|
||||
})
|
||||
|
||||
this._visibility.addCallbackAndRunD((visible) => {
|
||||
map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none")
|
||||
map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none")
|
||||
})
|
||||
} else {
|
||||
src.setData({
|
||||
type: "FeatureCollection",
|
||||
|
@ -295,6 +305,24 @@ export default class ShowDataLayer {
|
|||
map.addCallbackAndRunD((map) => self.initDrawFeatures(map))
|
||||
}
|
||||
|
||||
public static showMultipleLayers(
|
||||
mlmap: UIEventSource<MlMap>,
|
||||
features: FeatureSource,
|
||||
layers: LayerConfig[],
|
||||
options?: Partial<ShowDataLayerOptions>
|
||||
) {
|
||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||
layers.map((l) => new FilteredLayer(l)),
|
||||
new StaticFeatureSource(features)
|
||||
)
|
||||
perLayer.forEach((fs) => {
|
||||
new ShowDataLayer(mlmap, {
|
||||
layer: fs.layer.layerDef,
|
||||
features: fs,
|
||||
...(options ?? {}),
|
||||
})
|
||||
})
|
||||
}
|
||||
public static showRange(
|
||||
map: Store<MlMap>,
|
||||
features: FeatureSource,
|
||||
|
@ -318,8 +346,11 @@ export default class ShowDataLayer {
|
|||
}
|
||||
|
||||
private initDrawFeatures(map: MlMap) {
|
||||
const { features, doShowLayer, fetchStore, selectedElement } = this._options
|
||||
const onClick = (feature: Feature) => selectedElement?.setData(feature)
|
||||
let { features, doShowLayer, fetchStore, selectedElement, selectedLayer } = this._options
|
||||
const onClick = (feature: Feature) => {
|
||||
selectedElement?.setData(feature)
|
||||
selectedLayer?.setData(this._options.layer)
|
||||
}
|
||||
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
|
||||
const lineRenderingConfig = this._options.layer.lineRendering[i]
|
||||
new LineRenderingLayer(
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
export interface ShowDataLayerOptions {
|
||||
/**
|
||||
|
@ -11,7 +13,12 @@ export interface ShowDataLayerOptions {
|
|||
* Indication of the current selected element; overrides some filters.
|
||||
* When a feature is tapped, the feature will be put in there
|
||||
*/
|
||||
selectedElement?: UIEventSource<any>
|
||||
selectedElement?: UIEventSource<Feature>
|
||||
|
||||
/**
|
||||
* When a feature of this layer is tapped, the layer will be marked
|
||||
*/
|
||||
selectedLayer?: UIEventSource<LayerConfig>
|
||||
|
||||
/**
|
||||
* If set, zoom to the features when initially loaded and when they are changed
|
||||
|
@ -26,5 +33,5 @@ export interface ShowDataLayerOptions {
|
|||
* Function which fetches the relevant store.
|
||||
* If given, the map will update when a property is changed
|
||||
*/
|
||||
fetchStore?: (id: string) => UIEventSource<OsmTags>
|
||||
fetchStore?: (id: string) => Store<Record<string, string>>
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/**
|
||||
* SHows geojson on the given leaflet map, but attempts to figure out the correct layer first
|
||||
*/
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import ShowDataLayer from "./ShowDataLayer"
|
||||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource"
|
||||
import { GlobalFilter } from "../../Models/GlobalFilter"
|
||||
|
||||
export default class ShowDataMultiLayer {
|
||||
constructor(
|
||||
map: Store<MlMap>,
|
||||
options: ShowDataLayerOptions & {
|
||||
layers: FilteredLayer[]
|
||||
globalFilters?: Store<GlobalFilter[]>
|
||||
}
|
||||
) {
|
||||
new PerLayerFeatureSourceSplitter(
|
||||
new ImmutableStore(options.layers),
|
||||
(features, layer) => {
|
||||
const newOptions = {
|
||||
...options,
|
||||
layer: layer.layerDef,
|
||||
features: new FilteringFeatureSource(
|
||||
layer,
|
||||
features,
|
||||
options.fetchStore,
|
||||
options.globalFilters
|
||||
),
|
||||
}
|
||||
new ShowDataLayer(map, newOptions)
|
||||
},
|
||||
options.features
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import LocationInput from "../Input/LocationInput"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
|
@ -18,18 +16,13 @@ import { Tag } from "../../Logic/Tags/Tag"
|
|||
import { WayId } from "../../Models/OsmFeature"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { Feature } from "geojson"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { GlobalFilter } from "../../Logic/State/GlobalFilter"
|
||||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import ClippedFeatureSource from "../../Logic/FeatureSource/Sources/ClippedFeatureSource"
|
||||
|
||||
export default class ConfirmLocationOfPoint extends Combine {
|
||||
constructor(
|
||||
state: {
|
||||
globalFilters: UIEventSource<GlobalFilter[]>
|
||||
featureSwitchIsTesting: UIEventSource<boolean>
|
||||
osmConnection: OsmConnection
|
||||
featurePipeline: FeaturePipeline
|
||||
backgroundLayer?: UIEventSource<RasterLayerPolygon | undefined>
|
||||
},
|
||||
state: SpecialVisualizationState,
|
||||
filterViewIsOpened: UIEventSource<boolean>,
|
||||
preset: PresetInfo,
|
||||
confirmText: BaseUIElement,
|
||||
|
@ -55,7 +48,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
const locationSrc = new UIEventSource(zloc)
|
||||
|
||||
let backgroundLayer = new UIEventSource(
|
||||
state?.backgroundLayer?.data ?? AvailableRasterLayers.osmCarto
|
||||
state?.mapProperties.rasterLayer?.data ?? AvailableRasterLayers.osmCarto
|
||||
)
|
||||
if (preset.preciseInput.preferredBackground) {
|
||||
const defaultBackground = AvailableRasterLayers.SelectBestLayerAccordingTo(
|
||||
|
@ -105,15 +98,13 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
Math.max(preset.boundsFactor ?? 0.25, 2)
|
||||
)
|
||||
loadedBbox = bbox
|
||||
const allFeatures: Feature[] = []
|
||||
preset.preciseInput.snapToLayers.forEach((layerId) => {
|
||||
console.log("Snapping to", layerId)
|
||||
state.featurePipeline
|
||||
.GetFeaturesWithin(layerId, bbox)
|
||||
?.forEach((feats) => allFeatures.push(...(<any[]>feats)))
|
||||
})
|
||||
console.log("Snapping to", allFeatures)
|
||||
snapToFeatures.setData(allFeatures)
|
||||
const sources = preset.preciseInput.snapToLayers.map(
|
||||
(layerId) =>
|
||||
new ClippedFeatureSource(
|
||||
state.perLayer.get(layerId),
|
||||
bbox.asGeoJson({})
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -488,7 +488,7 @@ export class OH {
|
|||
}
|
||||
|
||||
public static CreateOhObject(
|
||||
tags: object & { _lat: number; _lon: number; _country?: string },
|
||||
tags: Record<string, string> & { _lat: number; _lon: number; _country?: string },
|
||||
textToParse: string
|
||||
) {
|
||||
// noinspection JSPotentiallyInvalidConstructorUsage
|
||||
|
|
|
@ -23,7 +23,7 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
]
|
||||
|
||||
constructor(
|
||||
tags: UIEventSource<any>,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
state: { osmConnection?: OsmConnection },
|
||||
key: string,
|
||||
prefix = "",
|
||||
|
@ -49,7 +49,7 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
}
|
||||
try {
|
||||
return OpeningHoursVisualization.CreateFullVisualisation(
|
||||
OH.CreateOhObject(tags.data, ohtext)
|
||||
OH.CreateOhObject(<any>tags.data, ohtext)
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn(e, e.stack)
|
||||
|
|
|
@ -8,7 +8,8 @@ import Toggle from "../Input/Toggle"
|
|||
import { LoginToggle } from "./LoginButton"
|
||||
import Combine from "../Base/Combine"
|
||||
import Title from "../Base/Title"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
export class AddNoteCommentViz implements SpecialVisualization {
|
||||
funcName = "add_note_comment"
|
||||
|
@ -21,7 +22,11 @@ export class AddNoteCommentViz implements SpecialVisualization {
|
|||
},
|
||||
]
|
||||
|
||||
public constr(state, tags, args) {
|
||||
public constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
) {
|
||||
const t = Translations.t.notes
|
||||
const textField = new TextField({
|
||||
placeholder: t.addCommentPlaceholder,
|
||||
|
@ -62,12 +67,11 @@ export class AddNoteCommentViz implements SpecialVisualization {
|
|||
return t.addCommentAndClose
|
||||
})
|
||||
)
|
||||
).onClick(() => {
|
||||
).onClick(async () => {
|
||||
const id = tags.data[args[1] ?? "id"]
|
||||
state.osmConnection.closeNote(id, txt.data).then((_) => {
|
||||
tags.data["closed_at"] = new Date().toISOString()
|
||||
tags.ping()
|
||||
})
|
||||
await state.osmConnection.closeNote(id, txt.data)
|
||||
tags.data["closed_at"] = new Date().toISOString()
|
||||
tags.ping()
|
||||
})
|
||||
|
||||
const reopen = new SubtleButton(
|
||||
|
@ -80,12 +84,11 @@ export class AddNoteCommentViz implements SpecialVisualization {
|
|||
return t.reopenNoteAndComment
|
||||
})
|
||||
)
|
||||
).onClick(() => {
|
||||
).onClick(async () => {
|
||||
const id = tags.data[args[1] ?? "id"]
|
||||
state.osmConnection.reopenNote(id, txt.data).then((_) => {
|
||||
tags.data["closed_at"] = undefined
|
||||
tags.ping()
|
||||
})
|
||||
await state.osmConnection.reopenNote(id, txt.data)
|
||||
tags.data["closed_at"] = undefined
|
||||
tags.ping()
|
||||
})
|
||||
|
||||
const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "")
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Img from "../Base/Img"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
|
@ -9,8 +7,6 @@ import Combine from "../Base/Combine"
|
|||
import Link from "../Base/Link"
|
||||
import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
||||
import { Utils } from "../../Utils"
|
||||
import Minimap from "../Base/Minimap"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Loading from "../Base/Loading"
|
||||
|
@ -23,15 +19,21 @@ import FilteredLayer from "../../Models/FilteredLayer"
|
|||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import List from "../Base/List"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
|
||||
export interface AutoAction extends SpecialVisualization {
|
||||
supportsAutoAction: boolean
|
||||
|
||||
applyActionOn(
|
||||
state: {
|
||||
layoutToUse: LayoutConfig
|
||||
layout: LayoutConfig
|
||||
changes: Changes
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
},
|
||||
tagSource: UIEventSource<any>,
|
||||
argument: string[]
|
||||
|
@ -43,7 +45,7 @@ class ApplyButton extends UIElement {
|
|||
private readonly text: string
|
||||
private readonly targetTagRendering: string
|
||||
private readonly target_layer_id: string
|
||||
private readonly state: FeaturePipelineState
|
||||
private readonly state: SpecialVisualizationState
|
||||
private readonly target_feature_ids: string[]
|
||||
private readonly buttonState = new UIEventSource<
|
||||
"idle" | "running" | "done" | { error: string }
|
||||
|
@ -52,7 +54,7 @@ class ApplyButton extends UIElement {
|
|||
private readonly tagRenderingConfig: TagRenderingConfig
|
||||
|
||||
constructor(
|
||||
state: FeaturePipelineState,
|
||||
state: SpecialVisualizationState,
|
||||
target_feature_ids: string[],
|
||||
options: {
|
||||
target_layer_id: string
|
||||
|
@ -68,9 +70,7 @@ class ApplyButton extends UIElement {
|
|||
this.targetTagRendering = options.targetTagRendering
|
||||
this.text = options.text
|
||||
this.icon = options.icon
|
||||
this.layer = this.state.filteredLayers.data.find(
|
||||
(l) => l.layerDef.id === this.target_layer_id
|
||||
)
|
||||
this.layer = this.state.layerState.filteredLayers.get(this.target_layer_id)
|
||||
this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find(
|
||||
(tr) => tr.id === this.targetTagRendering
|
||||
)
|
||||
|
@ -101,22 +101,23 @@ class ApplyButton extends UIElement {
|
|||
),
|
||||
]).SetClass("subtle")
|
||||
|
||||
const previewMap = Minimap.createMiniMap({
|
||||
allowMoving: false,
|
||||
background: this.state.backgroundLayer,
|
||||
addLayerControl: true,
|
||||
}).SetClass("h-48")
|
||||
const mlmap = new UIEventSource(undefined)
|
||||
const mla = new MapLibreAdaptor(mlmap, {
|
||||
rasterLayer: this.state.mapProperties.rasterLayer,
|
||||
})
|
||||
mla.allowZooming.setData(false)
|
||||
mla.allowMoving.setData(false)
|
||||
|
||||
const previewMap = new SvelteUIElement(MaplibreMap, { map: mlmap }).SetClass("h-48")
|
||||
|
||||
const features = this.target_feature_ids.map((id) =>
|
||||
this.state.allElements.ContainingFeatures.get(id)
|
||||
this.state.indexedFeatures.featuresById.data.get(id)
|
||||
)
|
||||
|
||||
new ShowDataLayer({
|
||||
leafletMap: previewMap.leafletMap,
|
||||
zoomToFeatures: true,
|
||||
new ShowDataLayer(mlmap, {
|
||||
features: StaticFeatureSource.fromGeojson(features),
|
||||
state: this.state,
|
||||
layerToShow: this.layer.layerDef,
|
||||
zoomToFeatures: true,
|
||||
layer: this.layer.layerDef,
|
||||
})
|
||||
|
||||
return new VariableUiElement(
|
||||
|
@ -144,7 +145,7 @@ class ApplyButton extends UIElement {
|
|||
console.log("Applying auto-action on " + this.target_feature_ids.length + " features")
|
||||
|
||||
for (const targetFeatureId of this.target_feature_ids) {
|
||||
const featureTags = this.state.allElements.getEventSourceById(targetFeatureId)
|
||||
const featureTags = this.state.featureProperties.getStore(targetFeatureId)
|
||||
const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt
|
||||
const specialRenderings = Utils.NoNull(
|
||||
SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special)
|
||||
|
@ -153,8 +154,8 @@ class ApplyButton extends UIElement {
|
|||
if (specialRenderings.length == 0) {
|
||||
console.warn(
|
||||
"AutoApply: feature " +
|
||||
targetFeatureId +
|
||||
" got a rendering without supported auto actions:",
|
||||
targetFeatureId +
|
||||
" got a rendering without supported auto actions:",
|
||||
rendering
|
||||
)
|
||||
}
|
||||
|
@ -224,7 +225,7 @@ export default class AutoApplyButton implements SpecialVisualization {
|
|||
"To effectively use this button, you'll need some ingredients:",
|
||||
new List([
|
||||
"A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " +
|
||||
supportedActions.join(", "),
|
||||
supportedActions.join(", "),
|
||||
"A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ",
|
||||
new Link("current_view", "./BuiltinLayers.md#current_view"),
|
||||
"Then, use a calculated tag on the host feature to determine the overlapping object ids",
|
||||
|
@ -234,18 +235,17 @@ export default class AutoApplyButton implements SpecialVisualization {
|
|||
}
|
||||
|
||||
constr(
|
||||
state: FeaturePipelineState,
|
||||
tagSource: UIEventSource<any>,
|
||||
argument: string[],
|
||||
guistate: DefaultGuiState
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[]
|
||||
): BaseUIElement {
|
||||
try {
|
||||
if (
|
||||
!state.layoutToUse.official &&
|
||||
!state.layout.official &&
|
||||
!(
|
||||
state.featureSwitchIsTesting.data ||
|
||||
state.osmConnection._oauth_config.url ===
|
||||
OsmConnection.oauth_configs["osm-test"].url
|
||||
OsmConnection.oauth_configs["osm-test"].url
|
||||
)
|
||||
) {
|
||||
const t = Translations.t.general.add.import
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Utils } from "../../Utils"
|
||||
|
@ -7,7 +6,8 @@ import Img from "../Base/Img"
|
|||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { LoginToggle } from "./LoginButton"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
export class CloseNoteButton implements SpecialVisualization {
|
||||
public readonly funcName = "close_note"
|
||||
|
@ -43,7 +43,11 @@ export class CloseNoteButton implements SpecialVisualization {
|
|||
},
|
||||
]
|
||||
|
||||
public constr(state: FeaturePipelineState, tags, args): BaseUIElement {
|
||||
public constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
): BaseUIElement {
|
||||
const t = Translations.t.notes
|
||||
|
||||
const params: {
|
||||
|
@ -78,7 +82,7 @@ export class CloseNoteButton implements SpecialVisualization {
|
|||
closeButton = new Toggle(
|
||||
closeButton,
|
||||
params.zoomButton ?? "",
|
||||
state.locationControl.map((l) => l.zoom >= Number(params.minZoom))
|
||||
state.mapProperties.zoom.map((zoom) => zoom >= Number(params.minZoom))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,14 +4,15 @@ import Svg from "../../Svg"
|
|||
import Combine from "../Base/Combine"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { Utils } from "../../Utils"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
export class ExportAsGpxViz implements SpecialVisualization {
|
||||
funcName = "export_as_gpx"
|
||||
docs = "Exports the selected feature as GPX-file"
|
||||
args = []
|
||||
|
||||
constr(state, tagSource) {
|
||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>) {
|
||||
const t = Translations.t.general.download
|
||||
|
||||
return new SubtleButton(
|
||||
|
@ -23,10 +24,10 @@ export class ExportAsGpxViz implements SpecialVisualization {
|
|||
).onClick(() => {
|
||||
console.log("Exporting as GPX!")
|
||||
const tags = tagSource.data
|
||||
const feature = state.allElements.ContainingFeatures.get(tags.id)
|
||||
const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags)
|
||||
const gpx = GeoOperations.AsGpx(feature, matchingLayer)
|
||||
const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track"
|
||||
const feature = state.indexedFeatures.featuresById.data.get(tags.id)
|
||||
const layer = state?.layout?.getMatchingLayer(tags)
|
||||
const gpx = GeoOperations.AsGpx(feature, { layer })
|
||||
const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track"
|
||||
Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", {
|
||||
mimetype: "{gpx=application/gpx+xml}",
|
||||
})
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
// import Histogram from "../BigComponents/Histogram";
|
||||
// import {SpecialVisualization} from "../SpecialVisualization";
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Histogram from "../BigComponents/Histogram"
|
||||
|
||||
export class HistogramViz {
|
||||
export class HistogramViz implements SpecialVisualization {
|
||||
funcName = "histogram"
|
||||
docs = "Create a histogram for a list of given values, read from the properties."
|
||||
example =
|
||||
|
@ -30,7 +29,11 @@ export class HistogramViz {
|
|||
},
|
||||
]
|
||||
|
||||
constr(state, tagSource: UIEventSource<any>, args: string[]) {
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
) {
|
||||
let assignColors = undefined
|
||||
if (args.length >= 3) {
|
||||
const colors = [...args]
|
||||
|
@ -63,10 +66,8 @@ export class HistogramViz {
|
|||
return undefined
|
||||
}
|
||||
})
|
||||
return new FixedUiElement("HISTORGRAM")
|
||||
/*
|
||||
return new Histogram(listSource, args[1], args[2], {
|
||||
assignColor: assignColors,
|
||||
})*/
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,51 +1,47 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"
|
||||
import Loading from "../Base/Loading"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"
|
||||
import Img from "../Base/Img"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Svg from "../../Svg"
|
||||
import { Utils } from "../../Utils"
|
||||
import Minimap from "../Base/Minimap"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
|
||||
import CreateWayWithPointReuseAction, {
|
||||
MergePointConfig,
|
||||
} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"
|
||||
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
import { PresetInfo } from "../BigComponents/SimpleAddUI"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import { And } from "../../Logic/Tags/And"
|
||||
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"
|
||||
import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import TagApplyButton from "./TagApplyButton"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import conflation_json from "../../assets/layers/conflation/conflation.json"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { LoginToggle } from "./LoginButton"
|
||||
import { AutoAction } from "./AutoApplyButton"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import { ElementStorage } from "../../Logic/ElementStorage"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import { SubtleButton } from "../Base/SubtleButton";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import { VariableUiElement } from "../Base/VariableUIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||
import Loading from "../Base/Loading";
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
|
||||
import Lazy from "../Base/Lazy";
|
||||
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
|
||||
import Img from "../Base/Img";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import { FixedUiElement } from "../Base/FixedUiElement";
|
||||
import Svg from "../../Svg";
|
||||
import { Utils } from "../../Utils";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import CreateWayWithPointReuseAction, { MergePointConfig } from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||
import OsmChangeAction, { OsmCreateAction } from "../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
|
||||
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject";
|
||||
import { PresetInfo } from "../BigComponents/SimpleAddUI";
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils";
|
||||
import { And } from "../../Logic/Tags/And";
|
||||
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||
import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction";
|
||||
import { Tag } from "../../Logic/Tags/Tag";
|
||||
import TagApplyButton from "./TagApplyButton";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import conflation_json from "../../assets/layers/conflation/conflation.json";
|
||||
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||
import { LoginToggle } from "./LoginButton";
|
||||
import { AutoAction } from "./AutoApplyButton";
|
||||
import Hash from "../../Logic/Web/Hash";
|
||||
import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig";
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import Maproulette from "../../Logic/Maproulette";
|
||||
import { Feature, Point } from "geojson";
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import ShowDataLayer from "../Map/ShowDataLayer";
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor";
|
||||
import SvelteUIElement from "../Base/SvelteUIElement";
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte";
|
||||
|
||||
/**
|
||||
* A helper class for the various import-flows.
|
||||
|
@ -106,7 +102,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
}
|
||||
|
||||
abstract constructElement(
|
||||
state: FeaturePipelineState,
|
||||
state: SpecialVisualizationState,
|
||||
args: {
|
||||
max_snap_distance: string
|
||||
snap_onto_layers: string
|
||||
|
@ -116,13 +112,16 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
newTags: UIEventSource<any>
|
||||
targetLayer: string
|
||||
},
|
||||
tagSource: UIEventSource<any>,
|
||||
guiState: DefaultGuiState,
|
||||
feature: any,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
feature: Feature,
|
||||
onCancelClicked: () => void
|
||||
): BaseUIElement
|
||||
|
||||
constr(state, tagSource: UIEventSource<any>, argsRaw, guiState) {
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argsRaw: string[]
|
||||
) {
|
||||
/**
|
||||
* Some generic import button pre-validation is implemented here:
|
||||
* - Are we logged in?
|
||||
|
@ -139,7 +138,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
{
|
||||
// Some initial validation
|
||||
if (
|
||||
!state.layoutToUse.official &&
|
||||
!state.layout.official &&
|
||||
!(
|
||||
state.featureSwitchIsTesting.data ||
|
||||
state.osmConnection._oauth_config.url ===
|
||||
|
@ -148,11 +147,9 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
) {
|
||||
return new Combine([t.officialThemesOnly.SetClass("alert"), t.howToTest])
|
||||
}
|
||||
const targetLayer: FilteredLayer = state.filteredLayers.data.filter(
|
||||
(fl) => fl.layerDef.id === args.targetLayer
|
||||
)[0]
|
||||
const targetLayer: FilteredLayer = state.layerState.filteredLayers.get(args.targetLayer)
|
||||
if (targetLayer === undefined) {
|
||||
const e = `Target layer not defined: error in import button for theme: ${state.layoutToUse.id}: layer ${args.targetLayer} not found`
|
||||
const e = `Target layer not defined: error in import button for theme: ${state.layout.id}: layer ${args.targetLayer} not found`
|
||||
console.error(e)
|
||||
return new FixedUiElement(e).SetClass("alert")
|
||||
}
|
||||
|
@ -167,7 +164,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
const inviteToImportButton = new SubtleButton(img, args.text)
|
||||
|
||||
const id = tagSource.data.id
|
||||
const feature = state.allElements.ContainingFeatures.get(id)
|
||||
const feature = state.indexedFeatures.featuresById.data.get(id)
|
||||
|
||||
// Explanation of the tags that will be applied onto the imported/conflated object
|
||||
|
||||
|
@ -205,22 +202,13 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
return tags._imported === "yes"
|
||||
})
|
||||
|
||||
/**** THe actual panel showing the import guiding map ****/
|
||||
const importGuidingPanel = this.constructElement(
|
||||
state,
|
||||
args,
|
||||
tagSource,
|
||||
guiState,
|
||||
feature,
|
||||
() => importClicked.setData(false)
|
||||
/**** The actual panel showing the import guiding map ****/
|
||||
const importGuidingPanel = this.constructElement(state, args, tagSource, feature, () =>
|
||||
importClicked.setData(false)
|
||||
)
|
||||
|
||||
const importFlow = new Toggle(
|
||||
new Toggle(
|
||||
new Loading(t0.stillLoading),
|
||||
importGuidingPanel,
|
||||
state.featurePipeline.runningQuery
|
||||
),
|
||||
new Toggle(new Loading(t0.stillLoading), importGuidingPanel, state.dataIsLoading),
|
||||
inviteToImportButton,
|
||||
importClicked
|
||||
)
|
||||
|
@ -230,7 +218,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
new Toggle(
|
||||
new Toggle(t.hasBeenImported, importFlow, isImported),
|
||||
t.zoomInMore.SetClass("alert block"),
|
||||
state.locationControl.map((l) => l.zoom >= 18)
|
||||
state.mapProperties.zoom.map((zoom) => zoom >= 18)
|
||||
),
|
||||
pleaseLoginButton,
|
||||
state
|
||||
|
@ -258,8 +246,13 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
|
||||
protected abstract canBeImported(feature: any)
|
||||
|
||||
private static readonly conflationLayer = new LayerConfig(
|
||||
<LayerConfigJson>conflation_json,
|
||||
"all_known_layers",
|
||||
true
|
||||
)
|
||||
protected createConfirmPanelForWay(
|
||||
state: FeaturePipelineState,
|
||||
state: SpecialVisualizationState,
|
||||
args: {
|
||||
max_snap_distance: string
|
||||
snap_onto_layers: string
|
||||
|
@ -270,32 +263,32 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
},
|
||||
feature: any,
|
||||
originalFeatureTags: UIEventSource<any>,
|
||||
action: OsmChangeAction & { getPreview(): Promise<FeatureSource>; newElementId?: string },
|
||||
action: OsmChangeAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string },
|
||||
onCancel: () => void
|
||||
): BaseUIElement {
|
||||
const self = this
|
||||
const confirmationMap = Minimap.createMiniMap({
|
||||
allowMoving: state.featureSwitchIsDebugging.data ?? false,
|
||||
background: state.backgroundLayer,
|
||||
const map = new UIEventSource(undefined)
|
||||
new MapLibreAdaptor(map, {
|
||||
allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting),
|
||||
allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting),
|
||||
rasterLayer: state.mapProperties.rasterLayer,
|
||||
})
|
||||
const confirmationMap = new SvelteUIElement(MaplibreMap, { map })
|
||||
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
|
||||
|
||||
// SHow all relevant data - including (eventually) the way of which the geometry will be replaced
|
||||
new ShowDataMultiLayer({
|
||||
leafletMap: confirmationMap.leafletMap,
|
||||
zoomToFeatures: true,
|
||||
features: StaticFeatureSource.fromGeojson([feature]),
|
||||
state: state,
|
||||
layers: state.filteredLayers,
|
||||
})
|
||||
ShowDataLayer.showMultipleLayers(
|
||||
map,
|
||||
new StaticFeatureSource([feature]),
|
||||
state.layout.layers,
|
||||
{ zoomToFeatures: true }
|
||||
)
|
||||
// Show all relevant data - including (eventually) the way of which the geometry will be replaced
|
||||
|
||||
action.getPreview().then((changePreview) => {
|
||||
new ShowDataLayer({
|
||||
leafletMap: confirmationMap.leafletMap,
|
||||
new ShowDataLayer(map, {
|
||||
zoomToFeatures: false,
|
||||
features: changePreview,
|
||||
state,
|
||||
layerToShow: new LayerConfig(conflation_json, "all_known_layers", true),
|
||||
layer: AbstractImportButton.conflationLayer,
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -317,9 +310,9 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
{
|
||||
originalFeatureTags.data["_imported"] = "yes"
|
||||
originalFeatureTags.ping() // will set isImported as per its definition
|
||||
state.changes.applyAction(action)
|
||||
await state.changes.applyAction(action)
|
||||
const newId = action.newElementId ?? action.mainObjectId
|
||||
state.selectedElement.setData(state.allElements.ContainingFeatures.get(newId))
|
||||
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(newId))
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -392,7 +385,7 @@ export class ConflateButton extends AbstractImportButton {
|
|||
}
|
||||
|
||||
constructElement(
|
||||
state: FeaturePipelineState,
|
||||
state: SpecialVisualizationState,
|
||||
args: {
|
||||
max_snap_distance: string
|
||||
snap_onto_layers: string
|
||||
|
@ -403,8 +396,7 @@ export class ConflateButton extends AbstractImportButton {
|
|||
targetLayer: string
|
||||
},
|
||||
tagSource: UIEventSource<any>,
|
||||
guiState: DefaultGuiState,
|
||||
feature: any,
|
||||
feature: Feature,
|
||||
onCancelClicked: () => void
|
||||
): BaseUIElement {
|
||||
const nodesMustMatch = args.snap_onto_layers
|
||||
|
@ -424,10 +416,15 @@ export class ConflateButton extends AbstractImportButton {
|
|||
const key = args["way_to_conflate"]
|
||||
const wayToConflate = tagSource.data[key]
|
||||
feature = GeoOperations.removeOvernoding(feature)
|
||||
const action = new ReplaceGeometryAction(state, feature, wayToConflate, {
|
||||
theme: state.layoutToUse.id,
|
||||
newTags: args.newTags.data,
|
||||
})
|
||||
const action: OsmChangeAction & { getPreview(): Promise<any> } = new ReplaceGeometryAction(
|
||||
state,
|
||||
feature,
|
||||
wayToConflate,
|
||||
{
|
||||
theme: state.layout.id,
|
||||
newTags: args.newTags.data,
|
||||
}
|
||||
)
|
||||
|
||||
return this.createConfirmPanelForWay(
|
||||
state,
|
||||
|
@ -498,9 +495,9 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
|||
newTags: UIEventSource<any>
|
||||
targetLayer: string
|
||||
},
|
||||
state: FeaturePipelineState,
|
||||
state: SpecialVisualizationState,
|
||||
mergeConfigs: any[]
|
||||
) {
|
||||
): OsmCreateAction & { getPreview(): Promise<FeatureSource>; newElementId?: string } {
|
||||
const coors = feature.geometry.coordinates
|
||||
if (feature.geometry.type === "Polygon" && coors.length > 1) {
|
||||
const outer = coors[0]
|
||||
|
@ -525,8 +522,8 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
|||
}
|
||||
|
||||
async applyActionOn(
|
||||
state: { layoutToUse: LayoutConfig; changes: Changes; allElements: ElementStorage },
|
||||
originalFeatureTags: UIEventSource<any>,
|
||||
state: SpecialVisualizationState,
|
||||
originalFeatureTags: UIEventSource<Record<string, string>>,
|
||||
argument: string[]
|
||||
): Promise<void> {
|
||||
const id = originalFeatureTags.data.id
|
||||
|
@ -535,14 +532,9 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
|||
}
|
||||
AbstractImportButton.importedIds.add(originalFeatureTags.data.id)
|
||||
const args = this.parseArgs(argument, originalFeatureTags)
|
||||
const feature = state.allElements.ContainingFeatures.get(id)
|
||||
const feature = state.indexedFeatures.featuresById.data.get(id)
|
||||
const mergeConfigs = this.GetMergeConfig(args)
|
||||
const action = ImportWayButton.CreateAction(
|
||||
feature,
|
||||
args,
|
||||
<FeaturePipelineState>state,
|
||||
mergeConfigs
|
||||
)
|
||||
const action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs)
|
||||
await state.changes.applyAction(action)
|
||||
}
|
||||
|
||||
|
@ -557,7 +549,13 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
|||
return deps
|
||||
}
|
||||
|
||||
constructElement(state, args, originalFeatureTags, guiState, feature, onCancel): BaseUIElement {
|
||||
constructElement(
|
||||
state: SpecialVisualizationState,
|
||||
args,
|
||||
originalFeatureTags: UIEventSource<Record<string, string>>,
|
||||
feature,
|
||||
onCancel
|
||||
): BaseUIElement {
|
||||
const geometry = feature.geometry
|
||||
|
||||
if (!(geometry.type == "LineString" || geometry.type === "Polygon")) {
|
||||
|
@ -567,7 +565,12 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
|||
|
||||
// Upload the way to OSM
|
||||
const mergeConfigs = this.GetMergeConfig(args)
|
||||
let action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs)
|
||||
let action: OsmCreateAction & {getPreview?: any} = ImportWayButton.CreateAction(
|
||||
feature,
|
||||
args,
|
||||
state,
|
||||
mergeConfigs
|
||||
)
|
||||
return this.createConfirmPanelForWay(
|
||||
state,
|
||||
args,
|
||||
|
@ -663,10 +666,9 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
note_id: string
|
||||
maproulette_id: string
|
||||
},
|
||||
state: FeaturePipelineState,
|
||||
guiState: DefaultGuiState,
|
||||
state: SpecialVisualizationState,
|
||||
originalFeatureTags: UIEventSource<any>,
|
||||
feature: any,
|
||||
feature: Feature<Point>,
|
||||
onCancel: () => void,
|
||||
close: () => void
|
||||
): BaseUIElement {
|
||||
|
@ -690,7 +692,7 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
}
|
||||
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
|
||||
theme: state.layoutToUse.id,
|
||||
theme: state.layout.id,
|
||||
changeType: "import",
|
||||
snapOnto: <OsmWay>snapOnto,
|
||||
specialMotivation: specialMotivation,
|
||||
|
@ -698,7 +700,7 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
|
||||
await state.changes.applyAction(newElementAction)
|
||||
state.selectedElement.setData(
|
||||
state.allElements.ContainingFeatures.get(newElementAction.newElementId)
|
||||
state.indexedFeatures.featuresById.data.get(newElementAction.newElementId)
|
||||
)
|
||||
Hash.hash.setData(newElementAction.newElementId)
|
||||
|
||||
|
@ -742,19 +744,17 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
const presetInfo = <PresetInfo>{
|
||||
tags: args.newTags.data,
|
||||
icon: () => new Img(args.icon),
|
||||
layerToAddTo: state.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === args.targetLayer
|
||||
)[0],
|
||||
layerToAddTo: state.layerState.filteredLayers.get(args.targetLayer),
|
||||
name: args.text,
|
||||
title: Translations.T(args.text),
|
||||
preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise
|
||||
boundsFactor: 3,
|
||||
}
|
||||
|
||||
const [lon, lat] = feature.geometry.coordinates
|
||||
const [lon, lat] = <[number,number]> feature.geometry.coordinates
|
||||
return new ConfirmLocationOfPoint(
|
||||
state,
|
||||
guiState.filterViewIsOpened,
|
||||
state.guistate.filterViewIsOpened,
|
||||
presetInfo,
|
||||
Translations.W(args.text),
|
||||
{
|
||||
|
@ -783,10 +783,9 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
}
|
||||
|
||||
constructElement(
|
||||
state,
|
||||
state: SpecialVisualizationState,
|
||||
args,
|
||||
originalFeatureTags,
|
||||
guiState,
|
||||
feature,
|
||||
onCancel: () => void
|
||||
): BaseUIElement {
|
||||
|
@ -797,7 +796,6 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
ImportPointButton.createConfirmPanelForPoint(
|
||||
args,
|
||||
state,
|
||||
guiState,
|
||||
originalFeatureTags,
|
||||
feature,
|
||||
onCancel,
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import all_languages from "../../assets/language_translations.json"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Combine from "../Base/Combine"
|
||||
|
@ -16,10 +14,9 @@ import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
|||
import { And } from "../../Logic/Tags/And"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import { EditButton, SaveButton } from "./SaveButton"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { On } from "../../Models/ThemeConfig/Conversion/Conversion"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
export class LanguageElement implements SpecialVisualization {
|
||||
funcName: string = "language_chooser"
|
||||
|
@ -79,9 +76,10 @@ export class LanguageElement implements SpecialVisualization {
|
|||
`
|
||||
|
||||
constr(
|
||||
state: FeaturePipelineState,
|
||||
tagSource: UIEventSource<OsmTags>,
|
||||
argument: string[]
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
let [key, question, item_render, single_render, all_render, on_no_known_languages, mode] =
|
||||
argument
|
||||
|
@ -172,7 +170,7 @@ export class LanguageElement implements SpecialVisualization {
|
|||
new And(selection),
|
||||
tagSource.data,
|
||||
{
|
||||
theme: state?.layoutToUse?.id ?? "unkown",
|
||||
theme: state?.layout?.id ?? "unkown",
|
||||
changeType: "answer",
|
||||
}
|
||||
)
|
||||
|
|
|
@ -2,7 +2,9 @@ import { GeoOperations } from "../../Logic/GeoOperations"
|
|||
import { MapillaryLink } from "../BigComponents/MapillaryLink"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Feature } from "geojson"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
|
||||
export class MapillaryLinkVis implements SpecialVisualization {
|
||||
funcName = "mapillary_link"
|
||||
|
@ -15,9 +17,13 @@ export class MapillaryLinkVis implements SpecialVisualization {
|
|||
},
|
||||
]
|
||||
|
||||
public constr(state, tagsSource, args) {
|
||||
const feat = state.allElements.ContainingFeatures.get(tagsSource.data.id)
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feat)
|
||||
public constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagsSource: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
let zoom = Number(args[0])
|
||||
if (isNaN(zoom)) {
|
||||
zoom = 18
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import Minimap from "../Base/Minimap"
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Feature } from "geojson"
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import { stat } from "fs"
|
||||
|
||||
export class MinimapViz implements SpecialVisualization {
|
||||
funcName = "minimap"
|
||||
|
@ -22,16 +27,20 @@ export class MinimapViz implements SpecialVisualization {
|
|||
]
|
||||
example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`"
|
||||
|
||||
constr(state, tagSource, args, _) {
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
) {
|
||||
if (state === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const keys = [...args]
|
||||
keys.splice(0, 1)
|
||||
const featureStore = state.allElements.ContainingFeatures
|
||||
const featuresToShow: Store<{ freshness: Date; feature: any }[]> = tagSource.map(
|
||||
(properties) => {
|
||||
const features: { freshness: Date; feature: any }[] = []
|
||||
const featuresToShow: Store<Feature[]> = state.indexedFeatures.featuresById.map(
|
||||
(featuresById) => {
|
||||
const properties = tagSource.data
|
||||
const features: Feature[] = []
|
||||
for (const key of keys) {
|
||||
const value = properties[key]
|
||||
if (value === undefined || value === null) {
|
||||
|
@ -45,21 +54,22 @@ export class MinimapViz implements SpecialVisualization {
|
|||
}
|
||||
|
||||
for (const id of idList) {
|
||||
const feature = featureStore.get(id)
|
||||
const feature = featuresById.get(id)
|
||||
if (feature === undefined) {
|
||||
console.warn("No feature found for id ", id)
|
||||
continue
|
||||
}
|
||||
features.push({
|
||||
freshness: new Date(),
|
||||
feature,
|
||||
})
|
||||
features.push(feature)
|
||||
}
|
||||
}
|
||||
return features
|
||||
}
|
||||
},
|
||||
[tagSource]
|
||||
)
|
||||
const properties = tagSource.data
|
||||
|
||||
const mlmap = new UIEventSource(undefined)
|
||||
const mla = new MapLibreAdaptor(mlmap)
|
||||
|
||||
let zoom = 18
|
||||
if (args[0]) {
|
||||
const parsed = Number(args[0])
|
||||
|
@ -67,33 +77,18 @@ export class MinimapViz implements SpecialVisualization {
|
|||
zoom = parsed
|
||||
}
|
||||
}
|
||||
const locationSource = new UIEventSource<Loc>({
|
||||
lat: Number(properties._lat),
|
||||
lon: Number(properties._lon),
|
||||
zoom: zoom,
|
||||
})
|
||||
const minimap = Minimap.createMiniMap({
|
||||
background: state.backgroundLayer,
|
||||
location: locationSource,
|
||||
allowMoving: false,
|
||||
})
|
||||
mla.zoom.setData(zoom)
|
||||
mla.allowMoving.setData(false)
|
||||
mla.allowZooming.setData(false)
|
||||
|
||||
locationSource.addCallback((loc) => {
|
||||
if (loc.zoom > zoom) {
|
||||
// We zoom back
|
||||
locationSource.data.zoom = zoom
|
||||
locationSource.ping()
|
||||
}
|
||||
})
|
||||
ShowDataLayer.showMultipleLayers(
|
||||
mlmap,
|
||||
new StaticFeatureSource(featuresToShow),
|
||||
state.layout.layers
|
||||
)
|
||||
|
||||
new ShowDataMultiLayer({
|
||||
leafletMap: minimap["leafletMap"],
|
||||
zoomToFeatures: true,
|
||||
layers: state.filteredLayers,
|
||||
features: new StaticFeatureSource(featuresToShow),
|
||||
})
|
||||
|
||||
minimap.SetStyle("overflow: hidden; pointer-events: none;")
|
||||
return minimap
|
||||
return new SvelteUIElement(MaplibreMap, { map: mlmap }).SetStyle(
|
||||
"overflow: hidden; pointer-events: none;"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,17 +2,14 @@ import { Store } from "../../Logic/UIEventSource"
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import Combine from "../Base/Combine"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import { ElementStorage } from "../../Logic/ElementStorage"
|
||||
import { And } from "../../Logic/Tags/And"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export interface MultiApplyParams {
|
||||
featureIds: Store<string[]>
|
||||
|
@ -21,12 +18,7 @@ export interface MultiApplyParams {
|
|||
autoapply: boolean
|
||||
overwrite: boolean
|
||||
tagsSource: Store<any>
|
||||
state: {
|
||||
changes: Changes
|
||||
allElements: ElementStorage
|
||||
layoutToUse: LayoutConfig
|
||||
osmConnection: OsmConnection
|
||||
}
|
||||
state: SpecialVisualizationState
|
||||
}
|
||||
|
||||
class MultiApplyExecutor {
|
||||
|
@ -68,14 +60,14 @@ class MultiApplyExecutor {
|
|||
console.log("Multi-applying changes...")
|
||||
const featuresToChange = this.params.featureIds.data
|
||||
const changes = this.params.state.changes
|
||||
const allElements = this.params.state.allElements
|
||||
const allElements = this.params.state.featureProperties
|
||||
const keysToChange = this.params.keysToApply
|
||||
const overwrite = this.params.overwrite
|
||||
const selfTags = this.params.tagsSource.data
|
||||
const theme = this.params.state.layoutToUse.id
|
||||
const theme = this.params.state.layout.id
|
||||
for (const id of featuresToChange) {
|
||||
const tagsToApply: Tag[] = []
|
||||
const otherFeatureTags = allElements.getEventSourceById(id).data
|
||||
const otherFeatureTags = allElements.getStore(id).data
|
||||
for (const key of keysToChange) {
|
||||
const newValue = selfTags[key]
|
||||
if (newValue === undefined) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import MultiApply from "./MultiApply"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class MultiApplyViz implements SpecialVisualization {
|
||||
funcName = "multi_apply"
|
||||
|
@ -31,7 +31,11 @@ export class MultiApplyViz implements SpecialVisualization {
|
|||
example =
|
||||
"{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}"
|
||||
|
||||
constr(state, tagsSource, args) {
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagsSource: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
) {
|
||||
const featureIdsKey = args[0]
|
||||
const keysToApply = args[1].split(";")
|
||||
const text = args[2]
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
|
@ -19,7 +17,7 @@ import { VariableUiElement } from "../Base/VariableUIElement"
|
|||
import Toggle from "../Input/Toggle"
|
||||
import Title from "../Base/Title"
|
||||
import { MapillaryLinkVis } from "./MapillaryLinkVis"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class NearbyImageVis implements SpecialVisualization {
|
||||
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
|
||||
|
@ -39,14 +37,13 @@ export class NearbyImageVis implements SpecialVisualization {
|
|||
funcName = "nearby_images"
|
||||
|
||||
constr(
|
||||
state: FeaturePipelineState,
|
||||
tagSource: UIEventSource<any>,
|
||||
args: string[],
|
||||
guistate: DefaultGuiState
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
): BaseUIElement {
|
||||
const t = Translations.t.image.nearbyPictures
|
||||
const mode: "open" | "expandable" | "collapsable" = <any>args[0]
|
||||
const feature = state.allElements.ContainingFeatures.get(tagSource.data.id)
|
||||
const feature = state.indexedFeatures.featuresById.data.get(tagSource.data.id)
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
const id: string = tagSource.data["id"]
|
||||
const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+")
|
||||
|
@ -69,7 +66,7 @@ export class NearbyImageVis implements SpecialVisualization {
|
|||
}
|
||||
await state?.changes?.applyAction(
|
||||
new ChangeTagAction(id, new And(tags), tagSource.data, {
|
||||
theme: state?.layoutToUse.id,
|
||||
theme: state?.layout.id,
|
||||
changeType: "link-image",
|
||||
})
|
||||
)
|
||||
|
@ -116,8 +113,8 @@ export class NearbyImageVis implements SpecialVisualization {
|
|||
maxDaysOld: 365 * 3,
|
||||
}
|
||||
const slideshow = canBeEdited
|
||||
? new SelectOneNearbyImage(options, state)
|
||||
: new NearbyImages(options, state)
|
||||
? new SelectOneNearbyImage(options, state.indexedFeatures)
|
||||
: new NearbyImages(options, state.indexedFeatures)
|
||||
const controls = new Combine([
|
||||
towardsCenter,
|
||||
new Combine([
|
||||
|
|
|
@ -13,9 +13,10 @@ import Translations from "../i18n/Translations"
|
|||
import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { ElementStorage } from "../../Logic/ElementStorage"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import P4C from "pic4carto"
|
||||
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
|
||||
export interface P4CPicture {
|
||||
pictureUrl: string
|
||||
date?: number
|
||||
|
@ -47,15 +48,15 @@ export interface NearbyImageOptions {
|
|||
}
|
||||
|
||||
class ImagesInLoadedDataFetcher {
|
||||
private allElements: ElementStorage
|
||||
private indexedFeatures: IndexedFeatureSource
|
||||
|
||||
constructor(state: { allElements: ElementStorage }) {
|
||||
this.allElements = state.allElements
|
||||
constructor(indexedFeatures: IndexedFeatureSource) {
|
||||
this.indexedFeatures = indexedFeatures
|
||||
}
|
||||
|
||||
public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] {
|
||||
const foundImages: P4CPicture[] = []
|
||||
this.allElements.ContainingFeatures.forEach((feature) => {
|
||||
this.indexedFeatures.features.data.forEach((feature) => {
|
||||
const props = feature.properties
|
||||
const images = []
|
||||
if (props.image) {
|
||||
|
@ -100,7 +101,7 @@ class ImagesInLoadedDataFetcher {
|
|||
}
|
||||
|
||||
export default class NearbyImages extends Lazy {
|
||||
constructor(options: NearbyImageOptions, state?: { allElements: ElementStorage }) {
|
||||
constructor(options: NearbyImageOptions, state?: IndexedFeatureSource) {
|
||||
super(() => {
|
||||
const t = Translations.t.image.nearbyPictures
|
||||
const shownImages = options.shownImagesCount ?? new UIEventSource(25)
|
||||
|
@ -171,10 +172,7 @@ export default class NearbyImages extends Lazy {
|
|||
)
|
||||
}
|
||||
|
||||
private static buildPictureFetcher(
|
||||
options: NearbyImageOptions,
|
||||
state?: { allElements: ElementStorage }
|
||||
) {
|
||||
private static buildPictureFetcher(options: NearbyImageOptions, state?: IndexedFeatureSource) {
|
||||
const picManager = new P4C.PicturesManager({})
|
||||
const searchRadius = options.searchRadius ?? 500
|
||||
|
||||
|
@ -283,7 +281,7 @@ export class SelectOneNearbyImage extends NearbyImages implements InputElement<P
|
|||
|
||||
constructor(
|
||||
options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> },
|
||||
state?: { allElements: ElementStorage }
|
||||
state?: IndexedFeatureSource
|
||||
) {
|
||||
super(options, state)
|
||||
this.value = options.value ?? new UIEventSource<P4CPicture>(undefined)
|
||||
|
|
|
@ -12,7 +12,7 @@ import Combine from "../Base/Combine"
|
|||
import Svg from "../../Svg"
|
||||
import Translations from "../i18n/Translations"
|
||||
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization";
|
||||
|
||||
export class PlantNetDetectionViz implements SpecialVisualization {
|
||||
funcName = "plantnet_detection"
|
||||
|
@ -27,7 +27,7 @@ export class PlantNetDetectionViz implements SpecialVisualization {
|
|||
},
|
||||
]
|
||||
|
||||
public constr(state, tags, args) {
|
||||
public constr(state: SpecialVisualizationState, tags: UIEventSource<Record<string, string>>, args: string[]) {
|
||||
let imagePrefixes: string[] = undefined
|
||||
if (args.length > 0) {
|
||||
imagePrefixes = [].concat(...args.map((a) => a.split(",")))
|
||||
|
@ -53,7 +53,7 @@ export class PlantNetDetectionViz implements SpecialVisualization {
|
|||
]),
|
||||
tags.data,
|
||||
{
|
||||
theme: state.layoutToUse.id,
|
||||
theme: state.layout.id,
|
||||
changeType: "plantnet-ai-detection",
|
||||
}
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
|||
import ShareButton from "../BigComponents/ShareButton"
|
||||
import Svg from "../../Svg"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization";
|
||||
|
||||
export class ShareLinkViz implements SpecialVisualization {
|
||||
funcName = "share_link"
|
||||
|
@ -17,12 +17,12 @@ export class ShareLinkViz implements SpecialVisualization {
|
|||
},
|
||||
]
|
||||
|
||||
public constr(state, tagSource: UIEventSource<any>, args) {
|
||||
public constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[]) {
|
||||
if (window.navigator.share) {
|
||||
const generateShareData = () => {
|
||||
const title = state?.layoutToUse?.title?.txt ?? "MapComplete"
|
||||
const title = state?.layout?.title?.txt ?? "MapComplete"
|
||||
|
||||
let matchingLayer: LayerConfig = state?.layoutToUse?.getMatchingLayer(
|
||||
let matchingLayer: LayerConfig = state?.layout?.getMatchingLayer(
|
||||
tagSource?.data
|
||||
)
|
||||
let name =
|
||||
|
@ -41,7 +41,7 @@ export class ShareLinkViz implements SpecialVisualization {
|
|||
return {
|
||||
title: name,
|
||||
url: url,
|
||||
text: state?.layoutToUse?.shortDescription?.txt ?? "MapComplete",
|
||||
text: state?.layout?.shortDescription?.txt ?? "MapComplete",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { VariableUiElement } from "../Base/VariableUIElement"
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import EditableTagRendering from "./EditableTagRendering"
|
||||
import Combine from "../Base/Combine"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class StealViz implements SpecialVisualization {
|
||||
funcName = "steal"
|
||||
|
@ -21,12 +21,12 @@ export class StealViz implements SpecialVisualization {
|
|||
required: true,
|
||||
},
|
||||
]
|
||||
constr(state, featureTags, args) {
|
||||
constr(state: SpecialVisualizationState, featureTags, args) {
|
||||
const [featureIdKey, layerAndtagRenderingIds] = args
|
||||
const tagRenderings: [LayerConfig, TagRenderingConfig][] = []
|
||||
for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) {
|
||||
const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".")
|
||||
const layer = state.layoutToUse.layers.find((l) => l.id === layerId)
|
||||
const layer = state.layout.layers.find((l) => l.id === layerId)
|
||||
const tagRendering = layer.tagRenderings.find((tr) => tr.id === tagRenderingId)
|
||||
tagRenderings.push([layer, tagRendering])
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ export class StealViz implements SpecialVisualization {
|
|||
if (featureId === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const otherTags = state.allElements.getEventSourceById(featureId)
|
||||
const otherTags = state.featureProperties.getStore(featureId)
|
||||
const elements: BaseUIElement[] = []
|
||||
for (const [layer, tagRendering] of tagRenderings) {
|
||||
const el = new EditableTagRendering(
|
||||
|
|
|
@ -11,10 +11,9 @@ import { And } from "../../Logic/Tags/And"
|
|||
import Toggle from "../Input/Toggle"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export default class TagApplyButton implements AutoAction, SpecialVisualization {
|
||||
public readonly funcName = "tag_apply"
|
||||
|
@ -76,7 +75,10 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
return tgsSpec
|
||||
}
|
||||
|
||||
public static generateTagsToApply(spec: string, tagSource: Store<any>): Store<Tag[]> {
|
||||
public static generateTagsToApply(
|
||||
spec: string,
|
||||
tagSource: Store<Record<string, string>>
|
||||
): Store<Tag[]> {
|
||||
// Check whether we need to look up a single value
|
||||
|
||||
if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")) {
|
||||
|
@ -110,7 +112,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
|
||||
async applyActionOn(
|
||||
state: {
|
||||
layoutToUse: LayoutConfig
|
||||
layout: LayoutConfig
|
||||
changes: Changes
|
||||
},
|
||||
tags: UIEventSource<any>,
|
||||
|
@ -125,7 +127,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
new And(tagsToApply.data),
|
||||
tags.data, // We pass in the tags of the selected element, not the tags of the target element!
|
||||
{
|
||||
theme: state.layoutToUse.id,
|
||||
theme: state.layout.id,
|
||||
changeType: "answer",
|
||||
}
|
||||
)
|
||||
|
@ -133,8 +135,8 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
}
|
||||
|
||||
public constr(
|
||||
state: FeaturePipelineState,
|
||||
tags: UIEventSource<any>,
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
): BaseUIElement {
|
||||
const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags)
|
||||
|
@ -162,9 +164,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
const applyButton = new SubtleButton(
|
||||
image,
|
||||
new Combine([msg, tagsExplanation]).SetClass("flex flex-col")
|
||||
).onClick(() => {
|
||||
self.applyActionOn(state, tags, args)
|
||||
).onClick(async () => {
|
||||
applied.setData(true)
|
||||
await self.applyActionOn(state, tags, args)
|
||||
})
|
||||
|
||||
return new Toggle(
|
||||
|
|
0
UI/Popup/TagRenderingAnswer.svelte
Normal file
0
UI/Popup/TagRenderingAnswer.svelte
Normal file
|
@ -6,6 +6,7 @@ import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
|||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import Combine from "../Base/Combine"
|
||||
import Img from "../Base/Img"
|
||||
import { SpecialVisualisationState } from "../SpecialVisualization"
|
||||
|
||||
/***
|
||||
* Displays the correct value for a known tagrendering
|
||||
|
@ -14,7 +15,7 @@ export default class TagRenderingAnswer extends VariableUiElement {
|
|||
constructor(
|
||||
tagsSource: UIEventSource<any>,
|
||||
configuration: TagRenderingConfig,
|
||||
state: any,
|
||||
state: SpecialVisualisationState,
|
||||
contentClasses: string = "",
|
||||
contentStyle: string = "",
|
||||
options?: {
|
||||
|
@ -24,6 +25,7 @@ export default class TagRenderingAnswer extends VariableUiElement {
|
|||
if (configuration === undefined) {
|
||||
throw "Trying to generate a tagRenderingAnswer without configuration..."
|
||||
}
|
||||
UIEventSource
|
||||
if (tagsSource === undefined) {
|
||||
throw "Trying to generate a tagRenderingAnswer without tagSource..."
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@ import { Feature } from "geojson"
|
|||
import { Point } from "@turf/turf"
|
||||
import { GeoLocationPointProperties } from "../../Logic/State/GeoLocationState"
|
||||
import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
/**
|
||||
* Wrapper around 'UploadTraceToOsmUI'
|
||||
|
@ -14,15 +15,20 @@ export class UploadToOsmViz implements SpecialVisualization {
|
|||
"Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored."
|
||||
args = []
|
||||
|
||||
constr(state, featureTags, args) {
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
featureTags: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
) {
|
||||
function getTrace(title: string) {
|
||||
title = title?.trim()
|
||||
if (title === undefined || title === "") {
|
||||
title = "Uploaded with MapComplete"
|
||||
}
|
||||
title = Utils.EncodeXmlValue(title)
|
||||
const userLocations: Feature<Point, GeoLocationPointProperties>[] =
|
||||
state.historicalUserLocations.features.data.map((f) => f.feature)
|
||||
const userLocations = <Feature<Point, GeoLocationPointProperties>[]>(
|
||||
state.historicalUserLocations.features.data
|
||||
)
|
||||
const trackPoints: string[] = []
|
||||
for (const l of userLocations) {
|
||||
let trkpt = ` <trkpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">`
|
||||
|
|
|
@ -1,18 +1,70 @@
|
|||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import BaseUIElement from "./BaseUIElement"
|
||||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
|
||||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import FeatureSource, {
|
||||
IndexedFeatureSource,
|
||||
WritableFeatureSource,
|
||||
} from "../Logic/FeatureSource/FeatureSource"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../Logic/Osm/Changes"
|
||||
import { MapProperties } from "../Models/MapProperties"
|
||||
import LayerState from "../Logic/State/LayerState"
|
||||
import { Feature } from "geojson"
|
||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
|
||||
/**
|
||||
* The state needed to render a special Visualisation.
|
||||
*/
|
||||
export interface SpecialVisualizationState {
|
||||
readonly guistate: DefaultGuiState
|
||||
readonly layout: LayoutConfig
|
||||
|
||||
readonly layerState: LayerState
|
||||
readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> }
|
||||
|
||||
readonly indexedFeatures: IndexedFeatureSource
|
||||
|
||||
readonly historicalUserLocations: WritableFeatureSource
|
||||
|
||||
readonly osmConnection: OsmConnection
|
||||
readonly featureSwitchUserbadge: Store<boolean>
|
||||
readonly featureSwitchIsTesting: Store<boolean>
|
||||
readonly changes: Changes
|
||||
/**
|
||||
* State of the main map
|
||||
*/
|
||||
readonly mapProperties: MapProperties
|
||||
|
||||
readonly selectedElement: UIEventSource<Feature>
|
||||
|
||||
/**
|
||||
* If data is currently being fetched from external sources
|
||||
*/
|
||||
readonly dataIsLoading: Store<boolean>
|
||||
/**
|
||||
* Only needed for 'ReplaceGeometryAction'
|
||||
*/
|
||||
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
||||
|
||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||
readonly userRelatedState: { readonly mangroveIdentity: MangroveIdentity }
|
||||
}
|
||||
|
||||
export interface SpecialVisualization {
|
||||
funcName: string
|
||||
constr: (
|
||||
state: FeaturePipelineState,
|
||||
tagSource: UIEventSource<any>,
|
||||
argument: string[],
|
||||
guistate: DefaultGuiState
|
||||
) => BaseUIElement
|
||||
docs: string | BaseUIElement
|
||||
example?: string
|
||||
args: { name: string; defaultValue?: string; doc: string; required?: false | boolean }[]
|
||||
getLayerDependencies?: (argument: string[]) => string[]
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature
|
||||
): BaseUIElement
|
||||
}
|
||||
|
|
|
@ -44,15 +44,13 @@ import { LoginToggle } from "./Popup/LoginButton"
|
|||
import Toggle from "./Input/Toggle"
|
||||
import { SubstitutedTranslation } from "./SubstitutedTranslation"
|
||||
import List from "./Base/List"
|
||||
import { OsmFeature } from "../Models/OsmFeature"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import { GeoOperations } from "../Logic/GeoOperations"
|
||||
import StatisticsPanel from "./BigComponents/StatisticsPanel"
|
||||
import AutoApplyButton from "./Popup/AutoApplyButton"
|
||||
import { LanguageElement } from "./Popup/LanguageElement"
|
||||
import FeatureReviews from "../Logic/Web/MangroveReviews"
|
||||
import Maproulette from "../Logic/Maproulette"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||
|
||||
export default class SpecialVisualizations {
|
||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
||||
|
@ -146,63 +144,17 @@ export default class SpecialVisualizations {
|
|||
new MultiApplyViz(),
|
||||
new ExportAsGpxViz(),
|
||||
new AddNoteCommentViz(),
|
||||
new CloseNoteButton(),
|
||||
new PlantNetDetectionViz(),
|
||||
|
||||
new TagApplyButton(),
|
||||
|
||||
new ImportPointButton(),
|
||||
new ImportWayButton(),
|
||||
new ConflateButton(),
|
||||
new TagApplyButton(),
|
||||
new CloseNoteButton(),
|
||||
|
||||
new NearbyImageVis(),
|
||||
new MapillaryLinkVis(),
|
||||
new LanguageElement(),
|
||||
{
|
||||
funcName: "all_tags",
|
||||
docs: "Prints all key-value pairs of the object - used for debugging",
|
||||
args: [],
|
||||
constr: (state, tags: UIEventSource<any>) =>
|
||||
new SvelteUIElement(AllTagsPanel, { tags, state }),
|
||||
},
|
||||
{
|
||||
funcName: "image_carousel",
|
||||
docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)",
|
||||
args: [
|
||||
{
|
||||
name: "image_key",
|
||||
defaultValue: AllImageProviders.defaultKeys.join(","),
|
||||
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ",
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args) => {
|
||||
let imagePrefixes: string[] = undefined
|
||||
if (args.length > 0) {
|
||||
imagePrefixes = [].concat(...args.map((a) => a.split(",")))
|
||||
}
|
||||
return new ImageCarousel(
|
||||
AllImageProviders.LoadImagesFor(tags, imagePrefixes),
|
||||
tags,
|
||||
state
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "image_upload",
|
||||
docs: "Creates a button where a user can upload an image to IMGUR",
|
||||
args: [
|
||||
{
|
||||
name: "image-key",
|
||||
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
|
||||
defaultValue: "image",
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
doc: "The text to show on the button",
|
||||
defaultValue: "Add image",
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args) => {
|
||||
return new ImageUploadFlow(tags, state, args[0], args[1])
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
funcName: "wikipedia",
|
||||
docs: "A box showing the corresponding wikipedia article - based on the wikidata tag",
|
||||
|
@ -267,6 +219,56 @@ export default class SpecialVisualizations {
|
|||
})
|
||||
),
|
||||
},
|
||||
new MapillaryLinkVis(),
|
||||
new LanguageElement(),
|
||||
{
|
||||
funcName: "all_tags",
|
||||
docs: "Prints all key-value pairs of the object - used for debugging",
|
||||
args: [],
|
||||
constr: (state, tags: UIEventSource<any>) =>
|
||||
new SvelteUIElement(AllTagsPanel, { tags, state }),
|
||||
},
|
||||
{
|
||||
funcName: "image_carousel",
|
||||
docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)",
|
||||
args: [
|
||||
{
|
||||
name: "image_key",
|
||||
defaultValue: AllImageProviders.defaultKeys.join(","),
|
||||
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ",
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args) => {
|
||||
let imagePrefixes: string[] = undefined
|
||||
if (args.length > 0) {
|
||||
imagePrefixes = [].concat(...args.map((a) => a.split(",")))
|
||||
}
|
||||
return new ImageCarousel(
|
||||
AllImageProviders.LoadImagesFor(tags, imagePrefixes),
|
||||
tags,
|
||||
state
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "image_upload",
|
||||
docs: "Creates a button where a user can upload an image to IMGUR",
|
||||
args: [
|
||||
{
|
||||
name: "image-key",
|
||||
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
|
||||
defaultValue: "image",
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
doc: "The text to show on the button",
|
||||
defaultValue: "Add image",
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args) => {
|
||||
return new ImageUploadFlow(tags, state, args[0], args[1])
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "reviews",
|
||||
docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten",
|
||||
|
@ -283,14 +285,18 @@ export default class SpecialVisualizations {
|
|||
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args) => {
|
||||
constr: (state, tags, args, feature) => {
|
||||
const nameKey = args[0] ?? "name"
|
||||
let fallbackName = args[1]
|
||||
const feature = state.allElements.ContainingFeatures.get(tags.data.id)
|
||||
const mangrove = FeatureReviews.construct(feature, state, {
|
||||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
})
|
||||
const mangrove = FeatureReviews.construct(
|
||||
feature,
|
||||
tags,
|
||||
state.userRelatedState.mangroveIdentity,
|
||||
{
|
||||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
}
|
||||
)
|
||||
|
||||
const form = new ReviewForm((r) => mangrove.createReview(r), state)
|
||||
return new ReviewElement(mangrove, form)
|
||||
|
@ -348,7 +354,7 @@ export default class SpecialVisualizations {
|
|||
doc: "The path (or shorthand) that should be returned",
|
||||
},
|
||||
],
|
||||
constr: (state, tagSource: UIEventSource<any>, args) => {
|
||||
constr: (_, tagSource: UIEventSource<any>, args) => {
|
||||
const url = args[0]
|
||||
const shorthands = args[1]
|
||||
const neededValue = args[2]
|
||||
|
@ -380,7 +386,7 @@ export default class SpecialVisualizations {
|
|||
return undefined
|
||||
}
|
||||
const allUnits = [].concat(
|
||||
...(state?.layoutToUse?.layers?.map((lyr) => lyr.units) ?? [])
|
||||
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? [])
|
||||
)
|
||||
const unit = allUnits.filter((unit) =>
|
||||
unit.isApplicableToKey(key)
|
||||
|
@ -409,8 +415,8 @@ export default class SpecialVisualizations {
|
|||
).onClick(() => {
|
||||
console.log("Exporting as Geojson")
|
||||
const tags = tagSource.data
|
||||
const feature = state.allElements.ContainingFeatures.get(tags.id)
|
||||
const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags)
|
||||
const feature = state.indexedFeatures.featuresById.data.get(tags.id)
|
||||
const matchingLayer = state?.layout?.getMatchingLayer(tags)
|
||||
const title =
|
||||
matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson"
|
||||
const data = JSON.stringify(feature, null, " ")
|
||||
|
@ -429,15 +435,15 @@ export default class SpecialVisualizations {
|
|||
docs: "Opens the current view in the iD-editor",
|
||||
args: [],
|
||||
constr: (state, feature) => {
|
||||
return new OpenIdEditor(state, undefined, feature.data.id)
|
||||
return new OpenIdEditor(state.mapProperties, undefined, feature.data.id)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "open_in_josm",
|
||||
docs: "Opens the current view in the JOSM-editor",
|
||||
args: [],
|
||||
constr: (state, feature) => {
|
||||
return new OpenJosm(state)
|
||||
constr: (state) => {
|
||||
return new OpenJosm(state.osmConnection, state.mapProperties.bounds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -560,10 +566,10 @@ export default class SpecialVisualizations {
|
|||
docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'",
|
||||
example:
|
||||
"`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.",
|
||||
constr: (state, tagsSource) =>
|
||||
constr: (state, tagsSource, args, feature) =>
|
||||
new VariableUiElement(
|
||||
tagsSource.map((tags) => {
|
||||
const layer = state.layoutToUse.getMatchingLayer(tags)
|
||||
const layer = state.layout.getMatchingLayer(tags)
|
||||
const title = layer?.title?.GetRenderValue(tags)
|
||||
if (title === undefined) {
|
||||
return undefined
|
||||
|
@ -575,7 +581,7 @@ export default class SpecialVisualizations {
|
|||
{
|
||||
funcName: "maproulette_task",
|
||||
args: [],
|
||||
constr(state, tagSource, argument, guistate) {
|
||||
constr(state, tagSource) {
|
||||
let parentId = tagSource.data.mr_challengeId
|
||||
if (parentId === undefined) {
|
||||
console.warn("Element ", tagSource.data.id, " has no mr_challengeId")
|
||||
|
@ -666,7 +672,7 @@ export default class SpecialVisualizations {
|
|||
Number(maproulette_id),
|
||||
Number(status),
|
||||
{
|
||||
tags: `MapComplete MapComplete:${state.layoutToUse.id}`,
|
||||
tags: `MapComplete MapComplete:${state.layout.id}`,
|
||||
}
|
||||
)
|
||||
tagsSource.data["mr_taskStatus"] =
|
||||
|
@ -715,40 +721,19 @@ export default class SpecialVisualizations {
|
|||
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)
|
||||
return new Combine(
|
||||
state.layout.layers
|
||||
.filter((l) => l.name !== null)
|
||||
.map(
|
||||
(l) => {
|
||||
const fs = state.perLayer.get(l.id)
|
||||
const bbox = state.mapProperties.bounds.data
|
||||
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
|
||||
return new StatisticsPanel(fsBboxed)
|
||||
},
|
||||
[state.mapProperties.bounds]
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -777,7 +762,7 @@ export default class SpecialVisualizations {
|
|||
required: true,
|
||||
},
|
||||
],
|
||||
constr(state, tags, args) {
|
||||
constr(__, tags, args) {
|
||||
return new VariableUiElement(
|
||||
tags.map((tags) => {
|
||||
const [to, subject, body, button_text] = args.map((str) =>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { UIEventSource } from "../Logic/UIEventSource"
|
|||
import { Translation } from "./i18n/Translation"
|
||||
import Locale from "./i18n/Locale"
|
||||
import { FixedUiElement } from "./Base/FixedUiElement"
|
||||
import SpecialVisualizations from "./SpecialVisualizations"
|
||||
// import SpecialVisualizations from "./SpecialVisualizations"
|
||||
import { Utils } from "../Utils"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import Combine from "./Base/Combine"
|
||||
|
@ -10,13 +10,13 @@ import BaseUIElement from "./BaseUIElement"
|
|||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
|
||||
import LinkToWeblate from "./Base/LinkToWeblate"
|
||||
import { SpecialVisualization } from "./SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
|
||||
|
||||
export class SubstitutedTranslation extends VariableUiElement {
|
||||
public constructor(
|
||||
translation: Translation,
|
||||
tagsSource: UIEventSource<Record<string, string>>,
|
||||
state: FeaturePipelineState,
|
||||
state: SpecialVisualizationState,
|
||||
mapping: Map<
|
||||
string,
|
||||
| BaseUIElement
|
||||
|
@ -78,7 +78,7 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
}
|
||||
try {
|
||||
return viz.func
|
||||
.constr(state, tagsSource, proto.special.args, DefaultGuiState.state)
|
||||
.constr(state, tagsSource, proto.special.args)
|
||||
?.SetStyle(proto.special.style)
|
||||
} catch (e) {
|
||||
console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e)
|
||||
|
@ -125,7 +125,7 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
}
|
||||
|
||||
for (const knownSpecial of extraMappings.concat(
|
||||
SpecialVisualizations.specialVisualizations
|
||||
[] // TODO enable SpecialVisualizations.specialVisualizations
|
||||
)) {
|
||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||
const matched = template.match(
|
||||
|
@ -181,10 +181,10 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
console.warn(
|
||||
"Found a suspicious special rendering value in: ",
|
||||
template,
|
||||
" did you mean one of: ",
|
||||
SpecialVisualizations.specialVisualizations
|
||||
" did you mean one of: "
|
||||
/*SpecialVisualizations.specialVisualizations
|
||||
.map((sp) => sp.funcName + "()")
|
||||
.join(", ")
|
||||
.join(", ")*/
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,154 +1,66 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../Logic/UIEventSource";
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
||||
import { MapLibreAdaptor } from "./Map/MapLibreAdaptor";
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning";
|
||||
import { GeoLocationState } from "../Logic/State/GeoLocationState";
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters";
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState";
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler";
|
||||
import { Changes } from "../Logic/Osm/Changes";
|
||||
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor";
|
||||
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader";
|
||||
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater";
|
||||
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.js";
|
||||
import { BBox } from "../Logic/BBox";
|
||||
import ShowDataLayer from "./Map/ShowDataLayer";
|
||||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import type FeatureSource from "../Logic/FeatureSource/FeatureSource";
|
||||
import LayerState from "../Logic/State/LayerState";
|
||||
import Constants from "../Models/Constants";
|
||||
import type { Feature } from "geojson";
|
||||
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore";
|
||||
import ShowDataMultiLayer from "./Map/ShowDataMultiLayer";
|
||||
import { Or } from "../Logic/Tags/Or";
|
||||
import LayoutSource from "../Logic/FeatureSource/LayoutSource";
|
||||
import { type OsmTags } from "../Models/OsmFeature";
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import Filterview from "./BigComponents/Filterview.svelte";
|
||||
import RasterLayerPicker from "./Map/RasterLayerPicker.svelte";
|
||||
import ThemeViewState from "../Models/ThemeViewState";
|
||||
import type { MapProperties } from "../Models/MapProperties";
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
|
||||
import Translations from "./i18n/Translations";
|
||||
import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
|
||||
export let layout: LayoutConfig;
|
||||
const state = new ThemeViewState(layout);
|
||||
|
||||
const maplibremap: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
const initial = new InitialMapPositioning(layout);
|
||||
const mapproperties = new MapLibreAdaptor(maplibremap, initial);
|
||||
const geolocationState = new GeoLocationState();
|
||||
|
||||
const featureSwitches = new FeatureSwitchState(layout);
|
||||
|
||||
const osmConnection = new OsmConnection({
|
||||
dryRun: featureSwitches.featureSwitchIsTesting,
|
||||
fakeUser: featureSwitches.featureSwitchFakeUser.data,
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
"oauth_token",
|
||||
undefined,
|
||||
"Used to complete the login"
|
||||
),
|
||||
osmConfiguration: <"osm" | "osm-test">featureSwitches.featureSwitchApiURL.data
|
||||
});
|
||||
const userRelatedState = new UserRelatedState(osmConnection, layout?.language);
|
||||
const selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element");
|
||||
selectedElement.addCallbackAndRunD(s => console.log("Selected element:", s))
|
||||
const geolocation = new GeoLocationHandler(geolocationState, selectedElement, mapproperties, userRelatedState.gpsLocationHistoryRetentionTime);
|
||||
|
||||
const tags = new Or(layout.layers.filter(l => l.source !== null&& Constants.priviliged_layers.indexOf(l.id) < 0 && l.source.geojsonSource === undefined).map(l => l.source.osmTags ))
|
||||
const layerState = new LayerState(osmConnection, layout.layers, layout.id)
|
||||
|
||||
const indexedElements = new LayoutSource(layout.layers, featureSwitches, new StaticFeatureSource([]), mapproperties, osmConnection.Backend(),
|
||||
(id) => layerState.filteredLayers.get(id).isDisplayed
|
||||
)
|
||||
|
||||
const allElements = new FeaturePropertiesStore(indexedElements)
|
||||
const changes = new Changes({
|
||||
dryRun: featureSwitches.featureSwitchIsTesting,
|
||||
allElements: indexedElements,
|
||||
featurePropertiesStore: allElements,
|
||||
osmConnection,
|
||||
historicalUserLocations: geolocation.historicalUserLocations
|
||||
}, layout?.isLeftRightSensitive() ?? false);
|
||||
|
||||
new ShowDataMultiLayer(maplibremap, {
|
||||
layers: Array.from(layerState.filteredLayers.values()),
|
||||
features: indexedElements,
|
||||
fetchStore: id => <UIEventSource<OsmTags>> allElements.getStore(id),
|
||||
selectedElement,
|
||||
globalFilters: layerState.globalFilters
|
||||
})
|
||||
|
||||
|
||||
{
|
||||
// Various actors that we don't need to reference
|
||||
// TODO enable new TitleHandler(selectedElement,layout,allElements)
|
||||
new ChangeToElementsActor(changes, allElements);
|
||||
new PendingChangesUploader(changes, selectedElement);
|
||||
new SelectedElementTagsUpdater({
|
||||
allElements, changes, selectedElement, layoutToUse: layout, osmConnection
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Various initial setup
|
||||
userRelatedState.markLayoutAsVisited(layout);
|
||||
if(layout?.lockLocation){
|
||||
const bbox = new BBox(layout.lockLocation)
|
||||
mapproperties.maxbounds.setData(bbox)
|
||||
ShowDataLayer.showRange(
|
||||
maplibremap,
|
||||
new StaticFeatureSource([bbox.asGeoJson({})]),
|
||||
featureSwitches.featureSwitchIsTesting
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
type AddedByDefaultTypes = typeof Constants.added_by_default[number]
|
||||
/**
|
||||
* A listing which maps the layerId onto the featureSource
|
||||
*/
|
||||
const empty = []
|
||||
const specialLayers : Record<AddedByDefaultTypes | "current_view", FeatureSource> = {
|
||||
"home_location": userRelatedState.homeLocation,
|
||||
gps_location: geolocation.currentUserLocation,
|
||||
gps_location_history: geolocation.historicalUserLocations,
|
||||
gps_track: geolocation.historicalUserLocationsTrack,
|
||||
selected_element: new StaticFeatureSource(selectedElement.map(f => f === undefined ? empty : [f])),
|
||||
range: new StaticFeatureSource(mapproperties.maxbounds.map(bbox => bbox === undefined ? empty : <Feature[]> [bbox.asGeoJson({id:"range"})])) ,
|
||||
current_view: new StaticFeatureSource(mapproperties.bounds.map(bbox => bbox === undefined ? empty : <Feature[]> [bbox.asGeoJson({id:"current_view"})])),
|
||||
}
|
||||
layerState.filteredLayers.get("range")?.isDisplayed?.syncWith(featureSwitches.featureSwitchIsTesting, true)
|
||||
|
||||
specialLayers.range.features.addCallbackAndRun(fs => console.log("Range.features:", JSON.stringify(fs)))
|
||||
layerState.filteredLayers.forEach((flayer) => {
|
||||
const features = specialLayers[flayer.layerDef.id]
|
||||
if(features === undefined){
|
||||
return
|
||||
let selectedElementTags: Store<UIEventSource<Record<string, string>>> =
|
||||
state.selectedElement.mapD((f) => {
|
||||
return state.featureProperties.getStore(f.properties.id);
|
||||
}
|
||||
new ShowDataLayer(maplibremap, {
|
||||
features,
|
||||
doShowLayer: flayer.isDisplayed,
|
||||
layer: flayer.layerDef,
|
||||
selectedElement
|
||||
})
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let maplibremap: UIEventSource<MlMap> = state.map;
|
||||
let selectedElement: UIEventSource<Feature> = state.selectedElement;
|
||||
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer;
|
||||
let mapproperties: MapProperties = state.mapProperties;
|
||||
let featureSwitches: FeatureSwitchState = state.featureSwitches;
|
||||
let availableLayers = state.availableLayers;
|
||||
</script>
|
||||
|
||||
|
||||
<div class="h-screen w-screen absolute top-0 left-0 flex">
|
||||
<div id="fullscreen" class="transition-all transition-duration-500" style="border: 2px solid red">Hello world</div>
|
||||
<MaplibreMap class="w-full h-full border border-black" map={maplibremap}></MaplibreMap>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-0 left-0">
|
||||
<!-- Top-left elements -->
|
||||
<div class="absolute top-0 left-0 mt-2 ml-2">
|
||||
<MapControlButton on:click={() => state.guistate.welcomeMessageIsOpened.setData(true)}>
|
||||
<div class="flex mr-2 items-center">
|
||||
<img class="w-8 h-8 block mr-2" src={layout.icon}>
|
||||
<b>
|
||||
{layout.title}
|
||||
</b>
|
||||
</div>
|
||||
</MapControlButton>
|
||||
<MapControlButton on:click={() =>state.guistate.menuIsOpened.setData(true)}>
|
||||
<MenuIcon class="w-8 h-8"></MenuIcon>
|
||||
</MapControlButton>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0">
|
||||
<div class="absolute bottom-0 left-0 mb-4 ml-4">
|
||||
<MapControlButton on:click={() => state.guistate.filterViewIsOpened.setData(true)}>
|
||||
<ToSvelte class="w-7 h-7 block" construct={Svg.layers_ui}></ToSvelte>
|
||||
</MapControlButton>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 right-0 mb-4 mr-4">
|
||||
|
@ -160,11 +72,116 @@
|
|||
</MapControlButton>
|
||||
<If condition={featureSwitches.featureSwitchGeolocation}>
|
||||
<MapControlButton>
|
||||
<ToSvelte construct={() => new GeolocationControl(geolocation, mapproperties).SetClass("block w-8 h-8")}></ToSvelte>
|
||||
<ToSvelte
|
||||
construct={new GeolocationControl(state.geolocation, mapproperties).SetClass("block w-8 h-8")}></ToSvelte>
|
||||
</MapControlButton>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-0 right-0">
|
||||
<div class="absolute top-0 right-0 mt-4 mr-4">
|
||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||
<Geosearch bounds={state.mapProperties.bounds} layout={state.layout} location={state.mapProperties.location}
|
||||
{selectedElement} {selectedLayer}
|
||||
zoom={state.mapProperties.zoom}></Geosearch>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<If condition={state.guistate.filterViewIsOpened}>
|
||||
<div class="normal-background absolute bottom-0 left-0 flex flex-col">
|
||||
<div on:click={() => state.guistate.filterViewIsOpened.setData(false)}>Close</div>
|
||||
<!-- Filter panel -- TODO move to actual location-->
|
||||
{#each layout.layers as layer}
|
||||
<Filterview filteredLayer={state.layerState.filteredLayers.get(layer.id)}></Filterview>
|
||||
{/each}
|
||||
|
||||
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={state.guistate.welcomeMessageIsOpened}>
|
||||
<!-- Theme page -->
|
||||
<div class="absolute top-0 left-0 w-screen h-screen" style="background-color: #00000088">
|
||||
<div class="flex flex-col m-4 sm:m-6 md:m-8 p-4 sm:p-6 md:m-8 normal-background rounded">
|
||||
<div on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>Close</div>
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>About</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 2</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 3</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel class="flex flex-col">
|
||||
<ToSvelte construct={() => layout.description}></ToSvelte>
|
||||
{Translations.t.general.welcomeExplanation.general}
|
||||
{#if layout.layers.some((l) => l.presets?.length > 0)}
|
||||
<If condition={state.featureSwitches.featureSwitchAddNew}>
|
||||
{Translations.t.general.welcomeExplanation.addNew}
|
||||
</If>
|
||||
{/if}
|
||||
|
||||
<!--toTheMap,
|
||||
loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
|
||||
-->
|
||||
<ToSvelte construct= {() => layout.descriptionTail}></ToSvelte>
|
||||
<div class="m-x-8">
|
||||
<button class="subtle-background rounded w-full p-4">Explore the map</button>
|
||||
</div>
|
||||
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel>Content 2</TabPanel>
|
||||
<TabPanel>Content 3</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
|
||||
<If condition={state.guistate.menuIsOpened}>
|
||||
<!-- Menu page -->
|
||||
<div class="absolute top-0 left-0 w-screen h-screen" style="background-color: #00000088">
|
||||
<div class="flex flex-col m-4 sm:m-6 md:m-8 p-4 sm:p-6 md:m-8 normal-background rounded">
|
||||
<div on:click={() => state.guistate.menuIsOpened.setData(false)}>Close</div>
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>About MapComplete</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Settings</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Privacy</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel class="flex flex-col">
|
||||
About MC
|
||||
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel>User settings</TabPanel>
|
||||
<TabPanel>Privacy</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={selectedElement}>
|
||||
<div class="absolute top-0 right-0 normal-background">
|
||||
|
||||
<SelectedElementView layer={selectedLayer} {selectedElement}
|
||||
tags={selectedElementTags}></SelectedElementView>
|
||||
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<style>
|
||||
/* WARNING: This is just for demonstration.
|
||||
Using :global() in this way can be risky. */
|
||||
:global(.tab-selected) {
|
||||
background-color: rgb(59 130 246);
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
:global(.tab-unselected) {
|
||||
background-color: rgb(255 255 255);
|
||||
color: rgb(0 0 0);
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue