forked from MapComplete/MapComplete
Feature(distancePicker): revive geographical distance picker
This commit is contained in:
parent
e997653284
commit
5095bffc50
16 changed files with 245 additions and 107 deletions
|
@ -494,6 +494,7 @@
|
|||
"id": "Cycle barrier type"
|
||||
},
|
||||
{
|
||||
"id": "MaxWidth",
|
||||
"render": {
|
||||
"en": "Maximum width: {maxwidth:physical} m",
|
||||
"nl": "Maximumbreedte: {maxwidth:physical} m",
|
||||
|
@ -532,12 +533,11 @@
|
|||
"freeform": {
|
||||
"key": "maxwidth:physical",
|
||||
"type": "distance",
|
||||
"helperArgs": [
|
||||
"20",
|
||||
"map"
|
||||
]
|
||||
},
|
||||
"id": "MaxWidth"
|
||||
"helperArgs": {
|
||||
"zoom": 20,
|
||||
"background": "map"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"render": {
|
||||
|
@ -575,10 +575,10 @@
|
|||
"freeform": {
|
||||
"key": "width:separation",
|
||||
"type": "distance",
|
||||
"helperArgs": [
|
||||
"21",
|
||||
"map"
|
||||
]
|
||||
"helperArgs": {
|
||||
"zoom": 21,
|
||||
"background": "map"
|
||||
}
|
||||
},
|
||||
"id": "Space between barrier (cyclebarrier)"
|
||||
},
|
||||
|
@ -618,10 +618,10 @@
|
|||
"freeform": {
|
||||
"key": "width:opening",
|
||||
"type": "distance",
|
||||
"helperArgs": [
|
||||
"21",
|
||||
"map"
|
||||
]
|
||||
"helperArgs": {
|
||||
"zoom": 21,
|
||||
"background": "map"
|
||||
}
|
||||
},
|
||||
"id": "Width of opening (cyclebarrier)"
|
||||
},
|
||||
|
@ -661,10 +661,10 @@
|
|||
"freeform": {
|
||||
"key": "overlap",
|
||||
"type": "distance",
|
||||
"helperArgs": [
|
||||
"21",
|
||||
"map"
|
||||
]
|
||||
"helperArgs": {
|
||||
"zoom": 21,
|
||||
"background": "map"
|
||||
}
|
||||
},
|
||||
"id": "Overlap (cyclebarrier)"
|
||||
}
|
||||
|
|
|
@ -1283,10 +1283,10 @@
|
|||
"freeform": {
|
||||
"key": "width",
|
||||
"type": "distance",
|
||||
"helperArgs": [
|
||||
"20",
|
||||
"map"
|
||||
]
|
||||
"helperArgs": {
|
||||
"zoom": 20,
|
||||
"background": "map"
|
||||
}
|
||||
},
|
||||
"question": {
|
||||
"en": "What is the carriage width of this road (in meters)?",
|
||||
|
@ -1808,10 +1808,10 @@
|
|||
"freeform": {
|
||||
"key": "cycleway:buffer",
|
||||
"type": "distance",
|
||||
"helperArgs": [
|
||||
"20",
|
||||
"map"
|
||||
]
|
||||
"helperArgs": {
|
||||
"background": "map",
|
||||
"zoom": 20
|
||||
}
|
||||
},
|
||||
"id": "cycleways_and_roads-cycleway:buffer"
|
||||
},
|
||||
|
|
|
@ -69,10 +69,10 @@
|
|||
"freeform": {
|
||||
"key": "width:carriageway",
|
||||
"type": "distance",
|
||||
"helperArgs": [
|
||||
21,
|
||||
"map"
|
||||
]
|
||||
"helperArgs": {
|
||||
"zoom": 21,
|
||||
"background": "map"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -628,6 +628,11 @@
|
|||
"recentThemes": "Recently visited themes",
|
||||
"title": "MapComplete"
|
||||
},
|
||||
"input_helpers": {
|
||||
"distance": {
|
||||
"setFirst": "Measure from current location"
|
||||
}
|
||||
},
|
||||
"inspector": {
|
||||
"aggregateView": "Aggregate",
|
||||
"answeredCountTimes": "Answered {count} times",
|
||||
|
|
|
@ -32,6 +32,27 @@ export class AvailableRasterLayers {
|
|||
|
||||
public static readonly globalLayers: ReadonlyArray<RasterLayerPolygon> =
|
||||
AvailableRasterLayers.initGlobalLayers()
|
||||
public static bing = <RasterLayerPolygon>bingJson
|
||||
public static readonly osmCartoProperties: RasterLayerProperties = {
|
||||
id: "osm",
|
||||
name: "OpenStreetMap",
|
||||
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
attribution: {
|
||||
text: "OpenStreetMap",
|
||||
url: "https://openStreetMap.org/copyright"
|
||||
},
|
||||
best: true,
|
||||
max_zoom: 19,
|
||||
min_zoom: 0,
|
||||
category: "osmbasedmap"
|
||||
}
|
||||
public static readonly osmCarto: RasterLayerPolygon = {
|
||||
type: "Feature",
|
||||
properties: AvailableRasterLayers.osmCartoProperties,
|
||||
geometry: BBox.global.asGeometry()
|
||||
}
|
||||
|
||||
public static allAvailableGlobalLayers = new Set([...AvailableRasterLayers.globalLayers, AvailableRasterLayers.osmCarto, AvailableRasterLayers.bing])
|
||||
|
||||
private static initGlobalLayers(): RasterLayerPolygon[] {
|
||||
const gl: RasterLayerProperties[] = (globallayers["default"] ?? globallayers).layers.filter(
|
||||
|
@ -54,26 +75,7 @@ export class AvailableRasterLayers {
|
|||
)
|
||||
}
|
||||
|
||||
public static bing = <RasterLayerPolygon>bingJson
|
||||
public static readonly osmCartoProperties: RasterLayerProperties = {
|
||||
id: "osm",
|
||||
name: "OpenStreetMap",
|
||||
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
attribution: {
|
||||
text: "OpenStreetMap",
|
||||
url: "https://openStreetMap.org/copyright",
|
||||
},
|
||||
best: true,
|
||||
max_zoom: 19,
|
||||
min_zoom: 0,
|
||||
category: "osmbasedmap",
|
||||
}
|
||||
|
||||
public static readonly osmCarto: RasterLayerPolygon = {
|
||||
type: "Feature",
|
||||
properties: AvailableRasterLayers.osmCartoProperties,
|
||||
geometry: BBox.global.asGeometry(),
|
||||
}
|
||||
|
||||
/**
|
||||
* The default background layer that any theme uses which does not explicitly define a background
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { DesugaringStep } from "./Conversion"
|
||||
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import {
|
||||
MappingConfigJson,
|
||||
QuestionableTagRenderingConfigJson,
|
||||
} from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
import { Translation } from "../../../UI/i18n/Translation"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
|
@ -216,6 +213,14 @@ export class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJso
|
|||
"No need to explicitly set type to 'NSI', autodetected based on freeform type"
|
||||
)
|
||||
}
|
||||
|
||||
if (json.freeform["type"] && json.freeform["helperArgs"]) {
|
||||
const validator = Validators.get(json.freeform["type"])
|
||||
const feedback = validator?.validateArguments(json.freeform["helperArgs"])
|
||||
if (feedback) {
|
||||
context.enters("freeform", "helperArgs").err(feedback)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (json.render && json["question"] && json.freeform === undefined) {
|
||||
context.err(
|
||||
|
|
100
src/UI/InputElement/Helpers/DistanceInput.svelte
Normal file
100
src/UI/InputElement/Helpers/DistanceInput.svelte
Normal file
|
@ -0,0 +1,100 @@
|
|||
<script lang="ts">
|
||||
|
||||
/**
|
||||
* Used to quickly calculate a distance by dragging a map (and selecting start- and endpoints)
|
||||
*/
|
||||
|
||||
import LocationInput from "./LocationInput.svelte"
|
||||
import { UIEventSource, Store } from "../../../Logic/UIEventSource"
|
||||
import type { MapProperties } from "../../../Models/MapProperties"
|
||||
import ThemeViewState from "../../../Models/ThemeViewState"
|
||||
import type { Feature } from "geojson"
|
||||
import type { RasterLayerPolygon } from "../../../Models/RasterLayers"
|
||||
import { RasterLayerUtils } from "../../../Models/RasterLayers"
|
||||
import { eliCategory } from "../../../Models/RasterLayerProperties"
|
||||
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import ShowDataLayer from "../../Map/ShowDataLayer"
|
||||
import * as conflation from "../../../assets/generated/layers/conflation.json"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
|
||||
export let value: UIEventSource<number>
|
||||
export let feature: Feature
|
||||
export let args: { background?: string, zoom?: number }
|
||||
export let state: ThemeViewState = undefined
|
||||
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
|
||||
let center = GeoOperations.centerpointCoordinates(feature)
|
||||
export let initialCoordinate: { lon: number, lat: number } = { lon: center[0], lat: center[1] }
|
||||
let mapLocation: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(initialCoordinate)
|
||||
let bg = args?.background
|
||||
let rasterLayer = state?.mapProperties.rasterLayer
|
||||
if (bg !== undefined) {
|
||||
if (eliCategory.indexOf(bg) >= 0) {
|
||||
const availableLayers = state.availableLayers.store.data
|
||||
const startLayer: RasterLayerPolygon = RasterLayerUtils.SelectBestLayerAccordingTo(availableLayers, bg)
|
||||
rasterLayer = new UIEventSource(startLayer)
|
||||
state?.mapProperties.rasterLayer.addCallbackD(layer => rasterLayer.set(layer))
|
||||
}
|
||||
|
||||
}
|
||||
let mapProperties: Partial<MapProperties> = {
|
||||
rasterLayer: rasterLayer,
|
||||
location: mapLocation,
|
||||
zoom: new UIEventSource(args?.zoom ?? 18)
|
||||
}
|
||||
|
||||
let start: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(undefined)
|
||||
|
||||
function selectStart() {
|
||||
start.set(mapLocation.data)
|
||||
}
|
||||
|
||||
let lengthFeature: Store<Feature[]> = start.map(start => {
|
||||
if (!start) {
|
||||
return []
|
||||
}
|
||||
// A bit of a double task: calculate the actual value _and_ the map rendering
|
||||
const end = mapLocation.data
|
||||
const distance = GeoOperations.distanceBetween([start.lon, start.lat], [end.lon, end.lat])
|
||||
value.set(distance.toFixed(2))
|
||||
|
||||
|
||||
return <Feature[]>[
|
||||
|
||||
{
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: "distance_line_" + distance,
|
||||
distance: "" + distance
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [[start.lon, start.lat], [end.lon, end.lat]]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}, [mapLocation])
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: new LayerConfig(conflation),
|
||||
features: new StaticFeatureSource(lengthFeature)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<div class="relative w-full h-64">
|
||||
<LocationInput value={mapLocation} {mapProperties} {map} />
|
||||
<div class="absolute bottom-0 left-0 p-4">
|
||||
<OpenBackgroundSelectorButton {state} {map} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="primary" on:click={() => selectStart()}>
|
||||
<Tr t={Translations.t.input_helpers.distance.setFirst} />
|
||||
</button>
|
|
@ -19,12 +19,13 @@
|
|||
import SlopeInput from "./Helpers/SlopeInput.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import WikidataInputHelper from "./WikidataInputHelper.svelte"
|
||||
import DistanceInput from "./Helpers/DistanceInput.svelte"
|
||||
|
||||
export let type: ValidatorType
|
||||
export let value: UIEventSource<string | object>
|
||||
|
||||
export let feature: Feature = undefined
|
||||
export let args: (string | number | boolean)[] = undefined
|
||||
export let args: (string | number | boolean)[] | any = undefined
|
||||
export let state: SpecialVisualizationState = undefined
|
||||
</script>
|
||||
|
||||
|
@ -52,6 +53,8 @@
|
|||
<SlopeInput {value} {feature} {state} />
|
||||
{:else if type === "wikidata"}
|
||||
<WikidataInputHelper {value} {feature} {state} {args} />
|
||||
{:else if type === "distance"}
|
||||
<DistanceInput {value} {state} {feature} {args} />
|
||||
{:else}
|
||||
<slot name="fallback" />
|
||||
{/if}
|
||||
|
|
|
@ -27,7 +27,6 @@ export interface InputHelperProperties {
|
|||
export default class InputHelpers {
|
||||
public static hideInputField: string[] = ["translation", "simple_tag", "tag"]
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
/**
|
||||
* Constructs a mapProperties-object for the given properties.
|
||||
* Assumes that the first helper-args contains the desired zoom-level
|
||||
|
|
|
@ -14,7 +14,8 @@ export abstract class Validator {
|
|||
* */
|
||||
public readonly explanation: string
|
||||
/**
|
||||
* What HTML-inputmode to use
|
||||
* What HTML-inputmode to use?
|
||||
* Note: some inputHelpers will completely hide the default text field. This is kept in InputHelpers.hideInputField
|
||||
*/
|
||||
public readonly inputmode?:
|
||||
| "none"
|
||||
|
@ -81,4 +82,14 @@ export abstract class Validator {
|
|||
public reformat(s: string, _?: () => string): string {
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the helper arguments are correct.
|
||||
* This is called while preparing the themes.
|
||||
* Returns 'undefined' if everything is fine, or feedback if an error is detected
|
||||
* @param args the args for the input helper
|
||||
*/
|
||||
public validateArguments(args: string): undefined | string {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import TextValidator from "./Validators/TextValidator"
|
|||
import DateValidator from "./Validators/DateValidator"
|
||||
import NatValidator from "./Validators/NatValidator"
|
||||
import IntValidator from "./Validators/IntValidator"
|
||||
import LengthValidator from "./Validators/LengthValidator"
|
||||
import DistanceValidator from "./Validators/DistanceValidator"
|
||||
import DirectionValidator from "./Validators/DirectionValidator"
|
||||
import WikidataValidator from "./Validators/WikidataValidator"
|
||||
import PNatValidator from "./Validators/PNatValidator"
|
||||
|
@ -71,7 +71,7 @@ export default class Validators {
|
|||
new DateValidator(),
|
||||
new NatValidator(),
|
||||
new IntValidator(),
|
||||
new LengthValidator(),
|
||||
new DistanceValidator(),
|
||||
new DirectionValidator(),
|
||||
new WikidataValidator(),
|
||||
new PNatValidator(),
|
||||
|
|
55
src/UI/InputElement/Validators/DistanceValidator.ts
Normal file
55
src/UI/InputElement/Validators/DistanceValidator.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { Validator } from "../Validator"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { eliCategory } from "../../../Models/RasterLayerProperties"
|
||||
|
||||
export default class DistanceValidator extends Validator {
|
||||
private readonly docs: string = [
|
||||
"#### Helper-arguments",
|
||||
"Options are:",
|
||||
["````json",
|
||||
" \"background\": \"some_background_id or category, e.g. 'map'\"",
|
||||
" \"zoom\": 20 # initial zoom level of the map",
|
||||
"}",
|
||||
"```"].join("\n")
|
||||
].join("\n\n")
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
"distance",
|
||||
"A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `[\"21\", \"map,photo\"]",
|
||||
"decimal"
|
||||
)
|
||||
}
|
||||
|
||||
isValid = (str) => {
|
||||
const t = Number(str)
|
||||
return !isNaN(t)
|
||||
}
|
||||
|
||||
validateArguments(args: any): undefined | string {
|
||||
if (args === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (typeof args !== "object" || Array.isArray(args)) {
|
||||
return "Expected an object of type `{background?: string, zoom?: number}`"
|
||||
}
|
||||
|
||||
const optionalKeys = ["background", "zoom"]
|
||||
const keys = Object.keys(args).filter(k => optionalKeys.indexOf(k) < 0)
|
||||
if (keys.length > 0) {
|
||||
return "Unknown key " + keys.join("; ") + "; use " + optionalKeys.join("; ") + " instead"
|
||||
}
|
||||
const bg = args["background"]
|
||||
if (bg && eliCategory.indexOf(bg) < 0) {
|
||||
return "The given background layer is not a recognized ELI-type. Perhaps you meant one of " +
|
||||
Utils.sortedByLevenshteinDistance(bg, eliCategory, x => x).slice(0, 5)
|
||||
}
|
||||
if (typeof args["zoom"] !== "number") {
|
||||
return "zoom must be a number, got a " + typeof args["zoom"]
|
||||
}
|
||||
if (typeof args["zoom"] !== "number" || args["zoom"] <= 1 || args["zoom"] > 25) {
|
||||
return "zoom must be a number between 2 and 25, got " + args["zoom"]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import { Validator } from "../Validator"
|
|||
import { ValidatorType } from "../Validators"
|
||||
|
||||
export default class FloatValidator extends Validator {
|
||||
inputmode: "decimal" = "decimal"
|
||||
inputmode: "decimal" = "decimal" as const
|
||||
|
||||
constructor(name?: ValidatorType, explanation?: string) {
|
||||
super(name ?? "float", explanation ?? "A decimal number", "decimal")
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { Validator } from "../Validator"
|
||||
|
||||
export default class LengthValidator extends Validator {
|
||||
constructor() {
|
||||
super(
|
||||
"distance",
|
||||
'A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]',
|
||||
"decimal"
|
||||
)
|
||||
}
|
||||
|
||||
isValid = (str) => {
|
||||
const t = Number(str)
|
||||
return !isNaN(t)
|
||||
}
|
||||
}
|
|
@ -1,40 +1,14 @@
|
|||
import Title from "../../Base/Title"
|
||||
import Combine from "../../Base/Combine"
|
||||
import { Validator } from "../Validator"
|
||||
import Table from "../../Base/Table"
|
||||
|
||||
export default class NameSuggestionIndexValidator extends Validator {
|
||||
constructor() {
|
||||
super(
|
||||
"nsi",
|
||||
new Combine([
|
||||
"Gives a list of possible suggestions for a brand or operator tag.",
|
||||
new Title("Helper arguments"),
|
||||
new Table(
|
||||
["name", "doc"],
|
||||
[
|
||||
[
|
||||
"options",
|
||||
new Combine([
|
||||
"A JSON-object of type `{ main: string, key: string }`. ",
|
||||
new Table(
|
||||
["subarg", "doc"],
|
||||
[
|
||||
[
|
||||
"main",
|
||||
"The main tag to give suggestions for, e.g. `amenity=restaurant`.",
|
||||
],
|
||||
[
|
||||
"addExtraTags",
|
||||
"Extra tags to add to the suggestions, e.g. `nobrand=yes`.",
|
||||
],
|
||||
]
|
||||
),
|
||||
]),
|
||||
],
|
||||
]
|
||||
),
|
||||
])
|
||||
"Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
|
||||
)
|
||||
}
|
||||
|
||||
validateArguments(args: string): string | undefined {
|
||||
return "No arguments needed"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -352,7 +352,7 @@ class LineRenderingLayer {
|
|||
// After waiting 'till the map has loaded, the data might have changed already
|
||||
// As such, we only now read the features from the featureSource and compare with the previously set data
|
||||
const features = featureSource.data
|
||||
if (features.length === 0) {
|
||||
if (!features || features.length === 0) {
|
||||
// This is a very ugly workaround for https://source.mapcomplete.org/MapComplete/MapComplete/issues/2312,
|
||||
// but I couldn't find the root cause
|
||||
return
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue