First working version of snapping to already existing ways from the add-UI (still too slow though), partial fix of #436
This commit is contained in:
parent
bf2d634208
commit
0a01561d37
15 changed files with 460 additions and 143 deletions
|
@ -14,11 +14,11 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||
import SourceConfig from "./SourceConfig";
|
||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import {Unit} from "./Denomination";
|
||||
import DeleteConfig from "./DeleteConfig";
|
||||
import FilterConfig from "./FilterConfig";
|
||||
import PresetConfig from "./PresetConfig";
|
||||
|
||||
export default class LayerConfig {
|
||||
static WAYHANDLING_DEFAULT = 0;
|
||||
|
@ -35,7 +35,7 @@ export default class LayerConfig {
|
|||
isShown: TagRenderingConfig;
|
||||
minzoom: number;
|
||||
minzoomVisible: number;
|
||||
maxzoom:number;
|
||||
maxzoom: number;
|
||||
title?: TagRenderingConfig;
|
||||
titleIcons: TagRenderingConfig[];
|
||||
icon: TagRenderingConfig;
|
||||
|
@ -51,12 +51,7 @@ export default class LayerConfig {
|
|||
public readonly deletion: DeleteConfig | null;
|
||||
public readonly allowSplit: boolean
|
||||
|
||||
presets: {
|
||||
title: Translation,
|
||||
tags: Tag[],
|
||||
description?: Translation,
|
||||
preciseInput?: { preferredBackground?: string }
|
||||
}[];
|
||||
presets: PresetConfig[];
|
||||
|
||||
tagRenderings: TagRenderingConfig[];
|
||||
filters: FilterConfig[];
|
||||
|
@ -149,17 +144,41 @@ export default class LayerConfig {
|
|||
this.minzoomVisible = json.minzoomVisible ?? this.minzoom;
|
||||
this.wayHandling = json.wayHandling ?? 0;
|
||||
this.presets = (json.presets ?? []).map((pr, i) => {
|
||||
if (pr.preciseInput === true) {
|
||||
pr.preciseInput = {
|
||||
preferredBackground: undefined
|
||||
|
||||
let preciseInput = undefined;
|
||||
if(pr.preciseInput !== undefined){
|
||||
if (pr.preciseInput === true) {
|
||||
pr.preciseInput = {
|
||||
preferredBackground: undefined
|
||||
}
|
||||
}
|
||||
let snapToLayers: string[];
|
||||
if (typeof pr.preciseInput.snapToLayer === "string") {
|
||||
snapToLayers = [pr.preciseInput.snapToLayer]
|
||||
} else {
|
||||
snapToLayers = pr.preciseInput.snapToLayer
|
||||
}
|
||||
|
||||
let preferredBackground : string[]
|
||||
if (typeof pr.preciseInput.preferredBackground === "string") {
|
||||
preferredBackground = [pr.preciseInput.preferredBackground]
|
||||
} else {
|
||||
preferredBackground = pr.preciseInput.preferredBackground
|
||||
}
|
||||
preciseInput = {
|
||||
preferredBackground: preferredBackground,
|
||||
snapToLayers: snapToLayers,
|
||||
maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
const config : PresetConfig= {
|
||||
title: Translations.T(pr.title, `${context}.presets[${i}].title`),
|
||||
tags: pr.tags.map((t) => FromJSON.SimpleTag(t)),
|
||||
description: Translations.T(pr.description, `${context}.presets[${i}].description`),
|
||||
preciseInput: pr.preciseInput
|
||||
preciseInput: preciseInput,
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
/** Given a key, gets the corresponding property from the json (or the default if not found
|
||||
|
@ -407,12 +426,15 @@ export default class LayerConfig {
|
|||
}
|
||||
|
||||
function render(tr: TagRenderingConfig, deflt?: string) {
|
||||
if(tags === undefined){
|
||||
return deflt
|
||||
}
|
||||
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt;
|
||||
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
|
||||
}
|
||||
|
||||
const iconSize = render(this.iconSize, "40,40,center").split(",");
|
||||
const dashArray = render(this.dashArray).split(" ").map(Number);
|
||||
const dashArray = render(this.dashArray)?.split(" ")?.map(Number);
|
||||
let color = render(this.color, "#00f");
|
||||
|
||||
if (color.startsWith("--")) {
|
||||
|
@ -445,24 +467,26 @@ export default class LayerConfig {
|
|||
|
||||
const iconUrlStatic = render(this.icon);
|
||||
const self = this;
|
||||
const mappedHtml = tags.map((tgs) => {
|
||||
function genHtmlFromString(sourcePart: string): BaseUIElement {
|
||||
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
|
||||
let html: BaseUIElement = new FixedUiElement(
|
||||
`<img src="${sourcePart}" style="${style}" />`
|
||||
);
|
||||
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/);
|
||||
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
||||
html = new Combine([
|
||||
(Svg.All[match[1] + ".svg"] as string).replace(
|
||||
/#000000/g,
|
||||
match[2]
|
||||
),
|
||||
]).SetStyle(style);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function genHtmlFromString(sourcePart: string, rotation: string): BaseUIElement {
|
||||
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
|
||||
let html: BaseUIElement = new FixedUiElement(
|
||||
`<img src="${sourcePart}" style="${style}" />`
|
||||
);
|
||||
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/);
|
||||
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
||||
html = new Combine([
|
||||
(Svg.All[match[1] + ".svg"] as string).replace(
|
||||
/#000000/g,
|
||||
match[2]
|
||||
),
|
||||
]).SetStyle(style);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
const mappedHtml = tags?.map((tgs) => {
|
||||
// What do you mean, 'tgs' is never read?
|
||||
// It is read implicitly in the 'render' method
|
||||
const iconUrl = render(self.icon);
|
||||
|
@ -473,7 +497,7 @@ export default class LayerConfig {
|
|||
iconUrl.split(";").filter((prt) => prt != "")
|
||||
);
|
||||
for (const sourcePart of sourceParts) {
|
||||
htmlParts.push(genHtmlFromString(sourcePart));
|
||||
htmlParts.push(genHtmlFromString(sourcePart, rotation));
|
||||
}
|
||||
|
||||
let badges = [];
|
||||
|
@ -489,7 +513,7 @@ export default class LayerConfig {
|
|||
.filter((prt) => prt != "");
|
||||
|
||||
for (const badgePartStr of partDefs) {
|
||||
badgeParts.push(genHtmlFromString(badgePartStr));
|
||||
badgeParts.push(genHtmlFromString(badgePartStr, "0"));
|
||||
}
|
||||
|
||||
const badgeCompound = new Combine(badgeParts).SetStyle(
|
||||
|
@ -499,7 +523,7 @@ export default class LayerConfig {
|
|||
badges.push(badgeCompound);
|
||||
} else {
|
||||
htmlParts.push(
|
||||
genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt)
|
||||
genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt, "0")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -533,7 +557,7 @@ export default class LayerConfig {
|
|||
|
||||
return {
|
||||
icon: {
|
||||
html: new VariableUiElement(mappedHtml),
|
||||
html: mappedHtml === undefined ? new FixedUiElement(self.icon.render.txt) : new VariableUiElement(mappedHtml),
|
||||
iconSize: [iconW, iconH],
|
||||
iconAnchor: [anchorW, anchorH],
|
||||
popupAnchor: [0, 3 - anchorH],
|
||||
|
|
|
@ -226,7 +226,21 @@ export interface LayerConfigJson {
|
|||
* If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category.
|
||||
*/
|
||||
preciseInput?: true | {
|
||||
preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string
|
||||
/**
|
||||
* The type of background picture
|
||||
*/
|
||||
preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string | string [],
|
||||
/**
|
||||
* If specified, these layers will be shown to and the new point will be snapped towards it
|
||||
*/
|
||||
snapToLayer?: string | string[],
|
||||
/**
|
||||
* If specified, a new point will only be snapped if it is within this range.
|
||||
* Distance in meter
|
||||
*
|
||||
* Default: 10
|
||||
*/
|
||||
maxSnapDistance?: number
|
||||
}
|
||||
}[],
|
||||
|
||||
|
|
|
@ -99,7 +99,7 @@ export default class LayoutConfig {
|
|||
this.defaultBackgroundId = json.defaultBackgroundId;
|
||||
this.layers = LayoutConfig.ExtractLayers(json, this.units, official, context);
|
||||
|
||||
// ALl the layers are constructed, let them share tags in now!
|
||||
// ALl the layers are constructed, let them share tagRenderings now!
|
||||
const roaming: { r, source: LayerConfig }[] = []
|
||||
for (const layer of this.layers) {
|
||||
roaming.push({r: layer.GetRoamingRenderings(), source: layer});
|
||||
|
|
16
Customizations/JSON/PresetConfig.ts
Normal file
16
Customizations/JSON/PresetConfig.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {Translation} from "../../UI/i18n/Translation";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
|
||||
export default interface PresetConfig {
|
||||
title: Translation,
|
||||
tags: Tag[],
|
||||
description?: Translation,
|
||||
/**
|
||||
* If precise input is set, then an extra map is shown in which the user can drag the map to the precise location
|
||||
*/
|
||||
preciseInput?: {
|
||||
preferredBackground?: string[],
|
||||
snapToLayers?: string[],
|
||||
maxSnapDistance?: number
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ export interface ChangeDescription {
|
|||
lat: number,
|
||||
lon: number
|
||||
} | {
|
||||
// Coordinates are only used for rendering
|
||||
// Coordinates are only used for rendering. They should be lon, lat
|
||||
locations: [number, number][]
|
||||
nodes: number[],
|
||||
} | {
|
||||
|
|
|
@ -3,6 +3,8 @@ import OsmChangeAction from "./OsmChangeAction";
|
|||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {And} from "../../Tags/And";
|
||||
import {OsmWay} from "../OsmObject";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
|
||||
export default class CreateNewNodeAction extends OsmChangeAction {
|
||||
|
||||
|
@ -10,13 +12,20 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
|||
private readonly _lat: number;
|
||||
private readonly _lon: number;
|
||||
|
||||
public newElementId : string = undefined
|
||||
|
||||
constructor(basicTags: Tag[], lat: number, lon: number) {
|
||||
public newElementId: string = undefined
|
||||
private readonly _snapOnto: OsmWay;
|
||||
private readonly _reusePointDistance: number;
|
||||
|
||||
constructor(basicTags: Tag[], lat: number, lon: number, options?: { snapOnto: OsmWay, reusePointWithinMeters?: number }) {
|
||||
super()
|
||||
this._basicTags = basicTags;
|
||||
this._lat = lat;
|
||||
this._lon = lon;
|
||||
if(lat === undefined || lon === undefined){
|
||||
throw "Lat or lon are undefined!"
|
||||
}
|
||||
this._snapOnto = options?.snapOnto;
|
||||
this._reusePointDistance = options.reusePointWithinMeters ?? 1
|
||||
}
|
||||
|
||||
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||
|
@ -24,7 +33,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
|||
const properties = {
|
||||
id: "node/" + id
|
||||
}
|
||||
this.newElementId = "node/"+id
|
||||
this.newElementId = "node/" + id
|
||||
for (const kv of this._basicTags) {
|
||||
if (typeof kv.value !== "string") {
|
||||
throw "Invalid value: don't use a regex in a preset"
|
||||
|
@ -32,16 +41,68 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
|||
properties[kv.key] = kv.value;
|
||||
}
|
||||
|
||||
return [{
|
||||
const newPointChange: ChangeDescription = {
|
||||
tags: new And(this._basicTags).asChange(properties),
|
||||
type: "node",
|
||||
id: id,
|
||||
changes:{
|
||||
changes: {
|
||||
lat: this._lat,
|
||||
lon: this._lon
|
||||
}
|
||||
}]
|
||||
}
|
||||
if (this._snapOnto === undefined) {
|
||||
return [newPointChange]
|
||||
}
|
||||
|
||||
|
||||
// Project the point onto the way
|
||||
|
||||
const geojson = this._snapOnto.asGeoJson()
|
||||
const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat])
|
||||
const index = projected.properties.index
|
||||
// We check that it isn't close to an already existing point
|
||||
let reusedPointId = undefined;
|
||||
const prev = <[number, number]>geojson.geometry.coordinates[index]
|
||||
if (GeoOperations.distanceBetween(prev, <[number, number]>projected.geometry.coordinates) * 1000 < this._reusePointDistance) {
|
||||
// We reuse this point instead!
|
||||
reusedPointId = this._snapOnto.nodes[index]
|
||||
}
|
||||
const next = <[number, number]>geojson.geometry.coordinates[index + 1]
|
||||
if (GeoOperations.distanceBetween(next, <[number, number]>projected.geometry.coordinates) * 1000 < this._reusePointDistance) {
|
||||
// We reuse this point instead!
|
||||
reusedPointId = this._snapOnto.nodes[index + 1]
|
||||
}
|
||||
if (reusedPointId !== undefined) {
|
||||
console.log("Reusing an existing point:", reusedPointId)
|
||||
this.newElementId = "node/" + reusedPointId
|
||||
|
||||
return [{
|
||||
tags: new And(this._basicTags).asChange(properties),
|
||||
type: "node",
|
||||
id: reusedPointId
|
||||
}]
|
||||
}
|
||||
|
||||
const locations = [...this._snapOnto.coordinates]
|
||||
locations.forEach(coor => coor.reverse())
|
||||
console.log("Locations are: ", locations)
|
||||
const ids = [...this._snapOnto.nodes]
|
||||
|
||||
locations.splice(index + 1, 0, [this._lon, this._lat])
|
||||
ids.splice(index + 1, 0, id)
|
||||
|
||||
// Allright, we have to insert a new point in the way
|
||||
return [
|
||||
newPointChange,
|
||||
{
|
||||
type:"way",
|
||||
id: this._snapOnto.id,
|
||||
changes: {
|
||||
locations: locations,
|
||||
nodes: ids
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -27,9 +27,6 @@ export class Changes {
|
|||
private readonly previouslyCreated : OsmObject[] = []
|
||||
|
||||
constructor() {
|
||||
this.isUploading.addCallbackAndRun(uploading => {
|
||||
console.trace("Is uploading changed:", uploading)
|
||||
})
|
||||
}
|
||||
|
||||
private static createChangesetFor(csId: string,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Utils } from "../Utils";
|
|||
|
||||
export default class Constants {
|
||||
|
||||
public static vNumber = "0.9.0-rc0";
|
||||
public static vNumber = "0.9.0-rc2";
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
|
|
|
@ -9,19 +9,16 @@ import Combine from "../Base/Combine";
|
|||
import Translations from "../i18n/Translations";
|
||||
import Constants from "../../Models/Constants";
|
||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import LocationInput from "../Input/LocationInput";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import Loc from "../../Models/Loc";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||
import Hash from "../../Logic/Web/Hash";
|
||||
import PresetConfig from "../../Customizations/JSON/PresetConfig";
|
||||
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
|
||||
|
||||
/*
|
||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||
|
@ -32,17 +29,12 @@ import Hash from "../../Logic/Web/Hash";
|
|||
*/
|
||||
|
||||
/*private*/
|
||||
interface PresetInfo {
|
||||
description: string | Translation,
|
||||
interface PresetInfo extends PresetConfig {
|
||||
name: string | BaseUIElement,
|
||||
icon: () => BaseUIElement,
|
||||
tags: Tag[],
|
||||
layerToAddTo: {
|
||||
layerDef: LayerConfig,
|
||||
isDisplayed: UIEventSource<boolean>
|
||||
},
|
||||
preciseInput?: {
|
||||
preferredBackground?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,24 +57,43 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
|
||||
|
||||
|
||||
function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) {
|
||||
console.trace("Creating a new point")
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {snapOnto: snapOntoWay})
|
||||
State.state.changes.applyAction(newElementAction)
|
||||
selectedPreset.setData(undefined)
|
||||
isShown.setData(false)
|
||||
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
|
||||
newElementAction.newElementId
|
||||
))
|
||||
console.log("Did set selected element to", State.state.allElements.ContainingFeatures.get(
|
||||
newElementAction.newElementId
|
||||
))
|
||||
|
||||
}
|
||||
|
||||
const addUi = new VariableUiElement(
|
||||
selectedPreset.map(preset => {
|
||||
if (preset === undefined) {
|
||||
return presetsOverview
|
||||
}
|
||||
return SimpleAddUI.CreateConfirmButton(preset,
|
||||
(tags, location) => {
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon)
|
||||
State.state.changes.applyAction(newElementAction)
|
||||
selectedPreset.setData(undefined)
|
||||
isShown.setData(false)
|
||||
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
|
||||
newElementAction.newElementId
|
||||
))
|
||||
console.log("Did set selected element to",State.state.allElements.ContainingFeatures.get(
|
||||
newElementAction.newElementId
|
||||
))
|
||||
}, () => {
|
||||
(tags, location, snapOntoWayId?: string) => {
|
||||
if (snapOntoWayId === undefined) {
|
||||
createNewPoint(tags, location, undefined)
|
||||
} else {
|
||||
OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD(way => {
|
||||
createNewPoint(tags, location,<OsmWay> way)
|
||||
return true;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
|
||||
() => {
|
||||
selectedPreset.setData(undefined)
|
||||
})
|
||||
}
|
||||
|
@ -115,11 +126,11 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
|
||||
private static CreateConfirmButton(preset: PresetInfo,
|
||||
confirm: (tags: any[], location: { lat: number, lon: number }) => void,
|
||||
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
|
||||
cancel: () => void): BaseUIElement {
|
||||
|
||||
let location = State.state.LastClickLocation;
|
||||
let preciseInput: InputElement<Loc> = undefined
|
||||
let preciseInput: LocationInput = undefined
|
||||
if (preset.preciseInput !== undefined) {
|
||||
const locationSrc = new UIEventSource({
|
||||
lat: location.data.lat,
|
||||
|
@ -132,9 +143,22 @@ export default class SimpleAddUI extends Toggle {
|
|||
backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
|
||||
}
|
||||
|
||||
let features: UIEventSource<{ feature: any }[]> = undefined
|
||||
if (preset.preciseInput.snapToLayers) {
|
||||
// We have to snap to certain layers.
|
||||
// Lets fetch tehm
|
||||
const asSet = new Set(preset.preciseInput.snapToLayers)
|
||||
features = State.state.featurePipeline.features.map(f => f.filter(feat => asSet.has(feat.feature._matching_layer_id)))
|
||||
}
|
||||
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
console.log("Opening precise input ", preset.preciseInput, "with tags", tags)
|
||||
preciseInput = new LocationInput({
|
||||
mapBackground: backgroundLayer,
|
||||
centerLocation: locationSrc
|
||||
centerLocation: locationSrc,
|
||||
snapTo: features,
|
||||
snappedPointTags: tags,
|
||||
maxSnapDistance: preset.preciseInput.maxSnapDistance
|
||||
|
||||
})
|
||||
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
|
||||
|
@ -148,7 +172,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
]).SetClass("flex flex-col")
|
||||
).SetClass("font-bold break-words")
|
||||
.onClick(() => {
|
||||
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data);
|
||||
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data, preciseInput?.snappedOnto?.data?.properties?.id);
|
||||
});
|
||||
|
||||
if (preciseInput !== undefined) {
|
||||
|
@ -242,8 +266,8 @@ export default class SimpleAddUI extends Toggle {
|
|||
// The layer is not displayed and we cannot enable the layer control -> we skip
|
||||
continue;
|
||||
}
|
||||
|
||||
if(layer.layerDef.name === undefined){
|
||||
|
||||
if (layer.layerDef.name === undefined) {
|
||||
// this is a parlty hidden layer
|
||||
continue;
|
||||
}
|
||||
|
@ -258,6 +282,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
tags: preset.tags,
|
||||
layerToAddTo: layer,
|
||||
name: preset.title,
|
||||
title: preset.title,
|
||||
description: preset.description,
|
||||
icon: icon,
|
||||
preciseInput: preset.preciseInput
|
||||
|
|
|
@ -6,28 +6,114 @@ import BaseLayer from "../../Models/BaseLayer";
|
|||
import Combine from "../Base/Combine";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import ShowDataLayer from "../ShowDataLayer";
|
||||
|
||||
export default class LocationInput extends InputElement<Loc> {
|
||||
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private _centerLocation: UIEventSource<Loc>;
|
||||
private readonly mapBackground : UIEventSource<BaseLayer>;
|
||||
private readonly mapBackground: UIEventSource<BaseLayer>;
|
||||
private readonly _snapTo: UIEventSource<{ feature: any }[]>
|
||||
private readonly _value: UIEventSource<Loc>
|
||||
private readonly _snappedPoint: UIEventSource<any>
|
||||
private readonly _maxSnapDistance: number
|
||||
private readonly _snappedPointTags: any;
|
||||
public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
|
||||
constructor(options?: {
|
||||
constructor(options: {
|
||||
mapBackground?: UIEventSource<BaseLayer>,
|
||||
centerLocation?: UIEventSource<Loc>,
|
||||
snapTo?: UIEventSource<{ feature: any }[]>,
|
||||
maxSnapDistance?: number,
|
||||
snappedPointTags?: any,
|
||||
requiresSnapping?: boolean,
|
||||
centerLocation: UIEventSource<Loc>,
|
||||
}) {
|
||||
super();
|
||||
options = options ?? {}
|
||||
options.centerLocation = options.centerLocation ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
|
||||
this._snapTo = options.snapTo?.map(features => features?.filter(feat => feat.feature.geometry.type !== "Point"))
|
||||
this._maxSnapDistance = options.maxSnapDistance
|
||||
this._centerLocation = options.centerLocation;
|
||||
this._snappedPointTags = options.snappedPointTags
|
||||
if (this._snapTo === undefined) {
|
||||
this._value = this._centerLocation;
|
||||
} else {
|
||||
const self = this;
|
||||
|
||||
this.mapBackground = options.mapBackground ?? State.state.backgroundLayer
|
||||
let matching_layer: UIEventSource<string>
|
||||
|
||||
if (self._snappedPointTags !== undefined) {
|
||||
matching_layer = State.state.layoutToUse.map(layout => {
|
||||
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.source.osmTags.matchesProperties(self._snappedPointTags)) {
|
||||
return layer.id
|
||||
}
|
||||
}
|
||||
console.error("No matching layer found for tags ", self._snappedPointTags)
|
||||
return "matchpoint"
|
||||
})
|
||||
} else {
|
||||
matching_layer = new UIEventSource<string>("matchpoint")
|
||||
}
|
||||
|
||||
this._snappedPoint = options.centerLocation.map(loc => {
|
||||
if (loc === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// We reproject the location onto every 'snap-to-feature' and select the closest
|
||||
|
||||
let min = undefined;
|
||||
let matchedWay = undefined;
|
||||
for (const feature of self._snapTo.data) {
|
||||
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat])
|
||||
if (min === undefined) {
|
||||
min = nearestPointOnLine
|
||||
matchedWay = feature.feature;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (min.properties.dist > nearestPointOnLine.properties.dist) {
|
||||
min = nearestPointOnLine
|
||||
matchedWay = feature.feature;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (min.properties.dist * 1000 > self._maxSnapDistance) {
|
||||
if (options.requiresSnapping) {
|
||||
return undefined
|
||||
} else {
|
||||
return {
|
||||
"type": "Feature",
|
||||
"_matching_layer_id": matching_layer.data,
|
||||
"properties": options.snappedPointTags ?? min.properties,
|
||||
"geometry": {"type": "Point", "coordinates": [loc.lon, loc.lat]}
|
||||
}
|
||||
}
|
||||
}
|
||||
min._matching_layer_id = matching_layer?.data ?? "matchpoint"
|
||||
min.properties = options.snappedPointTags ?? min.properties
|
||||
self.snappedOnto.setData(matchedWay)
|
||||
return min
|
||||
}, [this._snapTo])
|
||||
|
||||
this._value = this._snappedPoint.map(f => {
|
||||
const [lon, lat] = f.geometry.coordinates;
|
||||
return {
|
||||
lon: lon, lat: lat, zoom: undefined
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
this.mapBackground = options.mapBackground ?? State.state.backgroundLayer ?? new UIEventSource(AvailableBaseLayers.osmCarto)
|
||||
this.SetClass("block h-full")
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<Loc> {
|
||||
return this._centerLocation;
|
||||
return this._value;
|
||||
}
|
||||
|
||||
IsValid(t: Loc): boolean {
|
||||
|
@ -35,41 +121,88 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const map = new Minimap(
|
||||
{
|
||||
location: this._centerLocation,
|
||||
background: this.mapBackground
|
||||
}
|
||||
)
|
||||
map.leafletMap.addCallbackAndRunD(leaflet => {
|
||||
leaflet.setMaxBounds(
|
||||
leaflet.getBounds().pad(0.15)
|
||||
try {
|
||||
const map = new Minimap(
|
||||
{
|
||||
location: this._centerLocation,
|
||||
background: this.mapBackground
|
||||
}
|
||||
)
|
||||
})
|
||||
map.leafletMap.addCallbackAndRunD(leaflet => {
|
||||
leaflet.setMaxBounds(
|
||||
leaflet.getBounds().pad(0.15)
|
||||
)
|
||||
})
|
||||
|
||||
this.mapBackground.map(layer => {
|
||||
if (this._snapTo !== undefined) {
|
||||
new ShowDataLayer(this._snapTo, map.leafletMap, State.state.layoutToUse, false, false)
|
||||
|
||||
const leaflet = map.leafletMap.data
|
||||
if (leaflet === undefined || layer === undefined) {
|
||||
return;
|
||||
const matchPoint = this._snappedPoint.map(loc => {
|
||||
if (loc === undefined) {
|
||||
return []
|
||||
}
|
||||
return [{feature: loc}];
|
||||
})
|
||||
if (this._snapTo) {
|
||||
let layout = LocationInput.matchLayout
|
||||
if (this._snappedPointTags !== undefined) {
|
||||
layout = State.state.layoutToUse
|
||||
}
|
||||
new ShowDataLayer(
|
||||
matchPoint,
|
||||
map.leafletMap,
|
||||
layout,
|
||||
false, false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
leaflet.setMaxZoom(layer.max_zoom)
|
||||
leaflet.setMinZoom(layer.max_zoom - 3)
|
||||
leaflet.setZoom(layer.max_zoom - 1)
|
||||
this.mapBackground.map(layer => {
|
||||
const leaflet = map.leafletMap.data
|
||||
if (leaflet === undefined || layer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
}, [map.leafletMap])
|
||||
return new Combine([
|
||||
new Combine([
|
||||
Svg.crosshair_empty_ui()
|
||||
.SetClass("block relative")
|
||||
.SetStyle("left: -1.25rem; top: -1.25rem; width: 2.5rem; height: 2.5rem")
|
||||
]).SetClass("block w-0 h-0 z-10 relative")
|
||||
.SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%"),
|
||||
map
|
||||
.SetClass("z-0 relative block w-full h-full bg-gray-100")
|
||||
leaflet.setMaxZoom(layer.max_zoom)
|
||||
leaflet.setMinZoom(layer.max_zoom - 3)
|
||||
leaflet.setZoom(layer.max_zoom - 1)
|
||||
|
||||
]).ConstructElement();
|
||||
}, [map.leafletMap])
|
||||
return new Combine([
|
||||
new Combine([
|
||||
Svg.crosshair_empty_ui()
|
||||
.SetClass("block relative")
|
||||
.SetStyle("left: -1.25rem; top: -1.25rem; width: 2.5rem; height: 2.5rem")
|
||||
]).SetClass("block w-0 h-0 z-10 relative")
|
||||
.SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%"),
|
||||
map
|
||||
.SetClass("z-0 relative block w-full h-full bg-gray-100")
|
||||
|
||||
]).ConstructElement();
|
||||
} catch (e) {
|
||||
console.error("Could not generate LocationInputElement:", e)
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly matchLayout = new UIEventSource(new LayoutConfig({
|
||||
description: "Matchpoint style",
|
||||
icon: "./assets/svg/crosshair-empty.svg",
|
||||
id: "matchpoint",
|
||||
language: ["en"],
|
||||
layers: [{
|
||||
id: "matchpoint", source: {
|
||||
osmTags: {and: []}
|
||||
},
|
||||
icon: "./assets/svg/crosshair-empty.svg"
|
||||
}],
|
||||
maintainer: "MapComplete",
|
||||
startLat: 0,
|
||||
startLon: 0,
|
||||
startZoom: 0,
|
||||
title: "Location input",
|
||||
version: "0"
|
||||
|
||||
}));
|
||||
|
||||
}
|
|
@ -16,9 +16,9 @@ export default class ShowDataLayer {
|
|||
private readonly _leafletMap: UIEventSource<L.Map>;
|
||||
private _cleanCount = 0;
|
||||
private readonly _enablePopups: boolean;
|
||||
private readonly _features: UIEventSource<{ feature: any}[]>
|
||||
private readonly _features: UIEventSource<{ feature: any }[]>
|
||||
|
||||
constructor(features: UIEventSource<{ feature: any}[]>,
|
||||
constructor(features: UIEventSource<{ feature: any }[]>,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
layoutToUse: UIEventSource<LayoutConfig>,
|
||||
enablePopups = true,
|
||||
|
@ -85,7 +85,9 @@ export default class ShowDataLayer {
|
|||
console.error(e)
|
||||
}
|
||||
}
|
||||
State.state.selectedElement.ping()
|
||||
if (self._enablePopups) {
|
||||
State.state.selectedElement.ping()
|
||||
}
|
||||
}
|
||||
|
||||
features.addCallback(() => update());
|
||||
|
@ -106,13 +108,12 @@ export default class ShowDataLayer {
|
|||
// We have to convert them to the appropriate icon
|
||||
// Click handling is done in the next step
|
||||
|
||||
const tagSource = State.state.allElements.getEventSourceById(feature.properties.id)
|
||||
const layer: LayerConfig = this._layerDict[feature._matching_layer_id];
|
||||
|
||||
if (layer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) : State.state.allElements.getEventSourceById(feature.properties.id)
|
||||
const style = layer.GenerateLeafletStyle(tagSource, !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0));
|
||||
const baseElement = style.icon.html;
|
||||
if (!this._enablePopups) {
|
||||
|
@ -146,8 +147,8 @@ export default class ShowDataLayer {
|
|||
autoPan: true,
|
||||
closeOnEscapeKey: true,
|
||||
closeButton: false,
|
||||
autoPanPaddingTopLeft: [15,15],
|
||||
|
||||
autoPanPaddingTopLeft: [15, 15],
|
||||
|
||||
}, leafletLayer);
|
||||
|
||||
leafletLayer.bindPopup(popup);
|
||||
|
@ -191,7 +192,7 @@ export default class ShowDataLayer {
|
|||
) {
|
||||
leafletLayer.openPopup()
|
||||
}
|
||||
if(feature.id !== feature.properties.id){
|
||||
if (feature.id !== feature.properties.id) {
|
||||
console.trace("Not opening the popup for", feature)
|
||||
}
|
||||
|
||||
|
|
|
@ -53,6 +53,11 @@
|
|||
"description": {
|
||||
"en": "A bollard in the road",
|
||||
"nl": "Een paaltje in de weg"
|
||||
},
|
||||
"preciseInput": {
|
||||
"preferredBackground": ["photo"],
|
||||
"snapToLayer": "cycleways_and_roads",
|
||||
"maxSnapDistance": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -66,6 +71,11 @@
|
|||
"description": {
|
||||
"en": "Cycle barrier, slowing down cyclists",
|
||||
"nl": "Fietshekjes, voor het afremmen van fietsers"
|
||||
},
|
||||
"preciseInput": {
|
||||
"preferredBackground": ["photo"],
|
||||
"snapToLayer": "cycleways_and_roads",
|
||||
"maxSnapDistance": 25
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -66,6 +66,11 @@
|
|||
"description": {
|
||||
"en": "Crossing for pedestrians and/or cyclists",
|
||||
"nl": "Oversteekplaats voor voetgangers en/of fietsers"
|
||||
},
|
||||
"preciseInput": {
|
||||
"preferredBackground": ["photo"],
|
||||
"snapToLayer": "cycleways_and_roads",
|
||||
"maxSnapDistance": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -79,6 +84,11 @@
|
|||
"description": {
|
||||
"en": "Traffic signal on a road",
|
||||
"nl": "Verkeerslicht op een weg"
|
||||
},
|
||||
"preciseInput": {
|
||||
"preferredBackground": ["photo"],
|
||||
"snapToLayer": "cycleways_and_roads",
|
||||
"maxSnapDistance": 25
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -16,14 +16,14 @@
|
|||
"en",
|
||||
"nl"
|
||||
],
|
||||
"maintainer": "",
|
||||
"maintainer": "MapComplete",
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"icon": "./assets/themes/cycle_infra/cycle-infra.svg",
|
||||
"version": "0",
|
||||
"startLat": 51,
|
||||
"startLon": 3.75,
|
||||
"startZoom": 11,
|
||||
"widenFactor": 0,
|
||||
"widenFactor": 0.05,
|
||||
"socialImage": "./assets/themes/cycle_infra/cycle-infra.svg",
|
||||
"enableDownload": true,
|
||||
"layers": [
|
||||
|
|
66
test.ts
66
test.ts
|
@ -2,40 +2,49 @@ import {UIEventSource} from "./Logic/UIEventSource";
|
|||
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
|
||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
||||
import State from "./State";
|
||||
import LocationInput from "./UI/Input/LocationInput";
|
||||
import Loc from "./Models/Loc";
|
||||
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||
|
||||
const layout = new UIEventSource<LayoutConfig>(AllKnownLayouts.allKnownLayouts.get("bookcases"))
|
||||
const layout = new UIEventSource<LayoutConfig>(AllKnownLayouts.allKnownLayouts.get("cycle_infra"))
|
||||
State.state = new State(layout.data)
|
||||
|
||||
const features = new UIEventSource<{ feature: any }[]>([
|
||||
{
|
||||
feature: {
|
||||
"type": "Feature",
|
||||
"properties": {"amenity": "public_bookcase", "id": "node/123"},
|
||||
|
||||
id: "node/123",
|
||||
_matching_layer_id: "public_bookcase",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
3.220506906509399,
|
||||
51.215009243433094
|
||||
[
|
||||
3.219616413116455,
|
||||
51.215315026941276
|
||||
],
|
||||
[
|
||||
3.221080899238586,
|
||||
51.21564432998662
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {
|
||||
},
|
||||
{
|
||||
feature: {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
amenity: "public_bookcase",
|
||||
id: "node/456"
|
||||
},
|
||||
_matching_layer_id: "public_bookcase",
|
||||
id: "node/456",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
3.4243011474609375,
|
||||
51.138432319543924
|
||||
[
|
||||
3.220340609550476,
|
||||
51.21547967875836
|
||||
],
|
||||
[
|
||||
3.2198095321655273,
|
||||
51.216390293480515
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -43,5 +52,22 @@ const features = new UIEventSource<{ feature: any }[]>([
|
|||
])
|
||||
|
||||
features.data.map(f => State.state.allElements.addOrGetElement(f.feature))
|
||||
|
||||
|
||||
const loc = new UIEventSource<Loc>({
|
||||
zoom: 19,
|
||||
lat: 51.21547967875836,
|
||||
lon: 3.220340609550476
|
||||
})
|
||||
const li = new LocationInput(
|
||||
{
|
||||
mapBackground: AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource<string | string[]>("map")),
|
||||
snapTo: features,
|
||||
snappedPointTags: {
|
||||
"barrier": "cycle_barrier"
|
||||
},
|
||||
maxSnapDistance: 15,
|
||||
requiresSnapping: false,
|
||||
centerLocation: loc
|
||||
}
|
||||
)
|
||||
li.SetStyle("height: 30rem").AttachTo("maindiv")
|
||||
new VariableUiElement(li.GetValue().map(JSON.stringify)).AttachTo("extradiv")
|
||||
|
|
Loading…
Reference in a new issue