Refactoring: add metatagging, add 'last edited by' element, add 'metacondition'

This commit is contained in:
Pieter Vander Vennet 2023-04-15 02:28:24 +02:00
parent 771783a31c
commit 105120060d
31 changed files with 217 additions and 142 deletions

View file

@ -62,7 +62,11 @@ export default class DetermineLayout {
layoutId,
"The layout to load into MapComplete"
).data
return AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase())
const layout = AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase())
if (layout === undefined) {
throw "No layout with name " + layoutId + " exists"
}
return layout
}
public static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>): LayoutConfig | null {

View file

@ -6,7 +6,7 @@ import { UIEventSource } from "../../UIEventSource"
*/
export default class FeaturePropertiesStore {
private readonly _source: FeatureSource & IndexedFeatureSource
private readonly _elements = new Map<string, UIEventSource<any>>()
private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>()
constructor(source: FeatureSource & IndexedFeatureSource) {
this._source = source
@ -83,7 +83,9 @@ export default class FeaturePropertiesStore {
return changeMade
}
addAlias(oldId: string, newId: string): void {
// noinspection JSUnusedGlobalSymbols
public addAlias(oldId: string, newId: string): void {
console.log("FeaturePropertiesStore: adding alias for", oldId, newId)
if (newId === undefined) {
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
const element = this._elements.get(oldId)

View file

@ -94,8 +94,9 @@ export default class MetaTagging {
let definedTags = new Set(Object.getOwnPropertyNames(feature.properties))
for (const metatag of metatagsToApply) {
try {
if (!metatag.keys.some((key) => feature.properties[key] === undefined)) {
if (!metatag.keys.some((key) => !(key in feature.properties))) {
// All keys are already defined, we probably already ran this one
// Note that we use 'key in properties', not 'properties[key] === undefined'. The latter will cause evaluation of lazy properties
continue
}

View file

@ -96,16 +96,11 @@ export class ReferencingWaysMetaTagger extends SimpleMetaTagger {
return false
}
console.trace("Downloading referencing ways for", feature.properties.id)
OsmObject.DownloadReferencingWays(id).then((referencingWays) => {
const currentTagsSource = state.allElements?.getEventSourceById(id) ?? []
Utils.AddLazyPropertyAsync(feature.properties, "_referencing_ways", async () => {
const referencingWays = await OsmObject.DownloadReferencingWays(id)
const wayIds = referencingWays.map((w) => "way/" + w.id)
wayIds.sort()
const wayIdsStr = wayIds.join(";")
if (wayIdsStr !== "" && currentTagsSource.data["_referencing_ways"] !== wayIdsStr) {
currentTagsSource.data["_referencing_ways"] = wayIdsStr
currentTagsSource.ping()
}
return wayIds.join(";")
})
return true
@ -221,6 +216,7 @@ class RewriteMetaInfoTags extends SimpleMetaTagger {
return movedSomething
}
}
export default class SimpleMetaTaggers {
/**
* A simple metatagger which rewrites various metatags as needed

View file

@ -575,12 +575,14 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
}
export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
constructor() {
private readonly _desugaring: DesugaringContext
constructor(desugaring: DesugaringContext) {
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"
)
this._desugaring = desugaring
}
convert(
@ -609,6 +611,30 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
})
}
if (json.deletion && !ValidationUtils.hasSpecialVisualisation(json, "all_tags")) {
const trc: TagRenderingConfigJson = {
id: "all-tags",
render: { "*": "{all_tags()}" },
metacondition: {
or: [
"__featureSwitchIsTesting=true",
"__featureSwitchIsDebugging=true",
"mapcomplete-show_debug=yes",
],
},
}
json.tagRenderings.push(trc)
}
if (
json.source !== "special" &&
json.source !== "special:library" &&
json.tagRenderings &&
!json.tagRenderings.some((tr) => tr["id"] === "last_edit")
) {
json.tagRenderings.push(this._desugaring.tagRenderings.get("last_edit"))
}
return { result: json }
}
}
@ -1145,7 +1171,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new On("tagRenderings", new Each(new DetectInline())),
new AddQuestionBox(),
new AddMiniMap(state),
new AddEditingElements(),
new AddEditingElements(state),
new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On<(PointRenderingConfigJson | LineRenderingConfigJson)[], LayerConfigJson>(
"mapRendering",

View file

@ -47,6 +47,10 @@ export default class TagRenderingConfig {
public readonly question?: TypedTranslation<object>
public readonly questionhint?: TypedTranslation<object>
public readonly condition?: TagsFilter
/**
* Evaluated against the current 'usersettings'-state
*/
public readonly metacondition?: TagsFilter
public readonly description?: Translation
public readonly configuration_warnings: string[] = []
@ -70,14 +74,6 @@ export default class TagRenderingConfig {
if (json === undefined) {
throw "Initing a TagRenderingConfig with undefined in " + context
}
if (json === "questions") {
// Very special value
this.render = null
this.question = null
this.condition = null
this.id = "questions"
return
}
if (typeof json === "number") {
json = "" + json
@ -114,11 +110,15 @@ export default class TagRenderingConfig {
}
this.labels = json.labels ?? []
this.render = Translations.T(json.render, translationKey + ".render")
this.render = Translations.T(<any>json.render, translationKey + ".render")
this.question = Translations.T(json.question, translationKey + ".question")
this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
this.description = Translations.T(json.description, translationKey + ".description")
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
this.metacondition = TagUtils.Tag(
json.metacondition ?? { and: [] },
`${context}.metacondition`
)
if (json.freeform) {
if (
json.freeform.addExtraTags !== undefined &&

View file

@ -205,6 +205,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
*/
private miscSetup() {
this.userRelatedState.markLayoutAsVisited(this.layout)
this.selectedElement.addCallbackAndRunD(() => {
// As soon as we have a selected element, we clear it
// This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
this.lastClickObject.features.setData([])
})
}
private initHotkeys() {

View file

@ -18,6 +18,11 @@
onDestroy(tags.addCallbackAndRun(tags => {
_tags = tags;
}));
let _metatags: Record<string, string>
onDestroy(state.userRelatedState.preferencesAsTags .addCallbackAndRun(tags => {
_metatags = tags;
}));
</script>
<div>
@ -40,7 +45,7 @@
<div class="flex flex-col">
{#each layer.tagRenderings as config (config.id)}
{#if config.condition === undefined || config.condition.matchesProperties(_tags)}
{#if (config.condition === undefined || config.condition.matchesProperties(_tags)) && (config.metacondition === undefined || config.metacondition.matchesProperties(_metatags))}
{#if config.IsKnown(_tags)}
<TagRenderingEditable {tags} {config} {state} {selectedElement} {layer} {highlightedRendering}></TagRenderingEditable>
{/if}

View file

@ -91,6 +91,7 @@ export class MapLibreAdaptor implements MapProperties {
// Workaround, 'ShowPointLayer' sets this flag
return
}
console.log(e)
const lon = e.lngLat.lng
const lat = e.lngLat.lat
lastClickLocation.setData({ lon, lat })

View file

@ -96,14 +96,15 @@
}
});
state.newFeatures.features.ping();
const tagsStore = state.featureProperties.getStore(newId);
{
// Set some metainfo
const tagsStore = state.featureProperties.getStore(newId);
const properties = tagsStore.data;
if (snapTo) {
// metatags (starting with underscore) are not uploaded, so we can safely mark this
properties["_referencing_ways"] = `["${snapTo}"]`;
}
properties["_backend"] = state.osmConnection.Backend()
properties["_last_edit:timestamp"] = new Date().toISOString();
const userdetails = state.osmConnection.userDetails.data;
properties["_last_edit:contributor"] = userdetails.name;
@ -112,8 +113,9 @@
}
const feature = state.indexedFeatures.featuresById.data.get(newId);
abort();
state.selectedElement.setData(feature);
state.selectedLayer.setData(selectedPreset.layer);
state.selectedElement.setData(feature);
tagsStore.ping()
}

View file

@ -1,46 +1,63 @@
<script lang="ts">
import ToSvelte from "../Base/ToSvelte.svelte"
import Table from "../Base/Table"
import { UIEventSource } from "../../Logic/UIEventSource"
import ToSvelte from "../Base/ToSvelte.svelte";
import Table from "../Base/Table";
import { UIEventSource } from "../../Logic/UIEventSource";
import SimpleMetaTaggers from "../../Logic/SimpleMetaTagger";
import { FixedUiElement } from "../Base/FixedUiElement";
import { onDestroy } from "svelte";
import Toggle, { ClickableToggle } from "../Input/Toggle";
import Lazy from "../Base/Lazy";
import BaseUIElement from "../BaseUIElement";
//Svelte props
export let tags: UIEventSource<any>
export let state: any
export let tags: UIEventSource<any>;
export let state: any;
const calculatedTags = [].concat(
// SimpleMetaTagger.lazyTags,
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? [])
)
);
const allTags = tags.map((tags) => {
const parts = []
const parts: (string | BaseUIElement)[][] = [];
for (const key in tags) {
if (!tags.hasOwnProperty(key)) {
continue
}
let v = tags[key]
let v = tags[key];
if (v === "") {
v = "<b>empty string</b>"
v = "<b>empty string</b>";
}
parts.push([key, v ?? "<b>undefined</b>"])
parts.push([key, v ?? "<b>undefined</b>"]);
}
for (const key of calculatedTags) {
const value = tags[key]
const value = tags[key];
if (value === undefined) {
continue
continue;
}
let type = ""
let type = "";
if (typeof value !== "string") {
type = " <i>" + typeof value + "</i>"
type = " <i>" + typeof value + "</i>";
}
parts.push(["<i>" + key + "</i>", value])
parts.push(["<i>" + key + "</i>", value]);
}
return parts
})
for (const metatag of SimpleMetaTaggers.metatags.filter(mt => mt.isLazy)) {
const title = "<i>" + metatag.keys.join(";") + "</i> (lazy)";
const toggleState = new UIEventSource(false)
const toggle: BaseUIElement = new Toggle(
new Lazy(() => new FixedUiElement(metatag.keys.map(key => tags[key]).join(";"))),
new FixedUiElement("Evaluate").onClick(() => toggleState.setData(true)) ,
toggleState
);
parts.push([title, toggle]);
}
const tagsTable = new Table(["Key", "Value"], $allTags).SetClass("zebra-table")
return parts;
});
let _allTags = [];
onDestroy(allTags.addCallbackAndRunD(allTags => {
_allTags = allTags;
}));
const tagsTable = new Table(["Key", "Value"], _allTags).SetClass("zebra-table");
</script>
<section>

View file

@ -99,63 +99,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
})
),
]
allRenderings.push(
new Toggle(
new Lazy(() =>
FeatureInfoBox.createEditElements(questionBoxes, layerConfig, tags, state)
),
undefined,
state.featureSwitchUserbadge
)
)
return new Combine(allRenderings).SetClass("block")
}
/**
* All the edit elements, together (note that the question boxes are passed though)
* @param questionBoxes
* @param layerConfig
* @param tags
* @param state
* @private
*/
private static createEditElements(
questionBoxes: Map<string, QuestionBox>,
layerConfig: LayerConfig,
tags: UIEventSource<any>,
state: FeaturePipelineState
): BaseUIElement {
let editElements: BaseUIElement[] = []
questionBoxes.forEach((questionBox) => {
editElements.push(questionBox)
})
editElements.push(
new VariableUiElement(
state.osmConnection.userDetails
.map((ud) => ud.csCount)
.map(
(csCount) => {
if (
csCount <= Constants.userJourney.historyLinkVisible &&
state.featureSwitchIsDebugging.data == false &&
state.featureSwitchIsTesting.data === false
) {
return undefined
}
return new TagRenderingAnswer(
tags,
SharedTagRenderings.SharedTagRendering.get("last_edit"),
state
)
},
[state.featureSwitchIsDebugging, state.featureSwitchIsTesting]
)
)
)
return new Combine(editElements).SetClass("flex flex-col")
}
}

View file

@ -41,7 +41,10 @@
return true;
}
const baseQuestions = (layer.tagRenderings ?? [])?.filter(tr => allowed(tr.labels) && tr.question !== undefined);
let baseQuestions = []
$: {
baseQuestions = (layer.tagRenderings ?? [])?.filter(tr => allowed(tr.labels) && tr.question !== undefined);
}
let skippedQuestions = new UIEventSource<Set<string>>(new Set<string>());
let questionsToAsk = tags.map(tags => {
@ -80,6 +83,7 @@
skipped++;
}
}
$: console.log("Current questionbox state:", {answered, skipped, questionsToAsk, layer, selectedElement, tags})
</script>
{#if _questionsToAsk.length === 0}

View file

@ -32,7 +32,6 @@
checkedMappings = [...config.mappings.map(_ => false), false /*One element extra in case a freeform value is added*/];
}
}
$: console.log("Checked mappings:", checkedMappings)
let selectedTags: TagsFilter = undefined;
function mappingIsHidden(mapping: Mapping): boolean {

View file

@ -63,6 +63,7 @@ export interface SpecialVisualizationState {
readonly userRelatedState: {
readonly mangroveIdentity: MangroveIdentity
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
readonly preferencesAsTags: Store<Record<string, string>>
}
readonly lastClickObject: WritableFeatureSource
}

View file

@ -1265,21 +1265,24 @@ export default class SpecialVisualizations {
doc: "The URL to link to",
required: true,
},
{
name: "class",
doc: "CSS-classes to add to the element",
},
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[]
): BaseUIElement {
const [text, href] = args
const [text, href, classnames] = args
return new VariableUiElement(
tagSource.map(
(tags) =>
new Link(
Utils.SubstituteKeys(text, tags),
Utils.SubstituteKeys(href, tags),
true
)
tagSource.map((tags) =>
new Link(
Utils.SubstituteKeys(text, tags),
Utils.SubstituteKeys(href, tags),
true
).SetClass(classnames)
)
)
},

View file

@ -29,7 +29,14 @@ export class Translation extends BaseUIElement {
}
count++
if (typeof translations[translationsKey] != "string") {
console.error("Non-string object in translation: ", translations[translationsKey])
console.error(
"Non-string object at",
context,
"in translation: ",
translations[translationsKey],
"\n current translations are: ",
translations
)
throw (
"Error in an object depicting a translation: a non-string object was found. (" +
context +

View file

@ -307,13 +307,21 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* @param init
* @constructor
*/
public static AddLazyProperty(object: any, name: string, init: () => any) {
public static AddLazyProperty(
object: any,
name: string,
init: () => any,
whenDone?: () => void
) {
Object.defineProperty(object, name, {
enumerable: false,
configurable: true,
get: () => {
delete object[name]
object[name] = init()
if (whenDone) {
whenDone()
}
return object[name]
},
})
@ -332,6 +340,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
enumerable: false,
configurable: true,
get: () => {
console.trace("Property", name, "got requested")
init().then((r) => {
delete object[name]
object[name] = r

View file

@ -19,4 +19,4 @@
"https://commons.wikimedia.org/wiki/File:ISO_7010_P018.svg"
]
}
]
]

View file

@ -55,7 +55,16 @@
"*": "{open_note()}"
}
},
"all_tags"
{
"metacondition": {
"or": [
"__featureSwitchDebugging=true"
]
},
"render": {
"*": "{all_tags()}"
}
}
],
"mapRendering": [
{

View file

@ -1365,10 +1365,26 @@
]
},
"last_edit": {
"#": "Gives some metainfo about the last edit and who did edit it - rendering only",
"description": "Gives some metainfo about the last edit and who did edit it - rendering only",
"condition": "_last_edit:contributor~*",
"metacondition": {
"or": [
"__featureSwitchIsTesting=true",
"__featureSwitchIsDebugging=true",
"mapcomplete-show_debug=yes",
"_csCount>=10"
]
},
"render": {
"*": "<div class='subtle' style='font-size: small; margin-top: 2em; margin-bottom: 0.5em;'><a href='https://www.openStreetMap.org/changeset/{_last_edit:changeset}' target='_blank'>Last edited on {_last_edit:timestamp}</a> by <a href='https://www.openStreetMap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a></div>"
"special": {
"type": "link",
"href": "{_backend}/changeset/{_last_edit:changeset}",
"text": {
"en": "Last edited on {_last_edit:timestamp} by {_last_edit:contributor}",
"nl": "Laatst gewijzigd op {_last_edit:timestamp} door {_last_edit:contributor}"
},
"class": "subtle font-small"
}
}
},
"all_tags": {

View file

@ -3560,6 +3560,9 @@
"19": {
"then": "Aquí es poden reciclar sabates"
},
"20": {
"then": "Aquí es poden reciclar petits aparells elèctrics"
},
"21": {
"then": "Aquí es poden reciclar petits aparells elèctrics"
},
@ -4554,4 +4557,4 @@
}
}
}
}
}

View file

@ -6927,13 +6927,13 @@
"16": {
"question": "Recycling von Kunststoffen"
},
"18": {
"17": {
"question": "Recycling von Metallschrott"
},
"19": {
"18": {
"question": "Recycling von Elektrokleingeräten"
},
"20": {
"19": {
"question": "Recycling von Restabfällen"
},
"20": {

View file

@ -6946,15 +6946,12 @@
"question": "Recycling of plastic"
},
"17": {
"question": "Recycling of printer cartridges"
},
"18": {
"question": "Recycling of scrap metal"
},
"19": {
"18": {
"question": "Recycling of small electrical appliances"
},
"20": {
"19": {
"question": "Recycling of residual waste"
},
"20": {
@ -9182,4 +9179,4 @@
}
}
}
}
}

View file

@ -3448,7 +3448,7 @@
"16": {
"question": "Reciclaje de plástico"
},
"18": {
"17": {
"question": "Reciclaje de chatarra"
},
"18": {

View file

@ -1799,13 +1799,13 @@
"16": {
"question": "Riciclo di plastica"
},
"18": {
"17": {
"question": "Riciclo di rottami metallici"
},
"19": {
"18": {
"question": "Riciclo di piccoli elettrodomestici"
},
"20": {
"19": {
"question": "Riciclo di secco"
},
"20": {

View file

@ -6512,14 +6512,14 @@
"question": "Recycling van plastic"
},
"17": {
"question": "Recycling van printer cartridges"
},
"18": {
"question": "Recycling van oud metaal"
},
"19": {
"18": {
"question": "Recycling van kleine elektrische apparaten"
},
"19": {
"question": "Recycling van restafval"
},
"20": {
"question": "Recycling van restafval"
}
@ -8652,4 +8652,4 @@
}
}
}
}
}

View file

@ -131,6 +131,13 @@
"question": "What is the network name for the wireless internet access?",
"render": "The network name is <b>{internet_access:ssid}</b>"
},
"last_edit": {
"render": {
"special": {
"text": "Last edited on {_last_edit:timestamp} by {_last_edit:contributor}"
}
}
},
"level": {
"mappings": {
"0": {

View file

@ -131,6 +131,13 @@
"question": "Wat is de netwerknaam voor de draadloze internettoegang?",
"render": "De netwerknaam is <b>{internet_access:ssid}</b>"
},
"last_edit": {
"render": {
"special": {
"text": "Laatst gewijzigd op {_last_edit:timestamp} door {_last_edit:contributor} "
}
}
},
"level": {
"mappings": {
"0": {

View file

@ -16,7 +16,7 @@ import { Translation } from "../UI/i18n/Translation"
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
import questions from "../assets/tagRenderings/questions.json"
import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson"
import { PrepareLayer } from "../Models/ThemeConfig/Conversion/PrepareLayer"
import { PrepareLayer, RewriteSpecial } from "../Models/ThemeConfig/Conversion/PrepareLayer"
import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme"
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
import { Utils } from "../Utils"
@ -156,6 +156,7 @@ class LayerOverviewUtils extends Script {
getSharedTagRenderings(doesImageExist: DoesImageExist): Map<string, TagRenderingConfigJson> {
const dict = new Map<string, TagRenderingConfigJson>()
const prep = new RewriteSpecial()
const validator = new ValidateTagRenderings(undefined, doesImageExist)
for (const key in questions) {
if (key === "id") {
@ -163,7 +164,12 @@ class LayerOverviewUtils extends Script {
}
questions[key].id = key
questions[key]["source"] = "shared-questions"
const config = <TagRenderingConfigJson>questions[key]
const config = prep.convertStrict(
<TagRenderingConfigJson>questions[key],
"questions.json:" + key
)
delete config.description
delete config["#"]
validator.convertStrict(
config,
"generate-layer-overview:tagRenderings/questions.json:" + key

View file

@ -883,7 +883,10 @@ describe("ReplaceGeometryAction", () => {
const data = await Utils.downloadJson(url)
const fullNodeDatabase = undefined // TODO new FullNodeDatabaseSource(undefined)
// TODO fullNodeDatabase.handleOsmJson(data, 0)
const changes = new Changes()
const changes = new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection()
})
const osmConnection = new OsmConnection({
dryRun: new ImmutableStore(true),
})