Refactoring: fix 'delete' and 'move'-buttons as special elements

This commit is contained in:
Pieter Vander Vennet 2023-04-14 02:42:57 +02:00
parent 466dd16568
commit 8a1f0599d9
19 changed files with 317 additions and 296 deletions

View file

@ -38,9 +38,9 @@ export class MenuState {
[],
(str) => MenuState._menuviewTabs.indexOf(<any>str)
)
this.themeIsOpened.addCallbackAndRun((isOpen) => {
this.menuIsOpened.addCallbackAndRun((isOpen) => {
if (!isOpen) {
this.highlightedLayerInFilters.setData(undefined)
this.highlightedUserSetting.setData(undefined)
}
})
this.themeViewTab.addCallbackAndRun((tab) => {

View file

@ -234,9 +234,9 @@ class ExpandTagRendering extends Conversion<
if (typeof layer.source !== "string") {
if (found.condition === undefined) {
found.condition = layer.source.osmTags
found.condition = layer.source["osmTags"]
} else {
found.condition = { and: [found.condition, layer.source.osmTags] }
found.condition = { and: [found.condition, layer.source["osmTags"]] }
}
}
}
@ -436,7 +436,7 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
if (typeof json.render === "string") {
spec = { "*": json.render }
} else {
spec = json.render
spec = <Record<string, string>>json.render
}
const errors: string[] = []
for (const key in spec) {
@ -480,7 +480,10 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
json: LayerConfigJson,
context: string
): { result: LayerConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
if (json.tagRenderings === undefined) {
if (
json.tagRenderings === undefined ||
json.tagRenderings.some((tr) => tr["id"] === "leftover-questions")
) {
return { result: json }
}
json = JSON.parse(JSON.stringify(json))
@ -500,7 +503,6 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
const errors: string[] = []
const warnings: string[] = []
if (noLabels.length > 1) {
console.log(json.tagRenderings)
errors.push(
"At " +
context +
@ -572,6 +574,45 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
}
}
export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
constructor() {
super(
"Add some editing elements, such as the delete button or the move button if they are configured. These used to be handled by the feature info box, but this has been replaced by special visualisation elements",
[],
"AddEditingElements"
)
}
convert(
json: LayerConfigJson,
context: string
): { result: LayerConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
json = JSON.parse(JSON.stringify(json))
if (json.allowSplit && !ValidationUtils.hasSpecialVisualisation(json, "split_button")) {
json.tagRenderings.push({
id: "split-button",
render: { "*": "{split_button()}" },
})
}
if (json.allowMove && !ValidationUtils.hasSpecialVisualisation(json, "move_button")) {
json.tagRenderings.push({
id: "move-button",
render: { "*": "{move_button()}" },
})
}
if (json.deletion && !ValidationUtils.hasSpecialVisualisation(json, "delete_button")) {
json.tagRenderings.push({
id: "delete-button",
render: { "*": "{delete_button()}" },
})
}
return { result: json }
}
}
export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[]> {
constructor() {
super("Applies a rewrite", [], "ExpandRewrite")
@ -1064,6 +1105,36 @@ class PreparePointRendering extends Fuse<PointRenderingConfigJson | LineRenderin
}
}
export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
private readonly _state: DesugaringContext
constructor(state: DesugaringContext) {
super(
"Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap",
["tagRenderings"],
"AddMiniMap"
)
this._state = state
}
convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson } {
if (!layerConfig.tagRenderings) {
return { result: layerConfig }
}
const state = this._state
const hasMinimap = ValidationUtils.hasSpecialVisualisation(layerConfig, "minimap")
if (!hasMinimap) {
layerConfig = { ...layerConfig }
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
}
return {
result: layerConfig,
}
}
}
export class PrepareLayer extends Fuse<LayerConfigJson> {
constructor(state: DesugaringContext) {
super(
@ -1072,6 +1143,9 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On("tagRenderings", (layer) => new Concat(new ExpandTagRendering(state, layer))),
new On("tagRenderings", new Each(new DetectInline())),
new AddQuestionBox(),
new AddMiniMap(state),
new AddEditingElements(),
new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On<(PointRenderingConfigJson | LineRenderingConfigJson)[], LayerConfigJson>(
"mapRendering",

View file

@ -10,7 +10,7 @@ import {
SetDefault,
} from "./Conversion"
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { AddQuestionBox, PrepareLayer } from "./PrepareLayer"
import { PrepareLayer } from "./PrepareLayer"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { Utils } from "../../../Utils"
import Constants from "../../Constants"
@ -295,56 +295,6 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
}
}
export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
private readonly _state: DesugaringContext
constructor(state: DesugaringContext) {
super(
"Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap",
["tagRenderings"],
"AddMiniMap"
)
this._state = state
}
/**
* Returns true if this tag rendering has a minimap in some language.
* Note: this minimap can be hidden by conditions
*
* AddMiniMap.hasMinimap({render: "{minimap()}"}) // => true
* AddMiniMap.hasMinimap({render: {en: "{minimap()}"}}) // => true
* AddMiniMap.hasMinimap({render: {en: "{minimap()}", nl: "{minimap()}"}}) // => true
* AddMiniMap.hasMinimap({render: {en: "{minimap()}", nl: "No map for the dutch!"}}) // => true
* AddMiniMap.hasMinimap({render: "{minimap()}"}) // => true
* AddMiniMap.hasMinimap({render: "{minimap(18,featurelist)}"}) // => true
* AddMiniMap.hasMinimap({mappings: [{if: "xyz=abc",then: "{minimap(18,featurelist)}"}]}) // => true
* AddMiniMap.hasMinimap({render: "Some random value {key}"}) // => false
* AddMiniMap.hasMinimap({render: "Some random value {minimap}"}) // => false
*/
static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean {
return ValidationUtils.getSpecialVisualisations(renderingConfig).some(
(vis) => vis.funcName === "minimap"
)
}
convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson } {
const state = this._state
const hasMinimap =
layerConfig.tagRenderings?.some((tr) =>
AddMiniMap.hasMinimap(<TagRenderingConfigJson>tr)
) ?? true
if (!hasMinimap) {
layerConfig = { ...layerConfig }
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
}
return {
result: layerConfig,
}
}
}
class AddContextToTranslationsInLayout extends DesugaringStep<LayoutConfigJson> {
constructor() {
super(
@ -660,9 +610,7 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
? new Pass("AddDefaultLayers is disabled due to the set flag")
: new AddDefaultLayers(state),
new AddDependencyLayersToTheme(state),
new AddImportLayers(),
new On("layers", new Each(new AddQuestionBox())),
new On("layers", new Each(new AddMiniMap(state)))
new AddImportLayers()
)
}

View file

@ -2,8 +2,22 @@ import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import { RenderingSpecification, SpecialVisualization } from "../../../UI/SpecialVisualization"
import { LayerConfigJson } from "../Json/LayerConfigJson"
export default class ValidationUtils {
public static hasSpecialVisualisation(
layer: LayerConfigJson,
specialVisualisation: string
): boolean {
return (
layer.tagRenderings?.some((tagRendering) =>
ValidationUtils.getSpecialVisualisations(<TagRenderingConfigJson>tagRendering).some(
(vis) => vis.funcName === specialVisualisation
)
) ?? false
)
}
/**
* Gives all the (function names of) used special visualisations
* @param renderingConfig
@ -15,6 +29,7 @@ export default class ValidationUtils {
(spec) => spec["func"]
)
}
public static getSpecialVisualsationsWithArgs(
renderingConfig: TagRenderingConfigJson
): RenderingSpecification[] {

View file

@ -90,7 +90,7 @@ export default class LayerConfig extends WithContextLoader {
if (json.source === "special" || json.source === "special:library") {
this.source = null
} else if (json.source.osmTags === undefined) {
} else if (json.source["osmTags"] === undefined) {
throw (
"Layer " +
this.id +
@ -122,8 +122,8 @@ export default class LayerConfig extends WithContextLoader {
}
this.syncSelection = json.syncSelection ?? "no"
if (typeof json.source !== "string") {
this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30
const osmTags = TagUtils.Tag(json.source.osmTags, context + "source.osmTags")
this.maxAgeOfCache = json.source["maxCacheAge"] ?? 24 * 60 * 60 * 30
const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
if (osmTags.isNegative()) {
throw (
context +

View file

@ -248,7 +248,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
new UIEventSource<Record<string, string>>(last_click.properties)
)
new ShowDataLayer(this.map, {
features: last_click,
features: new FilteringFeatureSource(last_click_layer, last_click),
doShowLayer: new ImmutableStore(true),
layer: last_click_layer.layerDef,
selectedElement: this.selectedElement,

View file

@ -10,56 +10,58 @@
import Hotkeys from "../Base/Hotkeys";
import { Geocoding } from "../../Logic/Osm/Geocoding";
import { BBox } from "../../Logic/BBox";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore";
export let state: SpecialVisualizationState
export let bounds: UIEventSource<BBox>
export let selectedElement: UIEventSource<Feature>;
export let selectedLayer: UIEventSource<LayerConfig>;
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined;
export let bounds: UIEventSource<BBox>;
export let selectedElement: UIEventSource<Feature> | undefined = undefined;
export let selectedLayer: UIEventSource<LayerConfig> | undefined = undefined;
let searchContents: string = undefined;
let isRunning: boolean = false;
let inputElement: HTMLInputElement;
let feedback: string = undefined
let feedback: string = undefined;
Hotkeys.RegisterHotkey(
{ ctrl: "F" },
Translations.t.hotkeyDocumentation.selectSearch,
() => {
inputElement?.focus()
inputElement?.select()
inputElement?.focus();
inputElement?.select();
}
)
);
async function performSearch() {
try {
isRunning = true;
searchContents = searchContents?.trim() ?? ""
searchContents = searchContents?.trim() ?? "";
if (searchContents === "") {
return
return;
}
const result = await Geocoding.Search(searchContents, bounds.data)
const result = await Geocoding.Search(searchContents, bounds.data);
if (result.length == 0) {
feedback = Translations.t.search.nothing.txt
return
feedback = Translations.t.search.nothing.txt;
return;
}
const poi = result[0]
const [lat0, lat1, lon0, lon1] = poi.boundingbox
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01))
const id = poi.osm_type + "/" + poi.osm_id
const perLayer = state.perLayer
const layers = Array.from(perLayer.values())
for (const layer of layers) {
const found = layer.features.data.find(f => f.properties.id === id)
selectedElement.setData(found)
selectedLayer.setData(layer.layer.layerDef)
const poi = result[0];
const [lat0, lat1, lon0, lon1] = poi.boundingbox;
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01));
if (perLayer !== undefined) {
const id = poi.osm_type + "/" + poi.osm_id;
const layers = Array.from(perLayer?.values() ?? []);
for (const layer of layers) {
const found = layer.features.data.find(f => f.properties.id === id);
selectedElement?.setData(found);
selectedLayer?.setData(layer.layer.layerDef);
}
}
}catch (e) {
console.error(e)
feedback = Translations.t.search.error.txt
} catch (e) {
console.error(e);
feedback = Translations.t.search.error.txt;
} finally {
isRunning = false;
}
@ -72,7 +74,7 @@
{#if isRunning}
<Loading>{Translations.t.general.search.searching}</Loading>
{:else if feedback !== undefined}
{:else if feedback !== undefined}
<div class="alert" on:click={() => feedback = undefined}>
{feedback}
</div>

View file

@ -177,9 +177,7 @@ export class ImageUploadFlow extends Toggle {
)
.onClick(() => {
console.log("Opening the license settings... ")
ScrollableFullScreen.collapse()
DefaultGuiState.state.userInfoIsOpened.setData(true)
DefaultGuiState.state.userInfoFocusedQuestion.setData("picture-license")
state.guistate.openUsersettings("picture-license")
})
.SetClass("underline"),
]).SetStyle("font-size:small;"),

View file

@ -4,8 +4,6 @@
import { Map as MlMap } from "maplibre-gl";
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor";
import MaplibreMap from "../../Map/MaplibreMap.svelte";
import Svg from "../../../Svg";
import ToSvelte from "../../Base/ToSvelte.svelte";
import DragInvitation from "../../Base/DragInvitation.svelte";
/**
@ -14,13 +12,13 @@
export let value: UIEventSource<{lon: number, lat: number}>;
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> } = undefined;
/**
* Called when setup is done, cna be used to add layrs to the map
* Called when setup is done, can be used to add more layers to the map
*/
export let onCreated : (value: Store<{lon: number, lat: number}> , map: Store<MlMap>, mapProperties: MapProperties ) => void
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
let mla = new MapLibreAdaptor(map, mapProperties);
mapProperties.location.syncWith(value)
if(onCreated){
onCreated(value, map, mla)
}

View file

@ -8,7 +8,7 @@ import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
import { OsmTags } from "../../Models/OsmFeature"
import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource"
import { BBox } from "../../Logic/BBox"
import { Feature } from "geojson"
import { Feature, Point } from "geojson"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
import { Utils } from "../../Utils"
@ -26,6 +26,7 @@ class PointRenderingLayer {
private readonly _onClick: (feature: Feature) => void
private readonly _allMarkers: Map<string, Marker> = new Map<string, Marker>()
private _dirty = false
constructor(
map: MlMap,
features: FeatureSource,
@ -139,6 +140,18 @@ class PointRenderingLayer {
store
.map((tags) => this._config.rotationAlignment.GetRenderValue(tags).Subs(tags).txt)
.addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(pitchAligment))
if (feature.geometry.type === "Point") {
// When the tags get 'pinged', check that the location didn't change
store.addCallbackAndRunD(() => {
// Check if the location is still the same
const oldLoc = marker.getLngLat()
const newloc = (<Point>feature.geometry).coordinates
if (newloc[0] === oldLoc.lng && newloc[1] === oldLoc.lat) {
return
}
marker.setLngLat({ lon: newloc[0], lat: newloc[1] })
})
}
return marker
}
}
@ -159,6 +172,7 @@ class LineRenderingLayer {
private static readonly lineConfigKeysColor = ["color", "fillColor"] as const
private static readonly lineConfigKeysNumber = ["width", "offset"] as const
private static missingIdTriggered = false
private readonly _map: MlMap
private readonly _config: LineRenderingConfig
private readonly _visibility?: Store<boolean>
@ -167,7 +181,6 @@ class LineRenderingLayer {
private readonly _layername: string
private readonly _listenerInstalledOn: Set<string> = new Set<string>()
private static missingIdTriggered = false
constructor(
map: MlMap,
features: FeatureSource,
@ -248,6 +261,11 @@ class LineRenderingLayer {
},
})
map.on("click", linelayer, (e) => {
console.log("Click", e)
e.originalEvent["consumed"] = true
this._onClick(e.features[0])
})
const polylayer = this._layername + "_polygon"
map.addLayer({
source: this._layername,
@ -260,6 +278,10 @@ class LineRenderingLayer {
"fill-opacity": 0.1,
},
})
map.on("click", polylayer, (e) => {
e.originalEvent["consumed"] = true
this._onClick(e.features[0])
})
this._visibility?.addCallbackAndRunD((visible) => {
try {

View file

@ -20,10 +20,10 @@ import { RadioButton } from "../Input/RadioButton"
import { FixedInputElement } from "../Input/FixedInputElement"
import Title from "../Base/Title"
import { SubstitutedTranslation } from "../SubstitutedTranslation"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import TagRenderingQuestion from "./TagRenderingQuestion"
import { OsmId } from "../../Models/OsmFeature"
import { OsmId, OsmTags } from "../../Models/OsmFeature"
import { LoginToggle } from "./LoginButton"
import { SpecialVisualizationState } from "../SpecialVisualization"
export default class DeleteWizard extends Toggle {
/**
@ -41,13 +41,18 @@ export default class DeleteWizard extends Toggle {
* Ideal for the case of "THIS PATH IS ON MY GROUND AND SHOULD BE DELETED IMMEDIATELY OR I WILL GET MY LAWYER" but to mark it as private instead.
* (Note that _delete_reason is used as trigger to do actual deletion - setting such a tag WILL delete from the database with that as changeset comment)
*
* @param id: The id of the element to remove
* @param state: the state of the application
* @param options softDeletionTags: the tags to apply if the user doesn't have permission to delete, e.g. 'disused:amenity=public_bookcase', 'amenity='. After applying, the element should not be picked up on the map anymore. If undefined, the wizard will only show up if the point can be (hard) deleted
*/
constructor(id: OsmId, state: FeaturePipelineState, options: DeleteConfig) {
const deleteAbility = new DeleteabilityChecker(id, state, options.neededChangesets)
const tagsSource = state.allElements.getEventSourceById(id)
constructor(
id: OsmId,
tagsSource: UIEventSource<OsmTags>,
state: SpecialVisualizationState,
options: DeleteConfig
) {
const deleteAbility = new DeleteabilityChecker(
id,
state.osmConnection,
options.neededChangesets
)
const isDeleted = new UIEventSource(false)
const allowSoftDeletion = !!options.softDeletionTags
@ -62,7 +67,7 @@ export default class DeleteWizard extends Toggle {
if (selected["retagTo"] !== undefined) {
// no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping
actionToTake = new ChangeTagAction(id, selected["retagTo"], tagsSource.data, {
theme: state?.layoutToUse?.id ?? "unkown",
theme: state?.layout?.id ?? "unkown",
changeType: "special-delete",
})
} else {
@ -70,7 +75,7 @@ export default class DeleteWizard extends Toggle {
id,
options.softDeletionTags,
{
theme: state?.layoutToUse?.id ?? "unkown",
theme: state?.layout?.id ?? "unkown",
specialMotivation: selected["deleteReason"],
},
deleteAbility.canBeDeleted.data.canBeDeleted
@ -250,7 +255,7 @@ export default class DeleteWizard extends Toggle {
private static constructMultipleChoice(
config: DeleteConfig,
tagsSource: UIEventSource<Record<string, string>>,
state: FeaturePipelineState
state: SpecialVisualizationState
): InputElement<{ deleteReason: string } | { retagTo: TagsFilter }> {
const elements: InputElement<{ deleteReason: string } | { retagTo: TagsFilter }>[] = []
@ -282,19 +287,13 @@ export default class DeleteWizard extends Toggle {
class DeleteabilityChecker {
public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>
private readonly _id: string
private readonly _id: OsmId
private readonly _allowDeletionAtChangesetCount: number
private readonly _state: {
osmConnection: OsmConnection
}
private readonly _osmConnection: OsmConnection
constructor(
id: string,
state: { osmConnection: OsmConnection },
allowDeletionAtChangesetCount?: number
) {
constructor(id: OsmId, osmConnection: OsmConnection, allowDeletionAtChangesetCount?: number) {
this._id = id
this._state = state
this._osmConnection = osmConnection
this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE
this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({
@ -324,7 +323,7 @@ class DeleteabilityChecker {
}
// Does the currently logged in user have enough experience to delete this point?
const deletingPointsOfOtherAllowed = this._state.osmConnection.userDetails.map((ud) => {
const deletingPointsOfOtherAllowed = this._osmConnection.userDetails.map((ud) => {
if (ud === undefined) {
return undefined
}
@ -347,10 +346,10 @@ class DeleteabilityChecker {
// Not yet downloaded
return null
}
const userId = self._state.osmConnection.userDetails.data.uid
const userId = self._osmConnection.userDetails.data.uid
return !previous.some((editor) => editor !== userId)
},
[self._state.osmConnection.userDetails]
[self._osmConnection.userDetails]
)
// User allowed OR only edited by self?

View file

@ -17,7 +17,6 @@ import MoveWizard from "./MoveWizard"
import Toggle from "../Input/Toggle"
import Lazy from "../Base/Lazy"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import { Tag } from "../../Logic/Tags/Tag"
import Svg from "../../Svg"
import Translations from "../i18n/Translations"
@ -32,9 +31,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
setHash?: true | boolean
}
) {
if (state === undefined) {
throw "State is undefined!"
}
const showAllQuestions = state.featureSwitchShowAllQuestions.map(
(fsShow) => fsShow || state.showAllQuestionsAtOnce.data,
[state.showAllQuestionsAtOnce]
@ -98,27 +94,11 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
private static GenerateMainContent(
tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: FeaturePipelineState,
showAllQuestions?: Store<boolean>
state: FeaturePipelineState
): BaseUIElement {
let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>()
const t = Translations.t.general
const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map((tr) => tr.group))
if (state?.featureSwitchUserbadge?.data ?? true) {
const questionSpecs = layerConfig.tagRenderings.filter((tr) => tr.id === "questions")
for (const groupName of allGroupNames) {
const questions = layerConfig.tagRenderings.filter((tr) => tr.group === groupName)
const questionSpec = questionSpecs.filter((tr) => tr.group === groupName)[0]
const questionBox = new QuestionBox(state, {
tagsSource: tags,
tagRenderings: questions,
units: layerConfig.units,
showAllQuestionsAtOnce:
questionSpec?.freeform?.helperArgs["showAllQuestions"] ?? showAllQuestions,
})
questionBoxes.set(groupName, questionBox)
}
}
const withQuestion = layerConfig.tagRenderings.filter(
(tr) => tr.question !== undefined
@ -243,40 +223,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
editElements.push(questionBox)
})
if (layerConfig.allowMove) {
editElements.push(
new VariableUiElement(
tags
.map((tags) => tags.id)
.map((id) => {
const feature = state.allElements.ContainingFeatures.get(id)
if (feature === undefined) {
return "This feature is not register in the state.allElements and cannot be moved"
}
return new MoveWizard(feature, state, layerConfig.allowMove)
})
).SetClass("text-base")
)
}
if (layerConfig.deletion) {
editElements.push(
new VariableUiElement(
tags
.map((tags) => tags.id)
.map((id) => new DeleteWizard(id, state, layerConfig.deletion))
).SetClass("text-base")
)
}
if (layerConfig.allowSplit) {
editElements.push(
new VariableUiElement(
tags.map((tags) => tags.id).map((id) => new SplitRoadWizard(id, state))
).SetClass("text-base")
)
}
editElements.push(
new VariableUiElement(
state.osmConnection.userDetails
@ -302,30 +248,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
)
)
editElements.push(
Toggle.If(state.featureSwitchIsDebugging, () => {
const config_all_tags: TagRenderingConfig = new TagRenderingConfig(
{ render: "{all_tags()}" },
""
)
const config_download: TagRenderingConfig = new TagRenderingConfig(
{ render: "{export_as_geojson()}" },
""
)
const config_id: TagRenderingConfig = new TagRenderingConfig(
{ render: "{open_in_iD()}" },
""
)
return new Combine([
new TagRenderingAnswer(tags, config_all_tags, state),
new TagRenderingAnswer(tags, config_download, state),
new TagRenderingAnswer(tags, config_id, state),
"This is layer " + layerConfig.id,
])
})
)
return new Combine(editElements).SetClass("flex flex-col")
}
}

View file

@ -1,29 +1,28 @@
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
import Svg from "../../Svg"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Toggle from "../Input/Toggle"
import { UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement"
import { Translation } from "../i18n/Translation"
import BaseUIElement from "../BaseUIElement"
import LocationInput from "../Input/LocationInput"
import Loc from "../../Models/Loc"
import { GeoOperations } from "../../Logic/GeoOperations"
import { OsmObject } from "../../Logic/Osm/OsmObject"
import { Changes } from "../../Logic/Osm/Changes"
import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import MoveConfig from "../../Models/ThemeConfig/MoveConfig"
import { ElementStorage } from "../../Logic/ElementStorage"
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
import BaseLayer from "../../Models/BaseLayer"
import SearchAndGo from "../BigComponents/SearchAndGo"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../Logic/Tags/And"
import { Tag } from "../../Logic/Tags/Tag"
import { LoginToggle } from "./LoginButton"
import { SpecialVisualizationState } from "../SpecialVisualization"
import { Feature, Point } from "geojson"
import { OsmTags } from "../../Models/OsmFeature"
import SvelteUIElement from "../Base/SvelteUIElement"
import { MapProperties } from "../../Models/MapProperties"
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
import Geosearch from "../BigComponents/Geosearch.svelte"
import Constants from "../../Models/Constants"
interface MoveReason {
text: Translation | string
@ -43,14 +42,9 @@ export default class MoveWizard extends Toggle {
* The UI-element which helps moving a point
*/
constructor(
featureToMove: any,
state: {
osmConnection: OsmConnection
featureSwitchUserbadge: UIEventSource<boolean>
changes: Changes
layoutToUse: LayoutConfig
allElements: ElementStorage
},
featureToMove: Feature<Point>,
tags: UIEventSource<OsmTags>,
state: SpecialVisualizationState,
options: MoveConfig
) {
const t = Translations.t.move
@ -130,56 +124,38 @@ export default class MoveWizard extends Toggle {
if (reason === undefined) {
return undefined
}
const loc = new UIEventSource<Loc>({
lon: lon,
lat: lat,
zoom: reason?.startZoom ?? 16,
})
let background: string[]
if (typeof reason.background == "string") {
background = [reason.background]
} else {
background = reason.background
const mapProperties: Partial<MapProperties> = {
minzoom: new UIEventSource(reason.minZoom),
zoom: new UIEventSource(reason?.startZoom ?? 16),
location: new UIEventSource({ lon, lat }),
bounds: new UIEventSource(undefined),
}
const preferredBackground = AvailableBaseLayers.SelectBestLayerAccordingTo(
loc,
new UIEventSource(background)
).data
const locationInput = new LocationInput({
minZoom: reason.minZoom,
centerLocation: loc,
mapBackground: new UIEventSource<BaseLayer>(preferredBackground), // We detach the layer
state: <any>state,
const value = new UIEventSource<{ lon: number; lat: number }>(undefined)
const locationInput = new SvelteUIElement(LocationInput, {
mapProperties,
value,
})
if (reason.lockBounds) {
locationInput.installBounds(0.05, true)
}
let searchPanel: BaseUIElement = undefined
if (reason.includeSearch) {
searchPanel = new SearchAndGo({
leafletMap: locationInput.leafletMap,
})
searchPanel = new SvelteUIElement(Geosearch, { bounds: mapProperties.bounds })
}
locationInput.SetStyle("height: 17.5rem")
const confirmMove = new SubtleButton(Svg.move_confirm_svg(), t.confirmMove)
confirmMove.onClick(async () => {
const loc = locationInput.GetValue().data
const loc = value.data
await state.changes.applyAction(
new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], {
reason: reason.changesetCommentValue,
theme: state.layoutToUse.id,
theme: state.layout.id,
})
)
featureToMove.properties._lat = loc.lat
featureToMove.properties._lon = loc.lon
featureToMove.geometry.coordinates = [loc.lon, loc.lat]
if (reason.eraseAddressFields) {
await state.changes.applyAction(
new ChangeTagAction(
@ -193,13 +169,13 @@ export default class MoveWizard extends Toggle {
featureToMove.properties,
{
changeType: "relocated",
theme: state.layoutToUse.id,
theme: state.layout.id,
}
)
)
}
state.allElements.getEventSourceById(id).ping()
state.featureProperties.getStore(id).ping()
currentStep.setData("moved")
})
const zoomInFurhter = t.zoomInFurther.SetClass("alert block m-6")
@ -209,7 +185,7 @@ export default class MoveWizard extends Toggle {
new Toggle(
confirmMove,
zoomInFurhter,
locationInput.GetValue().map((l) => l.zoom >= 19)
mapProperties.zoom.map((zoom) => zoom >= Constants.minZoomLevelToAddNewPoint)
),
]).SetClass("flex flex-col")
})

View file

@ -2,33 +2,27 @@ import Toggle from "../Input/Toggle"
import Svg from "../../Svg"
import { UIEventSource } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton"
import Minimap from "../Base/Minimap"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import { GeoOperations } from "../../Logic/GeoOperations"
import { LeafletMouseEvent } from "leaflet"
import Combine from "../Base/Combine"
import { Button } from "../Base/Button"
import Translations from "../i18n/Translations"
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
import Title from "../Base/Title"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { BBox } from "../../Logic/BBox"
import split_point from "../../assets/layers/split_point/split_point.json"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Changes } from "../../Logic/Osm/Changes"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ElementStorage } from "../../Logic/ElementStorage"
import BaseLayer from "../../Models/BaseLayer"
import FilteredLayer from "../../Models/FilteredLayer"
import BaseUIElement from "../BaseUIElement"
import { VariableUiElement } from "../Base/VariableUIElement"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { LoginToggle } from "./LoginButton"
import { SpecialVisualizationState } from "../SpecialVisualization"
export default class SplitRoadWizard extends Combine {
// @ts-ignore
private static splitLayerStyling = new LayerConfig(
split_point,
"(BUILTIN) SplitRoadWizard.ts",
@ -43,22 +37,7 @@ export default class SplitRoadWizard extends Combine {
* @param id: The id of the road to remove
* @param state: the state of the application
*/
constructor(
id: string,
state: {
filteredLayers: UIEventSource<FilteredLayer[]>
backgroundLayer: UIEventSource<BaseLayer>
featureSwitchIsTesting: UIEventSource<boolean>
featureSwitchIsDebugging: UIEventSource<boolean>
featureSwitchShowAllQuestions: UIEventSource<boolean>
osmConnection: OsmConnection
featureSwitchUserbadge: UIEventSource<boolean>
changes: Changes
layoutToUse: LayoutConfig
allElements: ElementStorage
selectedElement: UIEventSource<any>
}
) {
constructor(id: string, state: SpecialVisualizationState) {
const t = Translations.t.split
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring

View file

@ -26,18 +26,27 @@
}));
let htmlElem: HTMLElement;
const _htmlElement = new UIEventSource<HTMLElement>(undefined);
$: _htmlElement.setData(htmlElem);
function setHighlighting() {
if (highlightedRendering === undefined) {
return;
}
if (htmlElem === undefined) {
return;
}
const highlighted = highlightedRendering.data;
if (config.id === highlighted) {
htmlElem.classList.add("glowing-shadow");
} else {
htmlElem.classList.remove("glowing-shadow");
}
}
if (highlightedRendering) {
$: onDestroy(highlightedRendering.addCallbackAndRun(highlighted => {
console.log("Highlighted rendering is", highlighted)
if(htmlElem === undefined){
return
}
if (config.id === highlighted) {
htmlElem.classList.add("glowing-shadow");
} else {
htmlElem.classList.remove("glowing-shadow");
}
}));
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()))
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()))
}

View file

@ -54,7 +54,7 @@ import Maproulette from "../Logic/Maproulette"
import SvelteUIElement from "./Base/SvelteUIElement"
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import QuestionViz from "./Popup/QuestionViz"
import { Feature } from "geojson"
import { Feature, Point } from "geojson"
import { GeoOperations } from "../Logic/GeoOperations"
import CreateNewNote from "./Popup/CreateNewNote.svelte"
import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
@ -76,6 +76,9 @@ import { SaveButton } from "./Popup/SaveButton"
import Lazy from "./Base/Lazy"
import { CheckBox } from "./Input/Checkboxes"
import Slider from "./Input/Slider"
import DeleteWizard from "./Popup/DeleteWizard"
import { OsmId, OsmTags } from "../Models/OsmFeature"
import MoveWizard from "./Popup/MoveWizard"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -226,6 +229,7 @@ class StealViz implements SpecialVisualization {
required: true,
},
]
constr(state: SpecialVisualizationState, featureTags, args) {
const [featureIdKey, layerAndtagRenderingIds] = args
const tagRenderings: [LayerConfig, TagRenderingConfig][] = []
@ -273,6 +277,7 @@ class StealViz implements SpecialVisualization {
return [layerId]
}
}
export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
@ -521,6 +526,74 @@ export default class SpecialVisualizations {
new HistogramViz(),
new StealViz(),
new MinimapViz(),
{
funcName: "split_button",
docs: "Adds a button which allows to split a way",
args: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
return new VariableUiElement(
// TODO
tagSource
.map((tags) => tags.id)
.map((id) => new FixedUiElement("TODO: enable splitting")) // new SplitRoadWizard(id, state))
)
},
},
{
funcName: "move_button",
docs: "Adds a button which allows to move the object to another location. The config will be read from the layer config",
args: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
if (feature.geometry.type !== "Point") {
return undefined
}
return new MoveWizard(
<Feature<Point>>feature,
<UIEventSource<OsmTags>>tagSource,
state,
layer.allowMove
)
},
},
{
funcName: "delete_button",
docs: "Adds a button which allows to delete the object at this location. The config will be read from the layer config",
args: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
return new VariableUiElement(
tagSource
.map((tags) => tags.id)
.map(
(id) =>
new DeleteWizard(
<OsmId>id,
<UIEventSource<OsmTags>>tagSource,
state,
layer.deletion // Reading the configuration from the layerconfig is a bit cheating and should be factored out
)
)
)
},
},
new ShareLinkViz(),
new UploadToOsmViz(),
new MultiApplyViz(),

View file

@ -93,7 +93,7 @@
<div class="absolute top-0 right-0 mt-4 mr-4">
<If condition={state.featureSwitches.featureSwitchSearch}>
<Geosearch bounds={state.mapProperties.bounds} {selectedElement} {selectedLayer} {state}></Geosearch>
<Geosearch bounds={state.mapProperties.bounds} {selectedElement} {selectedLayer} perLayer={state.perLayer}></Geosearch>
</If>
</div>

View file

@ -2,6 +2,12 @@
"id": "last_click",
"description": "This layer defines how to render the 'last click'-location. By default, it will show a marker with the possibility to add a new point (if there are some presets) and/or to add a new note (if the 'note' layer attribute is set). If none are possible, this layer won't show up",
"source": "special",
"isShown": {
"or": [
"has_presets=yes",
"has_note_layer=yes"
]
},
"name": null,
"titleIcons": [],
"title": {
@ -153,4 +159,4 @@
]
}
]
}
}

View file

@ -116,7 +116,7 @@
"mappings": [
{
"if": "theme=advertising",
"then": "./assets/themes/advertising/poster_box.svg"
"then": "./assets/themes/advertising/icon.svg"
},
{
"if": "theme=aed",