UX: fix #1805, disable zoom-in and zoom-out buttons when maxrange reached

This commit is contained in:
Pieter Vander Vennet 2024-03-04 15:31:09 +01:00
parent 346f45cff8
commit 48159b25f7
13 changed files with 202 additions and 184 deletions

View file

@ -439,6 +439,19 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.selectedElement.setData(feature)
}
public showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer {
const id = "gps_location"
const flayerGps = this.layerState.filteredLayers.get(id)
const features = this.geolocation.currentUserLocation
return new ShowDataLayer(map, {
features,
doShowLayer: flayerGps.isDisplayed,
layer: flayerGps.layerDef,
metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement,
})
}
/**
* Various small methods that need to be called
*/
@ -671,6 +684,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
)
return new SummaryTileSourceRewriter(summaryTileSource, this.layerState.filteredLayers)
}
/**
* Add the special layers to the map
*/

View file

@ -3,12 +3,14 @@
import { twJoin } from "tailwind-merge"
import { Translation } from "../i18n/Translation"
import { ariaLabel } from "../../Utils/ariaLabel"
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
/**
* A round button with an icon and possible a small text, which hovers above the map
*/
const dispatch = createEventDispatcher()
export let cls = "m-0.5 p-0.5 sm:p-1 md:m-1"
export let enabled : Store<boolean> = new ImmutableStore(true)
export let arialabel: Translation = undefined
</script>
@ -16,7 +18,7 @@
on:click={(e) => dispatch("click", e)}
on:keydown
use:ariaLabel={arialabel}
class={twJoin("pointer-events-auto relative h-fit w-fit rounded-full", cls)}
class={twJoin("pointer-events-auto relative h-fit w-fit rounded-full", cls, $enabled ? "" : "disabled")}
>
<slot />
</button>

View file

@ -1,31 +1,17 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import BaseUIElement from "../BaseUIElement"
import Img from "./Img"
import { twJoin, twMerge } from "tailwind-merge"
import { twMerge } from "tailwind-merge"
export let imageUrl: string | BaseUIElement = undefined
export const message: string | BaseUIElement = undefined
export let options: {
imgSize?: string
extraClasses?: string
} = {}
let imgClasses = twJoin("block justify-center shrink-0 mr-4", options?.imgSize ?? "h-11 w-11")
const dispatch = createEventDispatcher<{ click }>()
</script>
<button
class={twMerge(options.extraClasses, "secondary no-image-background")}
on:click={(e) => dispatch("click", e)}
on:click
>
<slot name="image">
{#if imageUrl !== undefined}
{#if typeof imageUrl === "string"}
<Img src={imageUrl} class={imgClasses} />
{/if}
{/if}
</slot>
<slot name="image" />
<slot name="message" />
</button>

View file

@ -53,9 +53,6 @@
lat: number
}>(undefined)
const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>()
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let initialMapProperties: Partial<MapProperties> & { location } = {
zoom: new UIEventSource<number>(19),
@ -73,6 +70,7 @@
minzoom: new UIEventSource<number>(18),
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
}
state?.showCurrentLocationOn(map)
if (targetLayer) {
const featuresForLayer = state.perLayer.get(targetLayer.id)
@ -120,7 +118,7 @@
<LocationInput
{map}
on:click={(data) => dispatch("click", data)}
on:click
mapProperties={initialMapProperties}
value={preciseLocation}
initialCoordinate={coordinate}

View file

@ -23,18 +23,20 @@
import { GeoOperations } from "../../Logic/GeoOperations"
import { BBox } from "../../Logic/BBox"
import type { Feature, LineString, Point } from "geojson"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import SmallZoomButtons from "../Map/SmallZoomButtons.svelte"
const splitpoint_style = new LayerConfig(
<LayerConfigJson>split_point,
"(BUILTIN) SplitRoadWizard.ts",
true
) as const
true,
)
const splitroad_style = new LayerConfig(
<LayerConfigJson>split_road,
"(BUILTIN) SplitRoadWizard.ts",
true
) as const
true,
)
/**
* The way to focus on
@ -45,6 +47,7 @@
* A default is given
*/
export let layer: LayerConfig = splitroad_style
export let state: SpecialVisualizationState | undefined = undefined
/**
* Optional: use these properties to set e.g. background layer
*/
@ -58,6 +61,7 @@
adaptor.bounds.setData(BBox.get(wayGeojson).pad(2))
adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2))
state?.showCurrentLocationOn(map)
new ShowDataLayer(map, {
features: new StaticFeatureSource([wayGeojson]),
drawMarkers: false,
@ -101,6 +105,7 @@
})
</script>
<div class="h-full w-full">
<div class="h-full w-full relative">
<MaplibreMap {map} />
<SmallZoomButtons {adaptor} />
</div>

View file

@ -13,6 +13,7 @@
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { createEventDispatcher, onDestroy } from "svelte"
import Move_arrows from "../../../assets/svg/Move_arrows.svelte"
import SmallZoomButtons from "../../Map/SmallZoomButtons.svelte"
/**
* A visualisation to pick a location on a map background
@ -95,4 +96,5 @@
</div>
<DragInvitation hideSignal={mla.location} />
<SmallZoomButtons adaptor={mla} />
</div>

View file

@ -0,0 +1,29 @@
<script lang="ts">
import Translations from "../i18n/Translations.js";
import Min from "../../assets/svg/Min.svelte";
import MapControlButton from "../Base/MapControlButton.svelte";
import Plus from "../../assets/svg/Plus.svelte";
import type { MapProperties } from "../../Models/MapProperties"
export let adaptor: MapProperties
let canZoomIn = adaptor.maxzoom.map(mz => adaptor.zoom.data < mz, [adaptor.zoom] )
let canZoomOut = adaptor.minzoom.map(mz => adaptor.zoom.data > mz, [adaptor.zoom] )
</script>
<div class="absolute bottom-0 right-0 pointer-events-none flex flex-col">
<MapControlButton
enabled={canZoomIn}
cls="m-0.5 p-1"
arialabel={Translations.t.general.labels.zoomIn}
on:click={() => adaptor.zoom.update((z) => z + 1)}
>
<Plus class="h-5 w-5" />
</MapControlButton>
<MapControlButton
enabled={canZoomOut}
cls={"m-0.5 p-1"}
arialabel={Translations.t.general.labels.zoomOut}
on:click={() => adaptor.zoom.update((z) => z - 1)}
>
<Min class="h-5 w-5" />
</MapControlButton>
</div>

View file

@ -0,0 +1,112 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import type { Feature, Point } from "geojson"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import LoginToggle from "../Base/LoginToggle.svelte"
import Tr from "../Base/Tr.svelte"
import Scissors from "../../assets/svg/Scissors.svelte"
import WaySplitMap from "../BigComponents/WaySplitMap.svelte"
import BackButton from "../Base/BackButton.svelte"
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
import Translations from "../i18n/Translations"
import NextButton from "../Base/NextButton.svelte"
import Loading from "../Base/Loading.svelte"
import { OsmWay } from "../../Logic/Osm/OsmObject"
import type { WayId } from "../../Models/OsmFeature"
import { Utils } from "../../Utils"
export let state: SpecialVisualizationState
export let id: WayId
const t = Translations.t.split
let step: "initial" | "loading_way" | "splitting" | "applying_split" | "has_been_split" | "deleted" = "initial"
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
let splitPoints = new UIEventSource<Feature<
Point,
{
id: number
index: number
dist: number
location: number
}
>[]>([])
let splitpointsNotEmpty = splitPoints.map(sp => sp.length > 0)
let osmWay: OsmWay
async function downloadWay() {
step = "loading_way"
const dloaded = await state.osmObjectDownloader.DownloadObjectAsync(id)
if (dloaded === "deleted") {
step = "deleted"
return
}
osmWay = dloaded
step = "splitting"
}
async function doSplit() {
step = "applying_split"
const splitAction = new SplitAction(
id,
splitPoints.data.map((ff) => <[number, number]>(<Point>ff.geometry).coordinates),
{
theme: state?.layout?.id,
},
5,
)
await state.changes?.applyAction(splitAction)
// We throw away the old map and splitpoints, and create a new map from scratch
splitPoints.setData([])
// Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219
state.selectedElement?.setData(undefined)
step = "has_been_split"
}
</script>
<LoginToggle ignoreLoading={true} {state}>
<Tr slot="not-logged-in" t={t.loginToSplit} />
{#if step === "deleted"}
<!-- Empty -->
{:else if step === "initial"}
<button on:click={() => downloadWay()}>
<Scissors class="w-6 h-6 shrink-0" />
<Tr t={t.inviteToSplit} />
</button>
{:else if step === "loading_way"}
<Loading />
{:else if step === "splitting"}
<div class="flex flex-col interactive border-interactive p-2">
<div class="w-full h-80">
<WaySplitMap {state} {splitPoints} {osmWay} />
</div>
<div class="flex flex-wrap-reverse md:flex-nowrap w-full">
<BackButton clss="w-full" on:click={() => {
splitPoints.set([])
step = "initial"
}}>
<Tr t={Translations.t.general.cancel} />
</BackButton>
<NextButton clss={ ($splitpointsNotEmpty ? "": "disabled ") + "w-full primary"} on:click={() => doSplit()}>
<Tr t={t.split} />
</NextButton>
</div>
</div>
{:else if step === "has_been_split"}
<Tr cls="thanks" t={ t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full")} />
<button on:click={() => downloadWay()}>
<Scissors class="w-6 h-6" />
<Tr t={t.splitAgain} />
</button>
{/if}
</LoginToggle>

View file

@ -1,147 +0,0 @@
import Toggle from "../Input/Toggle"
import { UIEventSource } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
import { Button } from "../Base/Button"
import Translations from "../i18n/Translations"
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
import Title from "../Base/Title"
import BaseUIElement from "../BaseUIElement"
import { VariableUiElement } from "../Base/VariableUIElement"
import { LoginToggle } from "./LoginButton"
import SvelteUIElement from "../Base/SvelteUIElement"
import WaySplitMap from "../BigComponents/WaySplitMap.svelte"
import { Feature, Point } from "geojson"
import { WayId } from "../../Models/OsmFeature"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Changes } from "../../Logic/Osm/Changes"
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
import Scissors from "../../assets/svg/Scissors.svelte"
export default class SplitRoadWizard extends Combine {
public dialogIsOpened: UIEventSource<boolean>
/**
* A UI Element used for splitting roads
*
* @param id The id of the road to remove
* @param state the state of the application
*/
constructor(
id: WayId,
state: {
layout?: LayoutConfig
osmConnection?: OsmConnection
osmObjectDownloader?: OsmObjectDownloader
changes?: Changes
indexedFeatures?: IndexedFeatureSource
selectedElement?: UIEventSource<Feature>
}
) {
const t = Translations.t.split
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
const splitPoints = new UIEventSource<Feature<Point>[]>([])
const hasBeenSplit = new UIEventSource(false)
// Toggle variable between show split button and map
const splitClicked = new UIEventSource<boolean>(false)
const leafletMap = new UIEventSource<BaseUIElement>(undefined)
function initMap() {
;(async function (
id: WayId,
splitPoints: UIEventSource<Feature[]>
): Promise<BaseUIElement> {
return new SvelteUIElement(WaySplitMap, {
osmWay: await state.osmObjectDownloader.DownloadObjectAsync(id),
splitPoints,
})
})(id, splitPoints).then((mapComponent) =>
leafletMap.setData(mapComponent.SetClass("w-full h-80"))
)
}
// Toggle between splitmap
const splitButton = new SubtleButton(
new SvelteUIElement(Scissors).SetClass("h-6 w-6"),
new Toggle(
t.splitAgain.Clone().SetClass("text-lg font-bold"),
t.inviteToSplit.Clone().SetClass("text-lg font-bold"),
hasBeenSplit
)
)
const splitToggle = new LoginToggle(splitButton, t.loginToSplit.Clone(), state)
// Save button
const saveButton = new Button(t.split.Clone(), async () => {
hasBeenSplit.setData(true)
splitClicked.setData(false)
const splitAction = new SplitAction(
id,
splitPoints.data.map((ff) => <[number, number]>(<Point>ff.geometry).coordinates),
{
theme: state?.layout?.id,
},
5
)
await state.changes?.applyAction(splitAction)
// We throw away the old map and splitpoints, and create a new map from scratch
splitPoints.setData([])
// Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219
state.selectedElement?.setData(undefined)
})
saveButton.SetClass("btn btn-primary mr-3")
const disabledSaveButton = new Button(t.split.Clone(), undefined)
disabledSaveButton.SetClass("btn btn-disabled mr-3")
// Only show the save button if there are split points defined
const saveToggle = new Toggle(
disabledSaveButton,
saveButton,
splitPoints.map((data) => data.length === 0)
)
const cancelButton = Translations.t.general.cancel
.Clone() // Not using Button() element to prevent full width button
.SetClass("btn btn-secondary mr-3")
.onClick(() => {
splitPoints.setData([])
splitClicked.setData(false)
})
cancelButton.SetClass("btn btn-secondary block")
const splitTitle = new Title(t.splitTitle)
const mapView = new Combine([
splitTitle,
new VariableUiElement(leafletMap),
new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"),
])
mapView.SetClass("question")
super([
Toggle.If(hasBeenSplit, () =>
t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full")
),
new Toggle(mapView, splitToggle, splitClicked),
])
splitClicked.addCallback((view) => {
if (view) {
initMap()
}
})
this.dialogIsOpened = splitClicked
const self = this
splitButton.onClick(() => {
splitClicked.setData(true)
self.ScrollIntoView()
})
}
}

View file

@ -22,6 +22,8 @@ import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { SummaryTileSourceRewriter } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
import { Map as MlMap } from "maplibre-gl"
import ShowDataLayer from "./Map/ShowDataLayer"
/**
* The state needed to render a special Visualisation.
@ -86,6 +88,8 @@ export interface SpecialVisualizationState {
readonly previewedImage: UIEventSource<ProvidedImage>
readonly geolocation: GeoLocationHandler
showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer
}
export interface SpecialVisualization {

View file

@ -46,8 +46,6 @@ import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
import UserProfile from "./BigComponents/UserProfile.svelte"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import { WayId } from "../Models/OsmFeature"
import SplitRoadWizard from "./Popup/SplitRoadWizard"
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"
@ -93,6 +91,7 @@ import SpecialVisualisationUtils from "./SpecialVisualisationUtils"
import LoginButton from "./Base/LoginButton.svelte"
import Toggle from "./Input/Toggle"
import ImportReviewIdentity from "./Reviews/ImportReviewIdentity.svelte"
import SplitRoadWizard from "./Popup/SplitRoadWizard.svelte"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -432,7 +431,7 @@ export default class SpecialVisualizations {
.map((tags) => tags.id)
.map((id) => {
if (id.startsWith("way/")) {
return new SplitRoadWizard(<WayId>id, state)
return new SvelteUIElement(SplitRoadWizard, { id, state })
}
return undefined
})
@ -741,12 +740,20 @@ export default class SpecialVisualizations {
{
funcName: "import_mangrove_key",
docs: "Only makes sense in the usersettings. Allows to import a mangrove public key and to use this to make reviews",
args: [{
name: "text",
doc: "The text that is shown on the button",
}],
args: [
{
name: "text",
doc: "The text that is shown on the button",
},
],
needsUrls: [],
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
const [text] = argument
return new SvelteUIElement(ImportReviewIdentity, { state, text })
},

View file

@ -118,7 +118,8 @@
let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
let mapproperties: MapProperties = state.mapProperties
state.mapProperties.installCustomKeyboardHandler(viewport)
let canZoomIn = mapproperties.maxzoom.map(mz => mapproperties.zoom.data < mz, [mapproperties.zoom] )
let canZoomOut = mapproperties.minzoom.map(mz => mapproperties.zoom.data > mz, [mapproperties.zoom] )
function updateViewport() {
const rect = viewport.data?.getBoundingClientRect()
if (!rect) {
@ -329,12 +330,14 @@
</If>
<MapControlButton
arialabel={Translations.t.general.labels.zoomIn}
enabled={canZoomIn}
on:click={() => mapproperties.zoom.update((z) => z + 1)}
on:keydown={forwardEventToMap}
>
<Plus class="h-8 w-8" />
</MapControlButton>
<MapControlButton
enabled={canZoomOut}
arialabel={Translations.t.general.labels.zoomOut}
on:click={() => mapproperties.zoom.update((z) => z - 1)}
on:keydown={forwardEventToMap}

View file

@ -1390,7 +1390,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
d.setUTCMinutes(0)
}
public static scrollIntoView(element: HTMLBaseElement | HTMLDivElement) {
public static scrollIntoView(element: HTMLBaseElement | HTMLDivElement): void {
if (!element) {
return
}
// Is the element completely in the view?
const parentRect = Utils.findParentWithScrolling(element)?.getBoundingClientRect()
if (!parentRect) {