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 {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||||
import SourceConfig from "./SourceConfig";
|
import SourceConfig from "./SourceConfig";
|
||||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||||
import {Tag} from "../../Logic/Tags/Tag";
|
|
||||||
import BaseUIElement from "../../UI/BaseUIElement";
|
import BaseUIElement from "../../UI/BaseUIElement";
|
||||||
import {Unit} from "./Denomination";
|
import {Unit} from "./Denomination";
|
||||||
import DeleteConfig from "./DeleteConfig";
|
import DeleteConfig from "./DeleteConfig";
|
||||||
import FilterConfig from "./FilterConfig";
|
import FilterConfig from "./FilterConfig";
|
||||||
|
import PresetConfig from "./PresetConfig";
|
||||||
|
|
||||||
export default class LayerConfig {
|
export default class LayerConfig {
|
||||||
static WAYHANDLING_DEFAULT = 0;
|
static WAYHANDLING_DEFAULT = 0;
|
||||||
|
@ -35,7 +35,7 @@ export default class LayerConfig {
|
||||||
isShown: TagRenderingConfig;
|
isShown: TagRenderingConfig;
|
||||||
minzoom: number;
|
minzoom: number;
|
||||||
minzoomVisible: number;
|
minzoomVisible: number;
|
||||||
maxzoom:number;
|
maxzoom: number;
|
||||||
title?: TagRenderingConfig;
|
title?: TagRenderingConfig;
|
||||||
titleIcons: TagRenderingConfig[];
|
titleIcons: TagRenderingConfig[];
|
||||||
icon: TagRenderingConfig;
|
icon: TagRenderingConfig;
|
||||||
|
@ -51,12 +51,7 @@ export default class LayerConfig {
|
||||||
public readonly deletion: DeleteConfig | null;
|
public readonly deletion: DeleteConfig | null;
|
||||||
public readonly allowSplit: boolean
|
public readonly allowSplit: boolean
|
||||||
|
|
||||||
presets: {
|
presets: PresetConfig[];
|
||||||
title: Translation,
|
|
||||||
tags: Tag[],
|
|
||||||
description?: Translation,
|
|
||||||
preciseInput?: { preferredBackground?: string }
|
|
||||||
}[];
|
|
||||||
|
|
||||||
tagRenderings: TagRenderingConfig[];
|
tagRenderings: TagRenderingConfig[];
|
||||||
filters: FilterConfig[];
|
filters: FilterConfig[];
|
||||||
|
@ -149,17 +144,41 @@ export default class LayerConfig {
|
||||||
this.minzoomVisible = json.minzoomVisible ?? this.minzoom;
|
this.minzoomVisible = json.minzoomVisible ?? this.minzoom;
|
||||||
this.wayHandling = json.wayHandling ?? 0;
|
this.wayHandling = json.wayHandling ?? 0;
|
||||||
this.presets = (json.presets ?? []).map((pr, i) => {
|
this.presets = (json.presets ?? []).map((pr, i) => {
|
||||||
if (pr.preciseInput === true) {
|
|
||||||
pr.preciseInput = {
|
let preciseInput = undefined;
|
||||||
preferredBackground: 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`),
|
title: Translations.T(pr.title, `${context}.presets[${i}].title`),
|
||||||
tags: pr.tags.map((t) => FromJSON.SimpleTag(t)),
|
tags: pr.tags.map((t) => FromJSON.SimpleTag(t)),
|
||||||
description: Translations.T(pr.description, `${context}.presets[${i}].description`),
|
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
|
/** 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) {
|
function render(tr: TagRenderingConfig, deflt?: string) {
|
||||||
|
if(tags === undefined){
|
||||||
|
return deflt
|
||||||
|
}
|
||||||
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt;
|
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt;
|
||||||
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
|
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconSize = render(this.iconSize, "40,40,center").split(",");
|
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");
|
let color = render(this.color, "#00f");
|
||||||
|
|
||||||
if (color.startsWith("--")) {
|
if (color.startsWith("--")) {
|
||||||
|
@ -445,24 +467,26 @@ export default class LayerConfig {
|
||||||
|
|
||||||
const iconUrlStatic = render(this.icon);
|
const iconUrlStatic = render(this.icon);
|
||||||
const self = this;
|
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?
|
// What do you mean, 'tgs' is never read?
|
||||||
// It is read implicitly in the 'render' method
|
// It is read implicitly in the 'render' method
|
||||||
const iconUrl = render(self.icon);
|
const iconUrl = render(self.icon);
|
||||||
|
@ -473,7 +497,7 @@ export default class LayerConfig {
|
||||||
iconUrl.split(";").filter((prt) => prt != "")
|
iconUrl.split(";").filter((prt) => prt != "")
|
||||||
);
|
);
|
||||||
for (const sourcePart of sourceParts) {
|
for (const sourcePart of sourceParts) {
|
||||||
htmlParts.push(genHtmlFromString(sourcePart));
|
htmlParts.push(genHtmlFromString(sourcePart, rotation));
|
||||||
}
|
}
|
||||||
|
|
||||||
let badges = [];
|
let badges = [];
|
||||||
|
@ -489,7 +513,7 @@ export default class LayerConfig {
|
||||||
.filter((prt) => prt != "");
|
.filter((prt) => prt != "");
|
||||||
|
|
||||||
for (const badgePartStr of partDefs) {
|
for (const badgePartStr of partDefs) {
|
||||||
badgeParts.push(genHtmlFromString(badgePartStr));
|
badgeParts.push(genHtmlFromString(badgePartStr, "0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const badgeCompound = new Combine(badgeParts).SetStyle(
|
const badgeCompound = new Combine(badgeParts).SetStyle(
|
||||||
|
@ -499,7 +523,7 @@ export default class LayerConfig {
|
||||||
badges.push(badgeCompound);
|
badges.push(badgeCompound);
|
||||||
} else {
|
} else {
|
||||||
htmlParts.push(
|
htmlParts.push(
|
||||||
genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt)
|
genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt, "0")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -533,7 +557,7 @@ export default class LayerConfig {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icon: {
|
icon: {
|
||||||
html: new VariableUiElement(mappedHtml),
|
html: mappedHtml === undefined ? new FixedUiElement(self.icon.render.txt) : new VariableUiElement(mappedHtml),
|
||||||
iconSize: [iconW, iconH],
|
iconSize: [iconW, iconH],
|
||||||
iconAnchor: [anchorW, anchorH],
|
iconAnchor: [anchorW, anchorH],
|
||||||
popupAnchor: [0, 3 - 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.
|
* If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category.
|
||||||
*/
|
*/
|
||||||
preciseInput?: true | {
|
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.defaultBackgroundId = json.defaultBackgroundId;
|
||||||
this.layers = LayoutConfig.ExtractLayers(json, this.units, official, context);
|
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 }[] = []
|
const roaming: { r, source: LayerConfig }[] = []
|
||||||
for (const layer of this.layers) {
|
for (const layer of this.layers) {
|
||||||
roaming.push({r: layer.GetRoamingRenderings(), source: layer});
|
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,
|
lat: number,
|
||||||
lon: number
|
lon: number
|
||||||
} | {
|
} | {
|
||||||
// Coordinates are only used for rendering
|
// Coordinates are only used for rendering. They should be lon, lat
|
||||||
locations: [number, number][]
|
locations: [number, number][]
|
||||||
nodes: number[],
|
nodes: number[],
|
||||||
} | {
|
} | {
|
||||||
|
|
|
@ -3,6 +3,8 @@ import OsmChangeAction from "./OsmChangeAction";
|
||||||
import {Changes} from "../Changes";
|
import {Changes} from "../Changes";
|
||||||
import {ChangeDescription} from "./ChangeDescription";
|
import {ChangeDescription} from "./ChangeDescription";
|
||||||
import {And} from "../../Tags/And";
|
import {And} from "../../Tags/And";
|
||||||
|
import {OsmWay} from "../OsmObject";
|
||||||
|
import {GeoOperations} from "../../GeoOperations";
|
||||||
|
|
||||||
export default class CreateNewNodeAction extends OsmChangeAction {
|
export default class CreateNewNodeAction extends OsmChangeAction {
|
||||||
|
|
||||||
|
@ -10,13 +12,20 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
||||||
private readonly _lat: number;
|
private readonly _lat: number;
|
||||||
private readonly _lon: number;
|
private readonly _lon: number;
|
||||||
|
|
||||||
public newElementId : string = undefined
|
public newElementId: string = undefined
|
||||||
|
private readonly _snapOnto: OsmWay;
|
||||||
constructor(basicTags: Tag[], lat: number, lon: number) {
|
private readonly _reusePointDistance: number;
|
||||||
|
|
||||||
|
constructor(basicTags: Tag[], lat: number, lon: number, options?: { snapOnto: OsmWay, reusePointWithinMeters?: number }) {
|
||||||
super()
|
super()
|
||||||
this._basicTags = basicTags;
|
this._basicTags = basicTags;
|
||||||
this._lat = lat;
|
this._lat = lat;
|
||||||
this._lon = lon;
|
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[] {
|
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||||
|
@ -24,7 +33,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
||||||
const properties = {
|
const properties = {
|
||||||
id: "node/" + id
|
id: "node/" + id
|
||||||
}
|
}
|
||||||
this.newElementId = "node/"+id
|
this.newElementId = "node/" + id
|
||||||
for (const kv of this._basicTags) {
|
for (const kv of this._basicTags) {
|
||||||
if (typeof kv.value !== "string") {
|
if (typeof kv.value !== "string") {
|
||||||
throw "Invalid value: don't use a regex in a preset"
|
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;
|
properties[kv.key] = kv.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [{
|
const newPointChange: ChangeDescription = {
|
||||||
tags: new And(this._basicTags).asChange(properties),
|
tags: new And(this._basicTags).asChange(properties),
|
||||||
type: "node",
|
type: "node",
|
||||||
id: id,
|
id: id,
|
||||||
changes:{
|
changes: {
|
||||||
lat: this._lat,
|
lat: this._lat,
|
||||||
lon: this._lon
|
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[] = []
|
private readonly previouslyCreated : OsmObject[] = []
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.isUploading.addCallbackAndRun(uploading => {
|
|
||||||
console.trace("Is uploading changed:", uploading)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createChangesetFor(csId: string,
|
private static createChangesetFor(csId: string,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Utils } from "../Utils";
|
||||||
|
|
||||||
export default class Constants {
|
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
|
// The user journey states thresholds when a new feature gets unlocked
|
||||||
public static userJourney = {
|
public static userJourney = {
|
||||||
|
|
|
@ -9,19 +9,16 @@ import Combine from "../Base/Combine";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||||
import {Tag} from "../../Logic/Tags/Tag";
|
|
||||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import Toggle from "../Input/Toggle";
|
import Toggle from "../Input/Toggle";
|
||||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||||
import {Translation} from "../i18n/Translation";
|
|
||||||
import LocationInput from "../Input/LocationInput";
|
import LocationInput from "../Input/LocationInput";
|
||||||
import {InputElement} from "../Input/InputElement";
|
|
||||||
import Loc from "../../Models/Loc";
|
|
||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
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:
|
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||||
|
@ -32,17 +29,12 @@ import Hash from "../../Logic/Web/Hash";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*private*/
|
/*private*/
|
||||||
interface PresetInfo {
|
interface PresetInfo extends PresetConfig {
|
||||||
description: string | Translation,
|
|
||||||
name: string | BaseUIElement,
|
name: string | BaseUIElement,
|
||||||
icon: () => BaseUIElement,
|
icon: () => BaseUIElement,
|
||||||
tags: Tag[],
|
|
||||||
layerToAddTo: {
|
layerToAddTo: {
|
||||||
layerDef: LayerConfig,
|
layerDef: LayerConfig,
|
||||||
isDisplayed: UIEventSource<boolean>
|
isDisplayed: UIEventSource<boolean>
|
||||||
},
|
|
||||||
preciseInput?: {
|
|
||||||
preferredBackground?: string
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,24 +57,43 @@ export default class SimpleAddUI extends Toggle {
|
||||||
|
|
||||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
|
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(
|
const addUi = new VariableUiElement(
|
||||||
selectedPreset.map(preset => {
|
selectedPreset.map(preset => {
|
||||||
if (preset === undefined) {
|
if (preset === undefined) {
|
||||||
return presetsOverview
|
return presetsOverview
|
||||||
}
|
}
|
||||||
return SimpleAddUI.CreateConfirmButton(preset,
|
return SimpleAddUI.CreateConfirmButton(preset,
|
||||||
(tags, location) => {
|
(tags, location, snapOntoWayId?: string) => {
|
||||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon)
|
if (snapOntoWayId === undefined) {
|
||||||
State.state.changes.applyAction(newElementAction)
|
createNewPoint(tags, location, undefined)
|
||||||
selectedPreset.setData(undefined)
|
} else {
|
||||||
isShown.setData(false)
|
OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD(way => {
|
||||||
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
|
createNewPoint(tags, location,<OsmWay> way)
|
||||||
newElementAction.newElementId
|
return true;
|
||||||
))
|
})
|
||||||
console.log("Did set selected element to",State.state.allElements.ContainingFeatures.get(
|
}
|
||||||
newElementAction.newElementId
|
|
||||||
))
|
|
||||||
}, () => {
|
},
|
||||||
|
|
||||||
|
|
||||||
|
() => {
|
||||||
selectedPreset.setData(undefined)
|
selectedPreset.setData(undefined)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -115,11 +126,11 @@ export default class SimpleAddUI extends Toggle {
|
||||||
|
|
||||||
|
|
||||||
private static CreateConfirmButton(preset: PresetInfo,
|
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 {
|
cancel: () => void): BaseUIElement {
|
||||||
|
|
||||||
let location = State.state.LastClickLocation;
|
let location = State.state.LastClickLocation;
|
||||||
let preciseInput: InputElement<Loc> = undefined
|
let preciseInput: LocationInput = undefined
|
||||||
if (preset.preciseInput !== undefined) {
|
if (preset.preciseInput !== undefined) {
|
||||||
const locationSrc = new UIEventSource({
|
const locationSrc = new UIEventSource({
|
||||||
lat: location.data.lat,
|
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))
|
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({
|
preciseInput = new LocationInput({
|
||||||
mapBackground: backgroundLayer,
|
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;")
|
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("flex flex-col")
|
||||||
).SetClass("font-bold break-words")
|
).SetClass("font-bold break-words")
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data);
|
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data, preciseInput?.snappedOnto?.data?.properties?.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (preciseInput !== undefined) {
|
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
|
// The layer is not displayed and we cannot enable the layer control -> we skip
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(layer.layerDef.name === undefined){
|
if (layer.layerDef.name === undefined) {
|
||||||
// this is a parlty hidden layer
|
// this is a parlty hidden layer
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -258,6 +282,7 @@ export default class SimpleAddUI extends Toggle {
|
||||||
tags: preset.tags,
|
tags: preset.tags,
|
||||||
layerToAddTo: layer,
|
layerToAddTo: layer,
|
||||||
name: preset.title,
|
name: preset.title,
|
||||||
|
title: preset.title,
|
||||||
description: preset.description,
|
description: preset.description,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
preciseInput: preset.preciseInput
|
preciseInput: preset.preciseInput
|
||||||
|
|
|
@ -6,28 +6,114 @@ import BaseLayer from "../../Models/BaseLayer";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import State from "../../State";
|
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> {
|
export default class LocationInput extends InputElement<Loc> {
|
||||||
|
|
||||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
private _centerLocation: UIEventSource<Loc>;
|
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>,
|
mapBackground?: UIEventSource<BaseLayer>,
|
||||||
centerLocation?: UIEventSource<Loc>,
|
snapTo?: UIEventSource<{ feature: any }[]>,
|
||||||
|
maxSnapDistance?: number,
|
||||||
|
snappedPointTags?: any,
|
||||||
|
requiresSnapping?: boolean,
|
||||||
|
centerLocation: UIEventSource<Loc>,
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
options = options ?? {}
|
this._snapTo = options.snapTo?.map(features => features?.filter(feat => feat.feature.geometry.type !== "Point"))
|
||||||
options.centerLocation = options.centerLocation ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
|
this._maxSnapDistance = options.maxSnapDistance
|
||||||
this._centerLocation = options.centerLocation;
|
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")
|
this.SetClass("block h-full")
|
||||||
}
|
}
|
||||||
|
|
||||||
GetValue(): UIEventSource<Loc> {
|
GetValue(): UIEventSource<Loc> {
|
||||||
return this._centerLocation;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
IsValid(t: Loc): boolean {
|
IsValid(t: Loc): boolean {
|
||||||
|
@ -35,41 +121,88 @@ export default class LocationInput extends InputElement<Loc> {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerConstructElement(): HTMLElement {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
const map = new Minimap(
|
try {
|
||||||
{
|
const map = new Minimap(
|
||||||
location: this._centerLocation,
|
{
|
||||||
background: this.mapBackground
|
location: this._centerLocation,
|
||||||
}
|
background: this.mapBackground
|
||||||
)
|
}
|
||||||
map.leafletMap.addCallbackAndRunD(leaflet => {
|
|
||||||
leaflet.setMaxBounds(
|
|
||||||
leaflet.getBounds().pad(0.15)
|
|
||||||
)
|
)
|
||||||
})
|
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
|
const matchPoint = this._snappedPoint.map(loc => {
|
||||||
if (leaflet === undefined || layer === undefined) {
|
if (loc === undefined) {
|
||||||
return;
|
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)
|
this.mapBackground.map(layer => {
|
||||||
leaflet.setMinZoom(layer.max_zoom - 3)
|
const leaflet = map.leafletMap.data
|
||||||
leaflet.setZoom(layer.max_zoom - 1)
|
if (leaflet === undefined || layer === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
}, [map.leafletMap])
|
leaflet.setMaxZoom(layer.max_zoom)
|
||||||
return new Combine([
|
leaflet.setMinZoom(layer.max_zoom - 3)
|
||||||
new Combine([
|
leaflet.setZoom(layer.max_zoom - 1)
|
||||||
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();
|
}, [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 readonly _leafletMap: UIEventSource<L.Map>;
|
||||||
private _cleanCount = 0;
|
private _cleanCount = 0;
|
||||||
private readonly _enablePopups: boolean;
|
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>,
|
leafletMap: UIEventSource<L.Map>,
|
||||||
layoutToUse: UIEventSource<LayoutConfig>,
|
layoutToUse: UIEventSource<LayoutConfig>,
|
||||||
enablePopups = true,
|
enablePopups = true,
|
||||||
|
@ -85,7 +85,9 @@ export default class ShowDataLayer {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State.state.selectedElement.ping()
|
if (self._enablePopups) {
|
||||||
|
State.state.selectedElement.ping()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
features.addCallback(() => update());
|
features.addCallback(() => update());
|
||||||
|
@ -106,13 +108,12 @@ export default class ShowDataLayer {
|
||||||
// We have to convert them to the appropriate icon
|
// We have to convert them to the appropriate icon
|
||||||
// Click handling is done in the next step
|
// 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];
|
const layer: LayerConfig = this._layerDict[feature._matching_layer_id];
|
||||||
|
|
||||||
if (layer === undefined) {
|
if (layer === undefined) {
|
||||||
return;
|
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 style = layer.GenerateLeafletStyle(tagSource, !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0));
|
||||||
const baseElement = style.icon.html;
|
const baseElement = style.icon.html;
|
||||||
if (!this._enablePopups) {
|
if (!this._enablePopups) {
|
||||||
|
@ -146,8 +147,8 @@ export default class ShowDataLayer {
|
||||||
autoPan: true,
|
autoPan: true,
|
||||||
closeOnEscapeKey: true,
|
closeOnEscapeKey: true,
|
||||||
closeButton: false,
|
closeButton: false,
|
||||||
autoPanPaddingTopLeft: [15,15],
|
autoPanPaddingTopLeft: [15, 15],
|
||||||
|
|
||||||
}, leafletLayer);
|
}, leafletLayer);
|
||||||
|
|
||||||
leafletLayer.bindPopup(popup);
|
leafletLayer.bindPopup(popup);
|
||||||
|
@ -191,7 +192,7 @@ export default class ShowDataLayer {
|
||||||
) {
|
) {
|
||||||
leafletLayer.openPopup()
|
leafletLayer.openPopup()
|
||||||
}
|
}
|
||||||
if(feature.id !== feature.properties.id){
|
if (feature.id !== feature.properties.id) {
|
||||||
console.trace("Not opening the popup for", feature)
|
console.trace("Not opening the popup for", feature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,11 @@
|
||||||
"description": {
|
"description": {
|
||||||
"en": "A bollard in the road",
|
"en": "A bollard in the road",
|
||||||
"nl": "Een paaltje in de weg"
|
"nl": "Een paaltje in de weg"
|
||||||
|
},
|
||||||
|
"preciseInput": {
|
||||||
|
"preferredBackground": ["photo"],
|
||||||
|
"snapToLayer": "cycleways_and_roads",
|
||||||
|
"maxSnapDistance": 25
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -66,6 +71,11 @@
|
||||||
"description": {
|
"description": {
|
||||||
"en": "Cycle barrier, slowing down cyclists",
|
"en": "Cycle barrier, slowing down cyclists",
|
||||||
"nl": "Fietshekjes, voor het afremmen van fietsers"
|
"nl": "Fietshekjes, voor het afremmen van fietsers"
|
||||||
|
},
|
||||||
|
"preciseInput": {
|
||||||
|
"preferredBackground": ["photo"],
|
||||||
|
"snapToLayer": "cycleways_and_roads",
|
||||||
|
"maxSnapDistance": 25
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -66,6 +66,11 @@
|
||||||
"description": {
|
"description": {
|
||||||
"en": "Crossing for pedestrians and/or cyclists",
|
"en": "Crossing for pedestrians and/or cyclists",
|
||||||
"nl": "Oversteekplaats voor voetgangers en/of fietsers"
|
"nl": "Oversteekplaats voor voetgangers en/of fietsers"
|
||||||
|
},
|
||||||
|
"preciseInput": {
|
||||||
|
"preferredBackground": ["photo"],
|
||||||
|
"snapToLayer": "cycleways_and_roads",
|
||||||
|
"maxSnapDistance": 25
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -79,6 +84,11 @@
|
||||||
"description": {
|
"description": {
|
||||||
"en": "Traffic signal on a road",
|
"en": "Traffic signal on a road",
|
||||||
"nl": "Verkeerslicht op een weg"
|
"nl": "Verkeerslicht op een weg"
|
||||||
|
},
|
||||||
|
"preciseInput": {
|
||||||
|
"preferredBackground": ["photo"],
|
||||||
|
"snapToLayer": "cycleways_and_roads",
|
||||||
|
"maxSnapDistance": 25
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -16,14 +16,14 @@
|
||||||
"en",
|
"en",
|
||||||
"nl"
|
"nl"
|
||||||
],
|
],
|
||||||
"maintainer": "",
|
"maintainer": "MapComplete",
|
||||||
"defaultBackgroundId": "CartoDB.Voyager",
|
"defaultBackgroundId": "CartoDB.Voyager",
|
||||||
"icon": "./assets/themes/cycle_infra/cycle-infra.svg",
|
"icon": "./assets/themes/cycle_infra/cycle-infra.svg",
|
||||||
"version": "0",
|
"version": "0",
|
||||||
"startLat": 51,
|
"startLat": 51,
|
||||||
"startLon": 3.75,
|
"startLon": 3.75,
|
||||||
"startZoom": 11,
|
"startZoom": 11,
|
||||||
"widenFactor": 0,
|
"widenFactor": 0.05,
|
||||||
"socialImage": "./assets/themes/cycle_infra/cycle-infra.svg",
|
"socialImage": "./assets/themes/cycle_infra/cycle-infra.svg",
|
||||||
"enableDownload": true,
|
"enableDownload": true,
|
||||||
"layers": [
|
"layers": [
|
||||||
|
|
66
test.ts
66
test.ts
|
@ -2,40 +2,49 @@ import {UIEventSource} from "./Logic/UIEventSource";
|
||||||
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
|
||||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
||||||
import State from "./State";
|
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)
|
State.state = new State(layout.data)
|
||||||
|
|
||||||
const features = new UIEventSource<{ feature: any }[]>([
|
const features = new UIEventSource<{ feature: any }[]>([
|
||||||
{
|
{
|
||||||
feature: {
|
feature: {
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"properties": {"amenity": "public_bookcase", "id": "node/123"},
|
"properties": {},
|
||||||
|
|
||||||
id: "node/123",
|
|
||||||
_matching_layer_id: "public_bookcase",
|
|
||||||
"geometry": {
|
"geometry": {
|
||||||
"type": "Point",
|
"type": "LineString",
|
||||||
"coordinates": [
|
"coordinates": [
|
||||||
3.220506906509399,
|
[
|
||||||
51.215009243433094
|
3.219616413116455,
|
||||||
|
51.215315026941276
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.221080899238586,
|
||||||
|
51.21564432998662
|
||||||
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
feature: {
|
feature: {
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"properties": {
|
"properties": {},
|
||||||
amenity: "public_bookcase",
|
|
||||||
id: "node/456"
|
|
||||||
},
|
|
||||||
_matching_layer_id: "public_bookcase",
|
|
||||||
id: "node/456",
|
|
||||||
"geometry": {
|
"geometry": {
|
||||||
"type": "Point",
|
"type": "LineString",
|
||||||
"coordinates": [
|
"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))
|
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