Improve 'slopes' input, add compass indicator

This commit is contained in:
Pieter Vander Vennet 2023-12-16 01:29:42 +01:00
parent b7175384f9
commit 7a3cb9fbdd
17 changed files with 268 additions and 108 deletions

View file

@ -281,11 +281,17 @@
"openTheMap": "Open the map",
"openTheMapAtGeolocation": "Zoom to your location",
"opening_hours": {
"all_days_from": "Opened every day {ranges}",
"closed_permanently": "Closed for an unknown duration",
"closed_until": "Closed until {date}",
"error": "Could not parse the opening hours",
"error_loading": "Error: could not visualize these opening hours.",
"friday": "On friday {ranges}",
"loadingCountry": "Determining country…",
"monday": "On monday {ranges}",
"not_all_rules_parsed": "These opening hours are complicated. The following rules are ignored in the input element:",
"on_weekdays": "Opened on weekdays {ranges}",
"on_weekends": "Opened on weekends {ranges}",
"openTill": "till",
"open_24_7": "Open around the clock",
"open_during_ph": "During a public holiday, this is",
@ -293,7 +299,15 @@
"ph_closed": "closed",
"ph_not_known": " ",
"ph_open": "open",
"ph_open_as_usual": "open, as usual"
"ph_open_as_usual": "open, as usual",
"ranges": "from {starttime} till {endtime}",
"rangescombined": "{range0} and {range1}",
"saturday": "On saturday {ranges}",
"sunday": "On sunday {ranges}",
"thursday": "On thursday {ranges}",
"tuesday": "On tuesday {ranges}",
"unknown": "The opening hours are unkown",
"wednesday": "On wednesday {ranges}"
},
"osmLinkTooltip": "Browse this object on OpenStreetMap for history and more editing options",
"pdf": {

View file

@ -551,6 +551,9 @@ export class OsmConnection {
private UpdateCapabilities(): void {
const self = this
if (this.fakeUser) {
return
}
this.FetchCapabilities().then(({ api, gpx }) => {
self.apiIsOnline.setData(api)
self.gpxServiceIsOnline.setData(gpx)

View file

@ -0,0 +1,80 @@
import { UIEventSource } from "../UIEventSource"
/**
* Exports the device orientation as UIEventSources and detects 'shakes'
*/
export class Orientation {
public static singleton = new Orientation()
public gotMeasurement: UIEventSource<boolean> = new UIEventSource<boolean>(false)
/**
* The direction wrt to the magnetic north, with clockwise = positive.
* 0 degrees is pointing towards the north
* 90° is east,
* 180° is south
* 270° is west
*
* Note that this is the opposite of what the DeviceOrientationEvent uses!
* */
public alpha: UIEventSource<number> = new UIEventSource<number>(undefined)
public beta: UIEventSource<number> = new UIEventSource<number>(undefined)
public gamma: UIEventSource<number> = new UIEventSource<number>(undefined)
/**
* Indicates if 'alpha' is with the actual magnetic field or just mimicks that
*/
public absolute: UIEventSource<boolean> = new UIEventSource<boolean>(undefined)
/**
* A derivate of beta and gamma
* An arrow pointing up, rotated with this amount should more or less point towards the sky
* Used in the slope input
*/
public arrowDirection: UIEventSource<number> = new UIEventSource(undefined)
private _measurementsStarted = false
constructor() {
this.fakeMeasurements()
}
public fakeMeasurements() {
this.alpha.setData(45)
this.beta.setData(20)
this.gamma.setData(30)
this.absolute.setData(true)
this.gotMeasurement.setData(true)
}
public startMeasurements() {
if (this._measurementsStarted) {
return
}
this._measurementsStarted = true
console.log("Starting device orientation listener")
try {
window.addEventListener("deviceorientationabsolute", (e: DeviceOrientationEvent) =>
this.update(e)
)
} catch (e) {
console.log("Could not init device orientation api due to", e)
}
}
private update(event: DeviceOrientationEvent) {
this.gotMeasurement.setData(true)
// IF the phone is lying flat, then:
// alpha is the compass direction (but not absolute)
// beta is tilt if you would lift the phone towards you
// gamma is rotation if you rotate the phone along the long axis
// Note: the event uses _counterclockwise_ = positive for alpha
// However, we use _clockwise_ = positive throughout the application, so we use '-' here!
this.alpha.setData(Math.floor(360 - event.alpha))
this.beta.setData(Math.floor(event.beta))
this.gamma.setData(Math.floor(event.gamma))
this.absolute.setData(event.absolute)
if (this.beta.data < 0) {
this.arrowDirection.setData(this.gamma.data + 180)
} else {
this.arrowDirection.setData(-this.gamma.data)
}
}
}

View file

@ -518,6 +518,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
if (this.selectedElement.data !== undefined) {
return false
}
if (
this.guistate.menuIsOpened.data ||
this.guistate.themeIsOpened.data ||
this.previewedImage.data !== undefined
) {
return
}
this.selectClosestAtCenter(0)
}
)

View file

@ -8,7 +8,7 @@
* A round button with an icon and possible a small text, which hovers above the map
*/
const dispatch = createEventDispatcher()
export let cls = ""
export let cls = "m-0.5 p-0.5 sm:p-1 md:m-1"
export let arialabel: Translation = undefined
</script>
@ -16,7 +16,7 @@
on:click={(e) => dispatch("click", e)}
on:keydown
use:ariaLabel={arialabel}
class={twJoin("pointer-events-auto m-0.5 h-fit w-fit rounded-full p-0.5 sm:p-1 md:m-1", cls)}
class={twJoin("pointer-events-auto h-fit w-fit rounded-full", cls)}
>
<slot />
</button>

View file

@ -92,9 +92,9 @@ export class GeolocationControl extends VariableUiElement {
this._lastClickWithinThreeSecs = lastClickWithinThreeSecs
this.onClick(() => this.handleClick())
Hotkeys.RegisterHotkey({ nomod: "L" }, Translations.t.hotkeyDocumentation.geolocate, () =>
Hotkeys.RegisterHotkey({ nomod: "L" }, Translations.t.hotkeyDocumentation.geolocate, () => {
this.handleClick()
)
})
lastClick.addCallbackAndRunD((_) => {
window.setTimeout(() => {

View file

@ -3,6 +3,8 @@
import type { SpecialVisualizationState } from "../SpecialVisualization"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
import { GeoOperations } from "../../Logic/GeoOperations"
import { Store } from "../../Logic/UIEventSource"
export let state: SpecialVisualizationState
export let feature: Feature
@ -16,11 +18,22 @@
state.selectedLayer.setData(layer)
state.selectedElement.setData(feature)
}
let bearingAndDist: Store<{ bearing: number, dist: number }> = state.mapProperties.location.map(l => {
let fcenter = GeoOperations.centerpointCoordinates(feature)
let mapCenter = [l.lon, l.lat]
let bearing = Math.round(GeoOperations.bearing(fcenter, mapCenter))
let dist = Math.round(GeoOperations.distanceBetween(fcenter, mapCenter))
return { bearing, dist }
},
)
</script>
<button class="cursor-pointer small" on:click={() => select()}>
<button class="cursor-pointer small flex" on:click={() => select()}>
{#if i !== undefined}
<span class="font-bold">{i + 1}.</span>
{/if}
<TagRenderingAnswer config={layer.title} {layer} selectedElement={feature} {state} {tags} />
<span class="flex">{$bearingAndDist.dist}m {$bearingAndDist.bearing}°</span>
</button>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { Orientation } from "../../Logic/Web/Orientation"
const o = Orientation.singleton
let alpha = o.alpha
let beta = o.beta
let gamma = o.gamma
let absolute = o.absolute
let gotMeasurement = o.gotMeasurement
o.startMeasurements()
</script>
{#if !$gotMeasurement}
No device orientation data available
{:else}
Device orientation data:
<ol>
<li>Alpha: {$alpha}</li>
<li>Beta: {$beta}</li>
<li>Gamma: {$gamma}</li>
<li>Absolute?: {$absolute}</li>
</ol>
{/if}

View file

@ -1,112 +1,92 @@
<script lang="ts">
import { UIEventSource } from "../../../Logic/UIEventSource"
import { ArrowUpIcon } from "@babeard/svelte-heroicons/mini"
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import { Orientation } from "../../../Logic/Web/Orientation"
import type { Feature } from "geojson"
import { GeoOperations } from "../../../Logic/GeoOperations"
import If from "../../Base/If.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
export let value: UIEventSource<string>
export let value: UIEventSource<string> = new UIEventSource<string>(undefined)
export let mode: "degrees" | "percentage" = "percentage"
export let preview: UIEventSource<string> = new UIEventSource<string>(undefined)
export let feature: Feature = undefined
export let state: SpecialVisualizationState = undefined
let previewMode: "degrees" | "percentage" = mode
function oppMode(m: "degrees" | "percentage"): "percentage" | "degrees" {
if (m === "degrees") {
return "percentage"
}
return "degrees"
let featureBearing: number = 45
if (feature?.geometry?.type === "LineString") {
/* Bearing between -180 and + 180, positive is clockwise*/
featureBearing = Math.round(GeoOperations.bearing(
feature.geometry.coordinates[0],
feature.geometry.coordinates.at(-1),
))
}
let previewDegrees: UIEventSource<string> = new UIEventSource<string>(undefined)
let previewPercentage: UIEventSource<string> = new UIEventSource<string>(undefined)
function degreesToPercentage(beta: number): string {
const perc = Math.tan(beta * Math.PI / 180) * 100
const rounded = Math.round(perc / 5) * 5
const rounded = Math.round(perc / 2.5) * 2.5
return rounded + "%"
}
export let safetyMargin: number = 10
if (safetyMargin < 5) {
throw "Safetymargin should be at least 5, it is " + JSON.stringify(safetyMargin)
}
const orientation = Orientation.singleton
orientation.startMeasurements()
const alpha = orientation.alpha
const beta = orientation.beta
let alpha = new UIEventSource<number>(undefined)
let beta = new UIEventSource<number>(undefined)
let gamma = new UIEventSource<number>(45)
let abs = new UIEventSource<number>(undefined)
let gotMeasurement = orientation.gotMeasurement
let gotMeasurement = new UIEventSource(false)
let arrowDirection: number = undefined
function handleOrientation(event) {
gotMeasurement.setData(true)
// IF the phone is lying flat, then:
// alpha is the compass direction (but not absolute)
// beta is tilt if you would lift the phone towards you
// gamma is rotation if you rotate the phone along the long axis
alpha.setData(Math.floor(event.alpha))
beta.setData(Math.floor(event.beta))
gamma.setData(Math.floor(event.gamma))
abs.setData((event.absolute))
if (beta.data < 0) {
arrowDirection = gamma.data + 180
} else {
arrowDirection = -gamma.data
let valuesign = alpha.map(phoneBearing => {
if (featureBearing === undefined) {
return 1
}
}
// are we going _with_ or _against_ the direction of the feature?
console.log("Starting device orientation listener")
try {
window.addEventListener("deviceorientation", e => handleOrientation(e))
} catch (e) {
console.log("Could not init device orientation api due to", e)
}
if (featureBearing < 0) {
featureBearing += 360
}
let relativeAngle = Math.abs(featureBearing - phoneBearing) % 360
if (relativeAngle < 90 || relativeAngle > 270) {
return 1
} else {
return -1
}
})
beta.map(beta => {
if (-safetyMargin < arrowDirection && arrowDirection < safetyMargin) {
if (mode === "degrees") {
value.setData("" + beta + "°")
} else {
value.setData(degreesToPercentage(beta))
}
if (previewMode === "degrees") {
preview.setData("" + beta + "°")
} else {
preview.setData(degreesToPercentage(beta))
}
// As one moves forward on a way, a positive incline gets higher, and a negative incline gets lower.
let valueSign = valuesign.data
if (mode === "degrees") {
value.setData(valueSign * beta + "°")
} else {
value.setData(undefined)
value.setData(degreesToPercentage(valueSign * beta))
}
}, [beta])
previewDegrees.setData(beta + "°")
previewPercentage.setData(degreesToPercentage(beta))
}, [valuesign, beta])
</script>
{#if $gotMeasurement}
<div class="flex flex-col m-2">
<div class="flex w-full">
<div class="shrink-0 relative w-32 h-32 p-0 m-0 overflow-hidden"
style="border-radius: 9999px; background: greenyellow">
<div class="absolute top-0 left-0 w-16 h-16 interactive"
style={`transform: rotate( ${-safetyMargin}deg ); transform-origin: 100% 100%`} />
<div class="absolute top-0 left-0 w-16 h-16 interactive"
style={`transform: rotate( ${90+safetyMargin}deg ); transform-origin: 100% 100%`} />
<div class="absolute top-0 mt-8 left-0 w-32 h-32 interactive" />
<div class="absolute w-30 h-30 top-0 left-0 rounded-full">
<ArrowUpIcon class="" style={`transform: rotate( ${arrowDirection}deg )`} />
<div class="font-bold w-full flex justify-around items-center text-5xl">
<div>
{$previewDegrees}
</div>
<div>
{$previewPercentage}
</div>
</div>
<div class="font-bold w-full flex justify-center items-center">
{#if $value}
<div class="text-5xl" on:click={() => {previewMode = oppMode(previewMode)}}>
{$preview}
</div>
{:else}
<Tr cls="alert" t={Translations.t.validation.slope.inputIncorrect} />
{/if}
</div>
</div>
@ -114,5 +94,16 @@
<div>
<Tr t={Translations.t.validation.slope.inputExplanation} />
</div>
<If condition={state?.featureSwitchIsTesting ?? new ImmutableStore(true)}>
<span class="subtle">
Way: {featureBearing}°, compass: {$alpha}°, diff: {(featureBearing - $alpha)}
{#if $valuesign === 1}
Forward
{:else}
Backward
{/if}
</span>
</If>
</div>
{/if}

View file

@ -9,7 +9,6 @@
import InputHelpers from "./InputHelpers"
import ToSvelte from "../Base/ToSvelte.svelte"
import type { Feature } from "geojson"
import { createEventDispatcher } from "svelte"
import ImageHelper from "./Helpers/ImageHelper.svelte"
import TranslationInput from "./Helpers/TranslationInput.svelte"
import TagInput from "./Helpers/TagInput.svelte"
@ -19,17 +18,16 @@
import ColorInput from "./Helpers/ColorInput.svelte"
import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"
import SlopeInput from "./Helpers/SlopeInput.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
export let type: ValidatorType
export let value: UIEventSource<string | object>
export let feature: Feature
export let args: (string | number | boolean)[] = undefined
export let state: SpecialVisualizationState
let properties = { feature, args: args ?? [] }
let dispatch = createEventDispatcher<{
selected
}>()
</script>
{#if type === "translation"}
@ -49,7 +47,7 @@
{:else if type === "opening_hours"}
<OpeningHoursInput {value} />
{:else if type === "slope"}
<SlopeInput {value} />
<SlopeInput {value} {feature} {state} />
{:else if type === "wikidata"}
<ToSvelte construct={() => InputHelpers.constructWikidataHelper(value, properties)} />
{/if}

View file

@ -1,8 +1,13 @@
import NatValidator from "./NatValidator"
import FloatValidator from "./FloatValidator"
export default class SlopeValidator extends NatValidator {
export default class SlopeValidator extends FloatValidator {
constructor() {
super("slope", "Validates that the slope is a valid number")
super(
"slope",
"Validates that the slope is a valid number." +
"The accompanying input element uses the gyroscope and the compass to determine the correct incline. The sign of the incline will be set automatically. The bearing of the way is compared to the bearing of the compass, as such, the device knows if it is measuring in the forward or backward direction."
)
}
isValid(str: string): boolean {
if (str.endsWith("%") || str.endsWith("°")) {

View file

@ -53,6 +53,7 @@
const canvas = _map.getCanvas()
ariaLabel(canvas, Translations.t.general.visualFeedback.navigation)
canvas.role="application"
canvas.tabIndex = 0
})
map.set(_map)
})

View file

@ -9,12 +9,14 @@
import type { Feature } from "geojson"
import { Unit } from "../../../Models/Unit"
import InputHelpers from "../../InputElement/InputHelpers"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
export let value: UIEventSource<string>
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature = undefined
export let state: SpecialVisualizationState
export let unit: Unit | undefined
let placeholder = config.freeform?.placeholder
@ -70,6 +72,7 @@
{feature}
type={config.freeform.type}
{value}
{state}
on:submit
/>
</div>

View file

@ -254,6 +254,7 @@
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}
@ -296,6 +297,7 @@
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:selected={() => (selectedMapping = config.mappings?.length)}
@ -338,6 +340,7 @@
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}

View file

@ -83,6 +83,7 @@ import NearbyImagesCollapsed from "./Image/NearbyImagesCollapsed.svelte"
import MoveWizard from "./Popup/MoveWizard.svelte"
import { Unit } from "../Models/Unit"
import Link from "./Base/Link.svelte"
import OrientationDebugPanel from "./Debug/OrientationDebugPanel.svelte"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -1536,10 +1537,13 @@ export default class SpecialVisualizations {
)).geolocation.currentUserLocation.features.map(
(features) => features[0]?.properties
)
return new SvelteUIElement(AllTagsPanel, {
state,
tags,
})
return new Combine([
new SvelteUIElement(OrientationDebugPanel, {}),
new SvelteUIElement(AllTagsPanel, {
state,
tags,
}),
])
},
},
{

View file

@ -2,10 +2,9 @@
// Testing grounds
import { UIEventSource } from "../Logic/UIEventSource"
import SlopeInput from "./InputElement/Helpers/SlopeInput.svelte"
import OrientationDebugPanel from "./Debug/OrientationDebugPanel.svelte"
let value: UIEventSource<string> = new UIEventSource(undefined)
</script>
<div class="w-full flex flex-col">
<div>Value: {$value}</div>
</div>
<SlopeInput {value}></SlopeInput>
<OrientationDebugPanel/>
<SlopeInput />

View file

@ -64,12 +64,15 @@
import Favourites from "./Favourites/Favourites.svelte"
import ImageOperations from "./Image/ImageOperations.svelte"
import VisualFeedbackPanel from "./BigComponents/VisualFeedbackPanel.svelte"
import { Orientation } from "../Logic/Web/Orientation"
export let state: ThemeViewState
let layout = state.layout
let maplibremap: UIEventSource<MlMap> = state.map
let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined)
let compass = Orientation.singleton.alpha
let compassLoaded = Orientation.singleton.gotMeasurement
state.selectedElement.addCallback(selected => {
if (!selected) {
selectedElement.setData(selected)
@ -258,15 +261,29 @@
<Min class="h-8 w-8" />
</MapControlButton>
<If condition={featureSwitches.featureSwitchGeolocation}>
<MapControlButton arialabel={Translations.t.general.labels.jumpToLocation}
on:click={() => geolocationControl.handleClick()}
on:keydown={forwardEventToMap}
>
<ToSvelte
construct={geolocationControl.SetClass("block w-8 h-8")}
/>
</MapControlButton>
<div class="relative m-0.5 h-12 w-12 p-0 sm:p-1 md:m-1">
{#if $compassLoaded}
<div class="absolute top-1/2 left-1/2 w-0 h-0">
<div class="w-5 h-5"
style={`rotate: calc(${-$compass}deg + 225deg); transform-origin: 0% 0%; background: var(--button-background);`} />
</div>
{/if}
<div class="absolute top-0 left-0 p-0.5 md:p-1">
<MapControlButton arialabel={Translations.t.general.labels.jumpToLocation}
cls="m-0 p-0.5 sm:p-1"
on:click={() => geolocationControl.handleClick()}
on:keydown={forwardEventToMap}
>
<ToSvelte
construct={geolocationControl.SetClass("block w-8 h-8")}
/>
</MapControlButton>
</div>
</div>
</If>
</div>
</div>
</div>