forked from MapComplete/MapComplete
Import helper: Improve error messages of non-matching presets; fix bug if a value is 'null' in source geojson
This commit is contained in:
parent
74f00b333b
commit
b119e1ac1d
13 changed files with 131 additions and 53 deletions
|
@ -148,12 +148,6 @@ export class And extends TagsFilter {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
AsJson() {
|
|
||||||
return {
|
|
||||||
and: this.and.map(a => a.AsJson())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
optimize(): TagsFilter | boolean {
|
optimize(): TagsFilter | boolean {
|
||||||
if(this.and.length === 0){
|
if(this.and.length === 0){
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -85,12 +85,6 @@ export class Or extends TagsFilter {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
AsJson() {
|
|
||||||
return {
|
|
||||||
or: this.or.map(o => o.AsJson())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
optimize(): TagsFilter | boolean {
|
optimize(): TagsFilter | boolean {
|
||||||
|
|
||||||
if(this.or.length === 0){
|
if(this.or.length === 0){
|
||||||
|
|
|
@ -167,6 +167,34 @@ export class TagUtils {
|
||||||
return new Tag(tag[0], tag[1]);
|
return new Tag(tag[0], tag[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns wether or not a keys is (probably) a valid key.
|
||||||
|
*
|
||||||
|
* // should accept common keys
|
||||||
|
* TagUtils.isValidKey("name") // => true
|
||||||
|
* TagUtils.isValidKey("image:0") // => true
|
||||||
|
* TagUtils.isValidKey("alt_name") // => true
|
||||||
|
*
|
||||||
|
* // should refuse short keys
|
||||||
|
* TagUtils.isValidKey("x") // => false
|
||||||
|
* TagUtils.isValidKey("xy") // => false
|
||||||
|
*
|
||||||
|
* // should refuse a string with >255 characters
|
||||||
|
* let a255 = ""
|
||||||
|
* for(let i = 0; i < 255; i++) { a255 += "a"; }
|
||||||
|
* a255.length // => 255
|
||||||
|
* TagUtils.isValidKey(a255) // => true
|
||||||
|
* TagUtils.isValidKey("a"+a255) // => false
|
||||||
|
*
|
||||||
|
* // Should refuse unexpected characters
|
||||||
|
* TagUtils.isValidKey("with space") // => false
|
||||||
|
* TagUtils.isValidKey("some$type") // => false
|
||||||
|
* TagUtils.isValidKey("_name") // => false
|
||||||
|
*/
|
||||||
|
public static isValidKey(key: string): boolean {
|
||||||
|
return key.match(/^[a-z][a-z0-9:_]{2,253}[a-z0-9]$/) !== null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a tag configuration (a json) into a TagsFilter
|
* Parses a tag configuration (a json) into a TagsFilter
|
||||||
*
|
*
|
||||||
|
|
|
@ -26,8 +26,6 @@ export abstract class TagsFilter {
|
||||||
*/
|
*/
|
||||||
abstract asChange(properties: any): { k: string, v: string }[]
|
abstract asChange(properties: any): { k: string, v: string }[]
|
||||||
|
|
||||||
abstract AsJson() ;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an optimized version (or self) of this tagsFilter
|
* Returns an optimized version (or self) of this tagsFilter
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -91,8 +91,10 @@ export class QueryParameters {
|
||||||
|
|
||||||
parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data))
|
parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data))
|
||||||
}
|
}
|
||||||
// Don't pollute the history every time a parameter changes
|
if(!Utils.runningFromConsole){
|
||||||
history.replaceState(null, "", "?" + parts.join("&") + Hash.Current());
|
// Don't pollute the history every time a parameter changes
|
||||||
|
history.replaceState(null, "", "?" + parts.join("&") + Hash.Current());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -93,7 +93,6 @@ export class AskMetadata extends Combine implements FlowStep<{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ([ obj.features, obj.intro, obj.wikilink, obj.source].some(v => v === undefined)){
|
if ([ obj.features, obj.intro, obj.wikilink, obj.source].some(v => v === undefined)){
|
||||||
console.log("Obj is", obj)
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,10 +30,14 @@ export class CreateNotes extends Combine {
|
||||||
|
|
||||||
const tags: string [] = []
|
const tags: string [] = []
|
||||||
for (const key in f.properties) {
|
for (const key in f.properties) {
|
||||||
|
if (f.properties[key] === null || f.properties[key] === undefined) {
|
||||||
|
console.warn("Null or undefined key for ", f.properties)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (f.properties[key] === "") {
|
if (f.properties[key] === "") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tags.push(key + "=" + f.properties[key].replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n"))
|
tags.push(key + "=" + (f.properties[key]+"").replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n"))
|
||||||
}
|
}
|
||||||
const lat = f.geometry.coordinates[1]
|
const lat = f.geometry.coordinates[1]
|
||||||
const lon = f.geometry.coordinates[0]
|
const lon = f.geometry.coordinates[0]
|
||||||
|
|
|
@ -10,6 +10,8 @@ import FileSelectorButton from "../Input/FileSelectorButton";
|
||||||
import {FlowStep} from "./FlowStep";
|
import {FlowStep} from "./FlowStep";
|
||||||
import {parse} from "papaparse";
|
import {parse} from "papaparse";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
import {del} from "idb-keyval";
|
||||||
|
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||||
|
|
||||||
class FileSelector extends InputElementMap<FileList, { name: string, contents: Promise<string> }> {
|
class FileSelector extends InputElementMap<FileList, { name: string, contents: Promise<string> }> {
|
||||||
constructor(label: BaseUIElement) {
|
constructor(label: BaseUIElement) {
|
||||||
|
@ -72,6 +74,17 @@ export class RequestFile extends Combine implements FlowStep<{features: any[]}>
|
||||||
if (parsed.features.some(f => f.geometry.type != "Point")) {
|
if (parsed.features.some(f => f.geometry.type != "Point")) {
|
||||||
return {error: t.errPointsOnly}
|
return {error: t.errPointsOnly}
|
||||||
}
|
}
|
||||||
|
parsed.features.forEach(f => {
|
||||||
|
const props = f.properties
|
||||||
|
for (const key in props) {
|
||||||
|
if(props[key] === undefined || props[key] === null || props[key] === ""){
|
||||||
|
delete props[key]
|
||||||
|
}
|
||||||
|
if(!TagUtils.isValidKey(key)){
|
||||||
|
return {error: "Probably an invalid key: "+key}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
return parsed;
|
return parsed;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -13,6 +13,9 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import Toggleable from "../Base/Toggleable";
|
import Toggleable from "../Base/Toggleable";
|
||||||
import {BBox} from "../../Logic/BBox";
|
import {BBox} from "../../Logic/BBox";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import PresetConfig from "../../Models/ThemeConfig/PresetConfig";
|
||||||
|
import List from "../Base/List";
|
||||||
|
|
||||||
export default class SelectTheme extends Combine implements FlowStep<{
|
export default class SelectTheme extends Combine implements FlowStep<{
|
||||||
features: any[],
|
features: any[],
|
||||||
|
@ -29,7 +32,7 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
||||||
}>;
|
}>;
|
||||||
public readonly IsValid: UIEventSource<boolean>;
|
public readonly IsValid: UIEventSource<boolean>;
|
||||||
|
|
||||||
constructor(params: ({ features: any[], layer: LayerConfig, bbox: BBox, })) {
|
constructor(params: ({ features: any[], layer: LayerConfig, bbox: BBox, })) {
|
||||||
|
|
||||||
let options: InputElement<string>[] = AllKnownLayouts.layoutsList
|
let options: InputElement<string>[] = AllKnownLayouts.layoutsList
|
||||||
.filter(th => th.layers.some(l => l.id === params.layer.id))
|
.filter(th => th.layers.some(l => l.id === params.layer.id))
|
||||||
|
@ -37,7 +40,7 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
||||||
.map(th => new FixedInputElement<string>(
|
.map(th => new FixedInputElement<string>(
|
||||||
new Combine([
|
new Combine([
|
||||||
new Img(th.icon).SetClass("block h-12 w-12 br-4"),
|
new Img(th.icon).SetClass("block h-12 w-12 br-4"),
|
||||||
new Title( th.title)
|
new Title(th.title)
|
||||||
]).SetClass("flex items-center"),
|
]).SetClass("flex items-center"),
|
||||||
th.id))
|
th.id))
|
||||||
|
|
||||||
|
@ -47,9 +50,8 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const applicablePresets = themeRadios.GetValue().map(theme => {
|
const applicablePresets = themeRadios.GetValue().map(theme => {
|
||||||
if(theme === undefined){
|
if (theme === undefined) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
// we get the layer with the correct ID via the actual theme config, as the actual theme might have different presets due to overrides
|
// we get the layer with the correct ID via the actual theme config, as the actual theme might have different presets due to overrides
|
||||||
|
@ -60,7 +62,7 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
||||||
|
|
||||||
|
|
||||||
const nonMatchedElements = applicablePresets.map(presets => {
|
const nonMatchedElements = applicablePresets.map(presets => {
|
||||||
if(presets === undefined || presets.length === 0){
|
if (presets === undefined || presets.length === 0) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return params.features.filter(feat => !presets.some(preset => new And(preset.tags).matchesProperties(feat.properties)))
|
return params.features.filter(feat => !presets.some(preset => new And(preset.tags).matchesProperties(feat.properties)))
|
||||||
|
@ -71,27 +73,15 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
||||||
"All of the following themes will show the import notes. However, the note on OpenStreetMap can link to only one single theme. Choose which theme that the created notes will link to",
|
"All of the following themes will show the import notes. However, the note on OpenStreetMap can link to only one single theme. Choose which theme that the created notes will link to",
|
||||||
themeRadios,
|
themeRadios,
|
||||||
new VariableUiElement(applicablePresets.map(applicablePresets => {
|
new VariableUiElement(applicablePresets.map(applicablePresets => {
|
||||||
if(themeRadios.GetValue().data === undefined){
|
if (themeRadios.GetValue().data === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
if(applicablePresets === undefined || applicablePresets.length === 0){
|
if (applicablePresets === undefined || applicablePresets.length === 0) {
|
||||||
return new FixedUiElement("This theme has no presets loaded. As a result, imports won't work here").SetClass("alert")
|
return new FixedUiElement("This theme has no presets loaded. As a result, imports won't work here").SetClass("alert")
|
||||||
}
|
}
|
||||||
},[themeRadios.GetValue()])),
|
}, [themeRadios.GetValue()])),
|
||||||
new VariableUiElement(nonMatchedElements.map(unmatched => {
|
|
||||||
if(unmatched === undefined || unmatched.length === 0){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return new Combine([new FixedUiElement(unmatched.length+" objects dont match any presets").SetClass("alert"),
|
|
||||||
...applicablePresets.data.map(preset => preset.title.txt +" needs tags "+ preset.tags.map(t => t.asHumanString()).join(" & ")),
|
|
||||||
,
|
|
||||||
new Toggleable( new Title( "The following elements don't match any of the presets"),
|
|
||||||
new Combine( unmatched.map(feat => JSON.stringify(feat.properties))).SetClass("flex flex-col")
|
|
||||||
)
|
|
||||||
|
|
||||||
]) .SetClass("flex flex-col")
|
new VariableUiElement(nonMatchedElements.map(unmatched => SelectTheme.nonMatchedElementsPanel(unmatched, applicablePresets.data), [applicablePresets]))
|
||||||
|
|
||||||
}))
|
|
||||||
]);
|
]);
|
||||||
this.SetClass("flex flex-col")
|
this.SetClass("flex flex-col")
|
||||||
|
|
||||||
|
@ -106,13 +96,13 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
||||||
if (obj === undefined) {
|
if (obj === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ([obj.theme, obj.features].some(v => v === undefined)){
|
if ([obj.theme, obj.features].some(v => v === undefined)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(applicablePresets.data === undefined || applicablePresets.data.length === 0){
|
if (applicablePresets.data === undefined || applicablePresets.data.length === 0) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if((nonMatchedElements.data?.length??0) > 0){
|
if ((nonMatchedElements.data?.length ?? 0) > 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,5 +111,60 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
||||||
}, [applicablePresets])
|
}, [applicablePresets])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static nonMatchedElementsPanel(unmatched: any[], applicablePresets: PresetConfig[]): BaseUIElement {
|
||||||
|
if (unmatched === undefined || unmatched.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const applicablePresetsOverview = applicablePresets.map(preset => new Combine([
|
||||||
|
preset.title.txt, "needs tags",
|
||||||
|
new FixedUiElement(preset.tags.map(t => t.asHumanString()).join(" & ")).SetClass("thanks")
|
||||||
|
]))
|
||||||
|
|
||||||
|
const unmatchedPanels: BaseUIElement[] = []
|
||||||
|
for (const feat of unmatched) {
|
||||||
|
const parts: BaseUIElement[] = []
|
||||||
|
parts.push(new Combine(Object.keys(feat.properties).map(k =>
|
||||||
|
k+"="+feat.properties[k]
|
||||||
|
)).SetClass("flex flex-col"))
|
||||||
|
|
||||||
|
for (const preset of applicablePresets) {
|
||||||
|
const tags = new And(preset.tags).asChange({})
|
||||||
|
const missing = []
|
||||||
|
for (const {k, v} of tags) {
|
||||||
|
if (preset[k] === undefined) {
|
||||||
|
missing.push(
|
||||||
|
`Expected ${k}=${v}, but it is completely missing`
|
||||||
|
)
|
||||||
|
} else if (feat.properties[k] !== v) {
|
||||||
|
missing.push(
|
||||||
|
`Property with key ${k} does not have expected value ${v}; instead it is ${feat.properties}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
parts.push(
|
||||||
|
new Combine([
|
||||||
|
new FixedUiElement(`Preset ${preset.title.txt} is not applicable:`),
|
||||||
|
new List(missing)
|
||||||
|
]).SetClass("flex flex-col alert")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
unmatchedPanels.push(new Combine(parts).SetClass("flex flex-col"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Combine([
|
||||||
|
new FixedUiElement(unmatched.length + " objects dont match any presets").SetClass("alert"),
|
||||||
|
...applicablePresetsOverview,
|
||||||
|
new Toggleable(new Title("The following elements don't match any of the presets"),
|
||||||
|
new Combine(unmatchedPanels))
|
||||||
|
]).SetClass("flex flex-col")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -26,7 +26,7 @@
|
||||||
"@types/wikidata-sdk": "^6.1.0",
|
"@types/wikidata-sdk": "^6.1.0",
|
||||||
"@types/xml2js": "^0.4.9",
|
"@types/xml2js": "^0.4.9",
|
||||||
"country-language": "^0.1.7",
|
"country-language": "^0.1.7",
|
||||||
"doctest-ts-improved": "^0.8.4",
|
"doctest-ts-improved": "^0.8.5",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"geojson2svg": "^1.3.1",
|
"geojson2svg": "^1.3.1",
|
||||||
|
@ -6024,9 +6024,9 @@
|
||||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
||||||
},
|
},
|
||||||
"node_modules/doctest-ts-improved": {
|
"node_modules/doctest-ts-improved": {
|
||||||
"version": "0.8.4",
|
"version": "0.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/doctest-ts-improved/-/doctest-ts-improved-0.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/doctest-ts-improved/-/doctest-ts-improved-0.8.5.tgz",
|
||||||
"integrity": "sha512-CMVSnyDB00sLkTqHIZ20Z/kHD2XczNHwWkD4UC4retGaSfuP8XG4cnAGwkr8qoQq3mjc3w8O4w3OTWrC4HC2vA==",
|
"integrity": "sha512-4zU8fQV263CU3jAi+K7xohhT9b2ZDGw20M4O7AgzW1IoKklmNkSlHMoKZX6gqN1DAouo08R+MD5aSgACG5ILRw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^4.3.0",
|
"@types/chai": "^4.3.0",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
|
@ -21413,9 +21413,9 @@
|
||||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
|
||||||
},
|
},
|
||||||
"doctest-ts-improved": {
|
"doctest-ts-improved": {
|
||||||
"version": "0.8.4",
|
"version": "0.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/doctest-ts-improved/-/doctest-ts-improved-0.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/doctest-ts-improved/-/doctest-ts-improved-0.8.5.tgz",
|
||||||
"integrity": "sha512-CMVSnyDB00sLkTqHIZ20Z/kHD2XczNHwWkD4UC4retGaSfuP8XG4cnAGwkr8qoQq3mjc3w8O4w3OTWrC4HC2vA==",
|
"integrity": "sha512-4zU8fQV263CU3jAi+K7xohhT9b2ZDGw20M4O7AgzW1IoKklmNkSlHMoKZX6gqN1DAouo08R+MD5aSgACG5ILRw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/chai": "^4.3.0",
|
"@types/chai": "^4.3.0",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
"@types/wikidata-sdk": "^6.1.0",
|
"@types/wikidata-sdk": "^6.1.0",
|
||||||
"@types/xml2js": "^0.4.9",
|
"@types/xml2js": "^0.4.9",
|
||||||
"country-language": "^0.1.7",
|
"country-language": "^0.1.7",
|
||||||
"doctest-ts-improved": "^0.8.4",
|
"doctest-ts-improved": "^0.8.5",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"geojson2svg": "^1.3.1",
|
"geojson2svg": "^1.3.1",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {Utils} from "../Utils";
|
||||||
|
|
||||||
describe("TestSuite", () => {
|
describe("TestSuite", () => {
|
||||||
|
|
||||||
describe("function onder test", () => {
|
describe("function under test", () => {
|
||||||
it("should work", () => {
|
it("should work", () => {
|
||||||
expect("abc").eq("abc")
|
expect("abc").eq("abc")
|
||||||
})
|
})
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue