diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts index 2c37b39203..307b3e94af 100644 --- a/Customizations/JSON/FromJSON.ts +++ b/Customizations/JSON/FromJSON.ts @@ -64,7 +64,6 @@ export class FromJSON { } public static LayoutFromJSON(json: LayoutConfigJson): Layout { - console.log(json) const tr = FromJSON.Translation; const layers = json.layers.map(FromJSON.Layer); @@ -105,6 +104,10 @@ export class FromJSON { if (typeof (json) === "string") { return new Translation({"*": json}); } + if(json.render !== undefined){ + console.error("Using a 'render' where a translation is expected. Content is", json.render); + throw "ERROR: using a 'render' where none is expected" + } const tr = {}; let keyCount = 0; for (let key in json) { @@ -115,7 +118,6 @@ export class FromJSON { return undefined; } const transl = new Translation(tr); - transl.addCallback(latest => console.log("tr callback changed to", latest)); return transl; } diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index ba994b9f6f..c5d37eb5de 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -42,7 +42,7 @@ export class FilteredLayer { private _showOnPopup: (tags: UIEventSource, feature: any) => UIElement; - private static readonly grid = codegrid.CodeGrid(); + private static readonly grid = codegrid.CodeGrid("./tiles/"); constructor( layerDef: LayerDefinition, @@ -52,12 +52,20 @@ export class FilteredLayer { this._wayHandling = layerDef.wayHandling; this._showOnPopup = showOnPopup; - this._style = layerDef.style; - if (this._style === undefined) { - this._style = function () { + this._style = (tags) => { + if(layerDef.style === undefined){ return {icon: {iconUrl: "./assets/bug.svg"}, color: "#000"}; } - } + + const obj = layerDef.style(tags); + if(obj.weight && typeof (obj.weight) === "string"){ + obj.weight = Number(obj.weight);// Weight MUST be a number, otherwise leaflet does weird things. see https://github.com/Leaflet/Leaflet/issues/6075 + if(isNaN(obj.weight)){ + obj.weight = undefined; + } + } + return obj; + }; this.name = name; this.filters = layerDef.overpassFilter; this._maxAllowedOverlap = layerDef.maxAllowedOverlapPercentage; @@ -102,14 +110,17 @@ export class FilteredLayer { if (this.filters.matches(tags)) { const centerPoint = GeoOperations.centerpoint(feature); - feature.properties["_surface"] = ""+GeoOperations.surfaceAreaInSqMeters(feature); - const lat = ""+centerPoint.geometry.coordinates[1]; - const lon = ""+centerPoint.geometry.coordinates[0] - feature.properties["_lon"] = lat; - feature.properties["_lat"] = lon; + feature.properties["_surface"] = "" + GeoOperations.surfaceAreaInSqMeters(feature); + const lat = centerPoint.geometry.coordinates[1]; + const lon = centerPoint.geometry.coordinates[0] + feature.properties["_lon"] = "" + lat; // We expect a string here for lat/lon + feature.properties["_lat"] = "" + lon; + // But the codegrid SHOULD be a number! FilteredLayer.grid.getCode(lat, lon, (error, code) => { if (error === null) { feature.properties["_country"] = code; + } else { + console.warn("Could not determine country for", feature.properties.id, error); } }) @@ -215,26 +226,26 @@ export class FilteredLayer { pointToLayer: function (feature, latLng) { const style = self._style(feature.properties); let marker; - if (style.icon === undefined) { - marker = L.circle(latLng, { - radius: 25, - color: style.color - }); + if (style.icon === undefined) { + marker = L.circle(latLng, { + radius: 25, + color: style.color + }); - } else if (style.icon.iconUrl.startsWith("$circle")) { - marker = L.circle(latLng, { - radius: 25, - color: style.color - }); - } else { - if (style.icon.iconSize === undefined) { - style.icon.iconSize = [50, 50] - } + } else if (style.icon.iconUrl.startsWith("$circle")) { + marker = L.circle(latLng, { + radius: 25, + color: style.color + }); + } else { + if (style.icon.iconSize === undefined) { + style.icon.iconSize = [50, 50] + } - marker = L.marker(latLng, { - icon: new L.icon(style.icon), - }); - } + marker = L.marker(latLng, { + icon: new L.icon(style.icon), + }); + } let eventSource = State.state.allElements.addOrGetElement(feature); const popup = L.popup({}, marker); let uiElement: UIElement; diff --git a/Logic/Tags.ts b/Logic/Tags.ts index 307ea06428..b7d54cbb56 100644 --- a/Logic/Tags.ts +++ b/Logic/Tags.ts @@ -95,10 +95,10 @@ export class Tag extends TagsFilter { this.key = key this.value = value if(key === undefined || key === ""){ - throw "Invalid key"; + throw "Invalid key: undefined or empty"; } if(value === undefined){ - throw "Invalid value"; + throw "Invalid value: value is undefined"; } if(value === "*"){ console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`) diff --git a/State.ts b/State.ts index 95f564d5c5..3620df2b78 100644 --- a/State.ts +++ b/State.ts @@ -22,7 +22,7 @@ export class State { // The singleton of the global state public static state: State; - public static vNumber = "0.0.8e"; + public static vNumber = "0.0.8g"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/UI/Input/CombinedInputElement.ts b/UI/Input/CombinedInputElement.ts new file mode 100644 index 0000000000..b15c0a5160 --- /dev/null +++ b/UI/Input/CombinedInputElement.ts @@ -0,0 +1,35 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import {UIElement} from "../UIElement"; + +export default class CombinedInputElement extends InputElement { + private readonly _a: InputElement; + private readonly _b: UIElement; + private readonly _combined: UIElement; + public readonly IsSelected: UIEventSource; + + constructor(a: InputElement, b: InputElement) { + super(); + this._a = a; + this._b = b; + this.IsSelected = this._a.IsSelected.map((isSelected) => { + return isSelected || b.IsSelected.data + }, [b.IsSelected]) + this._combined = new Combine([this._a, this._b]); + } + + GetValue(): UIEventSource { + return this._a.GetValue(); + } + + InnerRender(): string { + return this._combined.Render(); + } + + + IsValid(t: T): boolean { + return this._a.IsValid(t); + } + +} \ No newline at end of file diff --git a/UI/Input/MultiLingualTextFields.ts b/UI/Input/MultiLingualTextFields.ts index 5e83ff5216..d1f69fbf1a 100644 --- a/UI/Input/MultiLingualTextFields.ts +++ b/UI/Input/MultiLingualTextFields.ts @@ -6,13 +6,11 @@ export default class MultiLingualTextFields extends InputElement { private _fields: Map = new Map(); private readonly _value: UIEventSource; public readonly IsSelected: UIEventSource = new UIEventSource(false); - constructor(languages: UIEventSource, textArea: boolean = false, value: UIEventSource>> = undefined) { super(undefined); this._value = value ?? new UIEventSource({}); - this._value.addCallbackAndRun(latestData => { if (typeof (latestData) === "string") { console.warn("Refusing string for multilingual input", latestData); diff --git a/UI/Input/MultiTagInput.ts b/UI/Input/MultiTagInput.ts index 20bd3e282a..30295bc8a2 100644 --- a/UI/Input/MultiTagInput.ts +++ b/UI/Input/MultiTagInput.ts @@ -3,7 +3,6 @@ import TagInput from "./SingleTagInput"; import {MultiInput} from "./MultiInput"; export class MultiTagInput extends MultiInput { - constructor(value: UIEventSource = new UIEventSource([])) { super("Add a new tag", diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index 415760f785..2f46c2e905 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -11,8 +11,7 @@ export class RadioButton extends InputElement { private readonly value: UIEventSource; private readonly _elements: InputElement[] private readonly _selectFirstAsDefault: boolean; - - + constructor(elements: InputElement[], selectFirstAsDefault = true) { super(undefined); @@ -20,7 +19,6 @@ export class RadioButton extends InputElement { this._selectFirstAsDefault = selectFirstAsDefault; const self = this; - this.value = UIEventSource.flatten(this._selectedElementIndex.map( (selectedIndex) => { diff --git a/UI/Input/SimpleDatePicker.ts b/UI/Input/SimpleDatePicker.ts new file mode 100644 index 0000000000..3a6b203be2 --- /dev/null +++ b/UI/Input/SimpleDatePicker.ts @@ -0,0 +1,60 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; + +export default class SimpleDatePicker extends InputElement { + + private readonly value: UIEventSource + + constructor( + value?: UIEventSource + ) { + super(); + this.value = value ?? new UIEventSource(undefined); + const self = this; + this.value.addCallbackAndRun(v => { + if(v === undefined){ + return; + } + self.SetValue(v); + }); + } + + + InnerRender(): string { + return ``; + } + + private SetValue(date: string){ + const field = document.getElementById("date-" + this.id); + if (field === undefined || field === null) { + return; + } + // @ts-ignore + field.value = date; + } + + protected InnerUpdate() { + const field = document.getElementById("date-" + this.id); + if (field === undefined || field === null) { + return; + } + const self = this; + field.oninput = () => { + // Already in YYYY-MM-DD value! + // @ts-ignore + self.value.setData(field.value); + } + + } + + GetValue(): UIEventSource { + return this.value; + } + + IsSelected: UIEventSource = new UIEventSource(false); + + IsValid(t: string): boolean { + return false; + } + +} \ No newline at end of file diff --git a/UI/Input/SingleTagInput.ts b/UI/Input/SingleTagInput.ts index e3a6d5a906..e7525da01f 100644 --- a/UI/Input/SingleTagInput.ts +++ b/UI/Input/SingleTagInput.ts @@ -22,7 +22,6 @@ export default class SingleTagInput extends InputElement { constructor(value: UIEventSource = undefined) { super(undefined); this._value = value ?? new UIEventSource(""); - this.helpMessage = new VariableUiElement(this._value.map(tagDef => { try { FromJSON.Tag(tagDef, ""); diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index a1de27a5e2..4b80ebf5da 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -11,12 +11,14 @@ export class TextField extends InputElement { private readonly _isArea: boolean; private readonly _textAreaRows: number; + private readonly _isValid: (string, country) => boolean; + constructor(options?: { placeholder?: string | UIElement, value?: UIEventSource, textArea?: boolean, textAreaRows?: number, - isValid?: ((s: string) => boolean) + isValid?: ((s: string, country?: string) => boolean) }) { super(undefined); const self = this; @@ -25,9 +27,8 @@ export class TextField extends InputElement { this._isArea = options.textArea ?? false; this.value = options?.value ?? new UIEventSource(undefined); - // @ts-ignore - this._fromString = options.fromString ?? ((str) => (str)) this._textAreaRows = options.textAreaRows; + this._isValid = options.isValid ?? ((str, country) => true); this._placeholder = Translations.W(options.placeholder ?? ""); this.ListenTo(this._placeholder._source); @@ -36,23 +37,20 @@ export class TextField extends InputElement { self.IsSelected.setData(true) }); this.value.addCallback((t) => { - const field = document.getElementById(this.id); + console.log("Setting actual value to", t); + const field = document.getElementById("txt-"+this.id); if (field === undefined || field === null) { return; } - if (options.isValid) { - field.className = options.isValid(t) ? "" : "invalid"; - } + field.className = self.IsValid(t) ? "" : "invalid"; if (t === undefined || t === null) { - // @ts-ignore return; } // @ts-ignore field.value = t; }); this.dumbMode = false; - this.SetClass("deadbeef") } GetValue(): UIEventSource { @@ -72,16 +70,37 @@ export class TextField extends InputElement { ``; } - Update() { - super.Update(); - } - InnerUpdate() { const field = document.getElementById("txt-" + this.id); const self = this; field.oninput = () => { + + // How much characters are on the right, not including spaces? // @ts-ignore - self.value.setData(field.value); + const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length; + // @ts-ignore + let val: string = field.value; + if (!self.IsValid(val)) { + self.value.setData(undefined); + } else { + self.value.setData(val); + } + // Setting the value might cause the value to be set again. We keep the distance _to the end_ stable, as phone number formatting might cause the start to change + // See https://github.com/pietervdvn/MapComplete/issues/103 + // We reread the field value - it might have changed! + + // @ts-ignore + val = field.value; + let newCursorPos = val.length - endDistance; + while(newCursorPos >= 0 && + // We count the number of _actual_ characters (non-space characters) on the right of the new value + // This count should become bigger then the end distance + val.substr(newCursorPos).replace(/ /g, '').length < endDistance + ){ + newCursorPos --; + } + // @ts-ignore + self.SetCursorPosition(newCursorPos); }; if (this.value.data !== undefined && this.value.data !== null) { @@ -103,7 +122,7 @@ export class TextField extends InputElement { } public SetCursorPosition(i: number) { - const field = document.getElementById('text-' + this.id); + const field = document.getElementById('txt-' + this.id); if(field === undefined || field === null){ return; } @@ -114,11 +133,14 @@ export class TextField extends InputElement { field.focus(); // @ts-ignore field.setSelectionRange(i, i); + } IsValid(t: string): boolean { - return !(t === undefined || t === null); + if (t === undefined || t === null) { + return false + } + return this._isValid(t, undefined); } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index e007465396..c8e25b3fad 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -6,6 +6,16 @@ import {InputElement} from "./InputElement"; import {TextField} from "./TextField"; import {UIElement} from "../UIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; +import CombinedInputElement from "./CombinedInputElement"; +import SimpleDatePicker from "./SimpleDatePicker"; + +interface TextFieldDef { + name: string, + explanation: string, + isValid: ((s: string, country?: string) => boolean), + reformat?: ((s: string, country?: string) => string), + inputHelper?: (value:UIEventSource) => InputElement +} export default class ValidatedTextField { @@ -13,12 +23,8 @@ export default class ValidatedTextField { private static tp(name: string, explanation: string, isValid?: ((s: string, country?: string) => boolean), - reformat?: ((s: string, country?: string) => string)): { - name: string, - explanation: string, - isValid: ((s: string, country?: string) => boolean), - reformat?: ((s: string, country?: string) => string) - } { + reformat?: ((s: string, country?: string) => string), + inputHelper?: (value: UIEventSource) => InputElement): TextFieldDef { if (isValid === undefined) { isValid = () => true; @@ -33,17 +39,36 @@ export default class ValidatedTextField { name: name, explanation: explanation, isValid: isValid, - reformat: reformat + reformat: reformat, + inputHelper: inputHelper } } - public static tpList = [ + public static tpList: TextFieldDef[] = [ ValidatedTextField.tp( "string", "A basic string"), ValidatedTextField.tp( "date", - "A date"), + "A date", + (str) => { + const time = Date.parse(str); + return !isNaN(time); + }, + (str) => { + const d = new Date(str); + let month = '' + (d.getMonth() + 1); + let day = '' + d.getDate(); + const year = d.getFullYear(); + + if (month.length < 2) + month = '0' + month; + if (day.length < 2) + day = '0' + day; + + return [year, month, day].join('-'); + }, + (value) => new SimpleDatePicker(value)), ValidatedTextField.tp( "wikidata", "A wikidata identifier, e.g. Q42"), @@ -82,17 +107,40 @@ export default class ValidatedTextField { (str) => EmailValidator.validate(str)), ValidatedTextField.tp( "url", - "A url"), + "A url", + (str) => { + try { + new URL(str); + return true; + } catch (e) { + return false; + } + }, (str) => { + try { + const url = new URL(str); + const blacklistedTrackingParams = [ + "fbclid",// Oh god, how I hate the fbclid. Let it burn, burn in hell! + "gclid", + "cmpid", "agid", "utm", "utm_source"] + for (const dontLike of blacklistedTrackingParams) { + url.searchParams.delete(dontLike) + } + return url.toString(); + } catch (e) { + console.error(e) + return undefined; + } + }), ValidatedTextField.tp( "phone", "A phone number", (str, country: any) => { + if (str === undefined) { + return false; + } return parsePhoneNumberFromString(str, country?.toUpperCase())?.isValid() ?? false }, - (str, country: any) => { - console.log("country formatting", country) - return parsePhoneNumberFromString(str, country?.toUpperCase()).formatInternational() - } + (str, country: any) => parsePhoneNumberFromString(str, country?.toUpperCase()).formatInternational() ) ] @@ -112,15 +160,50 @@ export default class ValidatedTextField { } return new DropDown("", values) } - + public static AllTypes = ValidatedTextField.allTypesDict(); - public static InputForType(type: string): TextField { - - return new TextField({ - placeholder: type, - isValid: ValidatedTextField.AllTypes[type] - }) + public static InputForType(type: string, options?: { + placeholder?: string | UIElement, + value?: UIEventSource, + textArea?: boolean, + textAreaRows?: number, + isValid?: ((s: string, country: string) => boolean), + country?: string + }): InputElement { + options = options ?? {}; + options.placeholder = options.placeholder ?? type; + const tp: TextFieldDef = ValidatedTextField.AllTypes[type] + const isValidTp = tp.isValid; + let isValid; + if (options.isValid) { + const optValid = options.isValid; + isValid = (str, country) => { + if(str === undefined){ + return false; + } + return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country); + } + }else{ + isValid = isValidTp; + } + options.isValid = isValid; + + let input: InputElement = new TextField(options); + if (tp.reformat) { + input.GetValue().addCallbackAndRun(str => { + if (!options.isValid(str, options.country)) { + return; + } + const formatted = tp.reformat(str, options.country); + input.GetValue().setData(formatted); + }) + } + + if (tp.inputHelper) { + input = new CombinedInputElement(input, tp.inputHelper(input.GetValue())); + } + return input; } public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined): InputElement { @@ -179,13 +262,20 @@ export default class ValidatedTextField { static Mapped(fromString: (str) => T, toString: (T) => string, options?: { placeholder?: string | UIElement, + type?: string, value?: UIEventSource, startValidated?: boolean, textArea?: boolean, textAreaRows?: number, - isValid?: ((string: string) => boolean) + isValid?: ((string: string) => boolean), + country?: string }): InputElement { - const textField = new TextField(options); + let textField: InputElement; + if (options.type) { + textField = ValidatedTextField.InputForType(options.type, options); + } else { + textField = new TextField(options); + } return new InputElementMap( textField, (a, b) => a === b, fromString, toString diff --git a/UI/ShareScreen.ts b/UI/ShareScreen.ts index 590cc9e77d..638c8305c7 100644 --- a/UI/ShareScreen.ts +++ b/UI/ShareScreen.ts @@ -64,7 +64,9 @@ export class ShareScreen extends UIElement { if (State.state !== undefined) { const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = (State.state.bm as Basemap).CurrentLayer; - const currentBackground = tr.fsIncludeCurrentBackgroundMap.Subs({name: layout.id}); + const currentBackground = new VariableUiElement(currentLayer.map(layer => { + return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}).Render(); + })); const includeCurrentBackground = new CheckBox( new Combine([Img.checkmark, currentBackground]), new Combine([Img.no_checkmark, currentBackground]), @@ -143,9 +145,13 @@ export class ShareScreen extends UIElement { let hash = ""; if (layoutDefinition !== undefined) { - hash = ("#" + layoutDefinition) literalText = "https://pietervdvn.github.io/MapComplete/index.html" - parts.push("userlayout=true"); + if (layout.id.startsWith("wiki:")) { + parts.push("userlayout=" + encodeURIComponent(layout.id)) + } else { + hash = ("#" + layoutDefinition) + parts.push("userlayout=true"); + } } @@ -157,12 +163,12 @@ export class ShareScreen extends UIElement { }, optionParts); - this.iframe = url.map(url => `<iframe src="${url}" width="100%" height="100%" title="${layout.title?.InnerRender()??""} with MapComplete"></iframe>`); + this.iframe = url.map(url => `<iframe src="${url}" width="100%" height="100%" title="${layout.title?.InnerRender()??"MapComplete"} with MapComplete"></iframe>`); this._iframeCode = new VariableUiElement( url.map((url) => { return ` - <iframe src="${url}" width="100%" height="100%" title="${layout.title.InnerRender()} with MapComplete"></iframe> + <iframe src="${url}" width="100%" height="100%" title="${layout.title?.InnerRender() ?? "MapComplete"} with MapComplete"></iframe> ` }) ); diff --git a/UI/TagRendering.ts b/UI/TagRendering.ts index d5cefbff82..20928cd347 100644 --- a/UI/TagRendering.ts +++ b/UI/TagRendering.ts @@ -298,7 +298,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { } - private InputForFreeForm(freeform : { + private InputForFreeForm(freeform: { key: string, template: string | Translation, renderTemplate: string | Translation, @@ -313,7 +313,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { .replace("$$$", "$string$") .split("$"); let type = prepost[1]; - + let isTextArea = false; if(type === "text"){ isTextArea = true; @@ -325,27 +325,14 @@ export class TagRendering extends UIElement implements TagDependantUIElement { throw "Unkown type: "+type; } - let isValid = ValidatedTextField.AllTypes[type].isValid; - if (isValid === undefined) { - isValid = () => true; - } - let formatter = ValidatedTextField.AllTypes[type].reformat ?? ((str) => str); - + const pickString = (string: any) => { if (string === "" || string === undefined) { return undefined; } - if (!isValid(string, this._source.data._country)) { - return undefined; - } - - const tag = new Tag(freeform.key, formatter(string, this._source.data._country)); - - if (tag.value.length > 255) { - return undefined; // Too long - } + const tag = new Tag(freeform.key, string); if (freeform.extraTags === undefined) { return tag; @@ -371,9 +358,16 @@ export class TagRendering extends UIElement implements TagDependantUIElement { } return undefined; } - return ValidatedTextField.Mapped( - pickString, toString, {placeholder: this._freeform.placeholder, isValid: isValid, textArea: isTextArea} - ) + + console.log("Creating a freeform input element for ", this._source.data._country); + + return ValidatedTextField.Mapped(pickString, toString, { + placeholder: this._freeform.placeholder, + type: type, + isValid: (str) => (str.length <= 255), + textArea: isTextArea, + country: this._source.data._country + }) } diff --git a/UI/UserBadge.ts b/UI/UserBadge.ts index db91c58b27..d3ea22f835 100644 --- a/UI/UserBadge.ts +++ b/UI/UserBadge.ts @@ -83,7 +83,7 @@ export class UserBadge extends UIElement { let dryrun = ""; if (user.dryRun) { - dryrun = " TESTING"; + dryrun = new FixedUiElement("TESTING").SetClass("alert").Render(); } if (user.home !== undefined) { @@ -98,7 +98,7 @@ export class UserBadge extends UIElement { const settings = "" + "settings" + - " "; + ""; const userIcon = "profile-pic"; diff --git a/assets/layers/drinking_water/drinking_water.json b/assets/layers/drinking_water/drinking_water.json index 7c6e797070..4a8159b7c3 100644 --- a/assets/layers/drinking_water/drinking_water.json +++ b/assets/layers/drinking_water/drinking_water.json @@ -45,7 +45,8 @@ "nl": "Is deze drinkwaterkraan nog steeds werkende?" }, "render": { - "en": "The operational status is {operational_status" + "en": "The operational status is {operational_status", + "nl": "Deze waterkraan-status is {operational_status}" }, "freeform": { "key": "operational_status" diff --git a/assets/layers/ghost_bike/ghost_bike.svg b/assets/layers/ghost_bike/ghost_bike.svg index 93a4b2a4ea..0ed275a6c3 100644 --- a/assets/layers/ghost_bike/ghost_bike.svg +++ b/assets/layers/ghost_bike/ghost_bike.svg @@ -1,44 +1,73 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/assets/layers/maps/maps.json b/assets/layers/maps/maps.json index e2e5f87a33..a05eca0e50 100644 --- a/assets/layers/maps/maps.json +++ b/assets/layers/maps/maps.json @@ -18,8 +18,8 @@ } }, "description": { - "en": "", - "nl": "" + "en": "A map, meant for tourists which is permanently installed in the public space", + "nl": "Een permantent geinstalleerde kaart" }, "tagRenderings": [ { diff --git a/assets/themes/cyclestreets/F111.svg b/assets/themes/cyclestreets/F111.svg index 0217385159..e5f55a162c 100644 --- a/assets/themes/cyclestreets/F111.svg +++ b/assets/themes/cyclestreets/F111.svg @@ -1,775 +1,784 @@ -image/svg+xmlimage/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + d="m 3956,10770 c -16,-5 -40,-19 -53,-32 -55,-51 -53,-16 -53,-853 v -775 h -935 -935 v 375 375 h -300 -300 v -370 -370 h -63 -63 l -41,-72 c -104,-183 -165,-393 -194,-663 -15,-136 -14,-1000 1,-1056 10,-35 27,-53 124,-131 18,-16 40,-28 49,-28 24,0 31,-15 110,-225 42,-110 105,-276 140,-370 36,-93 140,-367 231,-609 92,-241 173,-453 181,-472 l 14,-34 h 518 518 l 94,-335 c 80,-285 98,-338 122,-361 35,-34 268,-130 473,-196 l 163,-52 -58,-43 c -104,-76 -177,-190 -205,-321 -17,-84 -17,-120 1,-207 53,-252 262,-425 514,-425 105,0 186,20 266,65 147,83 247,237 269,416 21,170 -80,387 -225,480 l -49,33 187,62 c 245,81 426,158 456,194 17,20 51,123 118,359 l 94,331 h 180 180 l 102,228 c 56,125 226,504 377,842 l 275,615 33,6 c 48,8 135,76 158,124 19,39 20,65 22,476 1,451 -5,566 -43,751 -35,175 -115,382 -198,511 l -31,47 h -100 -100 v 400 400 h -300 -300 v -375 -375 h -365 -365 v 270 270 h 140 140 v 125 125 h -290 -290 v -395 -395 h -100 -100 l -2,789 -3,790 -35,36 c -19,19 -50,40 -70,45 -40,11 -45,11 -84,0 z M 3850,8755 v -45 h -635 -635 v 45 45 h 635 635 z m 498,-2 3,-43 h -100 -101 v 45 46 l 98,-3 97,-3 z m 532,2 v -45 h -115 -115 v 45 45 h 115 115 z M 3080,8425 v -45 h -385 -385 v 45 45 h 385 385 z m 770,0 v -45 h -95 -95 v 45 45 h 95 95 z m 500,0 v -45 h -100 -100 v 45 45 h 100 100 z m 800,0 v -45 h -250 -250 v 45 45 h 250 250 z m -3247,-93 c 91,-35 199,-168 218,-267 12,-62 -5,-145 -43,-217 -40,-76 -70,-105 -144,-145 -126,-67 -259,-57 -374,27 -51,37 -77,68 -106,130 -23,47 -27,68 -27,150 -1,112 15,151 97,238 80,85 153,114 266,107 36,-2 87,-13 113,-23 z m 3964,9 c 52,-10 139,-74 192,-141 77,-97 77,-277 1,-391 -63,-92 -171,-151 -280,-151 -125,-1 -233,56 -291,155 -90,152 -90,262 -2,384 39,54 132,131 168,138 11,2 29,6 40,8 35,8 128,6 172,-2 z M 3350,8080 v -40 h -385 -385 v 40 40 h 385 385 z m 500,0 v -40 h -95 -95 v 40 40 h 95 95 z m 500,0 v -40 h -100 -100 v 40 40 h 100 100 z m 530,0 v -40 h -115 -115 v 40 40 h 115 115 z M 1625,7106 c 285,-33 685,-52 1313,-62 l 412,-6 v -223 c 0,-212 1,-224 23,-265 55,-104 187,-107 261,-7 20,27 21,43 24,257 l 3,229 121,3 c 66,2 122,1 124,-1 2,-2 4,-131 4,-285 0,-155 5,-314 10,-355 6,-41 8,-77 5,-79 -7,-8 -212,17 -314,38 -178,37 -481,154 -504,195 -7,13 -62,15 -362,15 h -355 v -95 -95 h 135 135 v -26 c 0,-23 175,-657 187,-677 2,-4 -194,-6 -437,-5 l -442,3 -229,615 c -126,338 -249,668 -274,734 l -44,118 37,-5 c 20,-4 96,-13 167,-21 z m 4313,-183 c -56,-115 -215,-444 -354,-733 l -253,-525 -75,-3 -75,-3 20,68 c 93,325 169,603 169,621 0,21 3,22 125,22 h 126 l -3,93 -3,92 -355,3 c -319,2 -355,1 -363,-14 -21,-38 -306,-148 -487,-189 -99,-22 -327,-52 -334,-44 -2,2 3,56 10,119 7,63 13,226 14,363 v 247 h 125 124 l 3,-244 3,-244 25,-27 c 76,-81 195,-71 247,21 22,37 23,49 23,270 v 231 l 298,6 c 405,9 824,37 997,66 104,18 105,34 -7,-196 z M 3257,6335 c 484,-196 1058,-189 1527,16 50,22 97,43 104,46 7,3 12,-2 12,-11 0,-13 18,-16 108,-18 l 108,-3 -92,-330 c -51,-181 -95,-340 -99,-352 -5,-23 -7,-23 -178,-23 h -174 l -17,98 -17,97 56,184 c 30,101 53,186 52,188 -2,2 -55,-10 -118,-27 -355,-92 -715,-92 -1062,1 -43,12 -80,20 -82,17 -2,-2 21,-84 51,-183 l 55,-179 -17,-98 -17,-98 h -173 -173 l -11,38 c -6,20 -50,179 -99,352 l -87,315 94,3 c 70,2 94,6 98,17 5,13 10,13 35,0 16,-9 68,-31 116,-50 z m 163,-882 c 0,-6 -62,-377 -93,-553 -2,-13 -157,532 -157,553 0,4 56,7 125,7 69,0 125,-3 125,-7 z m 1440,0 c 0,-6 -138,-508 -153,-557 -3,-7 -98,538 -97,557 0,4 56,7 125,7 69,0 125,-3 125,-7 z" /> + \ No newline at end of file diff --git a/assets/themes/toilets/toilets.json b/assets/themes/toilets/toilets.json index 0b85b52d7a..3c532ae915 100644 --- a/assets/themes/toilets/toilets.json +++ b/assets/themes/toilets/toilets.json @@ -23,10 +23,8 @@ { "id": "Toilet", "name": { - "render": { - "en": "Toilets", - "de": "Toiletten" - } + "en": "Toilets", + "de": "Toiletten" }, "overpassTags": "amenity=toilets", "title": { @@ -59,10 +57,8 @@ "amenity=toilets" ], "description": { - "render": { "en": "A publicly accessible toilet or restroom", "de": "Eine öffentlich zugängliche Toilette" - } } }, { diff --git a/createLayouts.ts b/createLayouts.ts index 5ead925049..49fda8c7e1 100644 --- a/createLayouts.ts +++ b/createLayouts.ts @@ -13,17 +13,13 @@ import Translations from "./UI/i18n/Translations"; import {TagRendering} from "./UI/TagRendering"; TagRendering.injectFunction(); - - console.log("Building the layouts") - function enc(str: string): string { return encodeURIComponent(str.toLowerCase()); } function validate(layout: Layout) { - console.log("Validationg ", layout.id) const translations: Translation[] = []; const queue: any[] = [layout] @@ -49,10 +45,13 @@ function validate(layout: Layout) { missing[ln] = 0; present[ln] = 0; for (const translation of translations) { + if (translation.translations["*"] !== undefined) { + continue; + } const txt = translation.translations[ln]; const isMissing = txt === undefined || txt === "" || txt.toLowerCase().indexOf("todo") >= 0; if (isMissing) { - console.log(`Missing or suspicious ${ln}-translation for '`, translation.txt, ":", txt) + console.log(` ${layout.id}: No translation for`, ln, "in", translation.translations, "got:", txt) missing[ln]++ } else { present[ln]++; @@ -60,12 +59,21 @@ function validate(layout: Layout) { } } - console.log("Translation completenes for", layout.id); + let message = `Translation completenes for theme ${layout.id}` + let isComplete = true; for (const ln of layout.supportedLanguages) { const amiss = missing[ln]; const ok = present[ln]; const total = amiss + ok; - console.log(`${ln}: ${ok}/${total}`) + message += `\n${ln}: ${ok}/${total}` + if (ok !== total) { + isComplete = false; + } + } + if (isComplete) { + console.log(`${layout.id} is fully translated!`) + } else { + console.log(message) } } @@ -129,23 +137,21 @@ function createIcon(iconPath: string, size: number) { } console.log("Creating icon ", name, newname) + try { + svg2img(iconPath, + // @ts-ignore + {width: size, height: size, preserveAspectRatio: true}) + .then((buffer) => { + console.log("Writing icon", newname) + writeFileSync(newname, buffer); + }).catch((error) => { + console.log("ERROR while writing" + iconPath, error) + }); - try{ - - svg2img(iconPath, - // @ts-ignore - {width: size, height: size, preserveAspectRatio: true}) - .then((buffer) => { - console.log("Writing icon", newname) - writeFileSync(newname, buffer); - }).catch((error) => { - console.log("ERROR while writing" + iconPath, error) - }); - - }catch(e){ - console.error("Could not read icon",iconPath,"due to",e) + } catch (e) { + console.error("Could not read icon", iconPath, "due to", e) } - + return newname; } @@ -155,7 +161,6 @@ function createManifest(layout: Layout, relativePath: string) { const icons = []; let icon = layout.icon; - console.log(icon) if (icon.endsWith(".svg")) { // This is an svg. Lets create the needed pngs! const sizes = [72, 96, 120, 128, 144, 152, 180, 192, 384, 512]; @@ -240,20 +245,21 @@ for (const layoutName in all) { }; const layout = all[layoutName]; validate(layout) - console.log("Generating manifest") const manif = JSON.stringify(createManifest(layout, "/MapComplete")); const manifestLocation = encodeURIComponent(layout.id.toLowerCase()) + ".webmanifest"; writeFile(manifestLocation, manif, err); const landing = createLandingPage(layout); - console.log("Generating html-file for ", layout.id) writeFile(enc(layout.id) + ".html", landing, err) - console.log("done") wikiPage += "\n\n"+generateWikiEntry(layout); } -writeFile("wikiIndex", wikiPage, (err) => {err ?? console.log("Could not save wikiindex", err)}); +writeFile("./assets/generated/wikiIndex", wikiPage, (err) => { + if (err !== null) { + console.log("Could not save wikiindex", err); + } +}); console.log("Counting all translations") Translations.CountTranslations(); console.log("All done!"); \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index 2b5e795a35..6c00c40efb 100755 --- a/deploy.sh +++ b/deploy.sh @@ -9,6 +9,8 @@ ts-node createLayouts.ts || { echo 'Creating layouts failed' ; exit 1; } find -name '*.png' | parallel optipng '{}' npm run build || { echo 'Npm build failed' ; exit 1; } +rm -rf .cache + if [[ $1 == "groen" ]] then echo "DEPLOYING TO BUURTNATUUR!" diff --git a/index.css b/index.css index 105ce424a1..c6eb9c3bfe 100644 --- a/index.css +++ b/index.css @@ -71,6 +71,8 @@ body { .invalid { box-shadow: 0 0 10px #ff5353; + display: block; + height: min-content; } .shadow { diff --git a/package-lock.json b/package-lock.json index 692dba9a3d..0ffcd79026 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4636,9 +4636,10 @@ "integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==" }, "node-forge": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", - "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "dev": true }, "node-libs-browser": { "version": "2.2.1", @@ -5218,7 +5219,6 @@ "json5": "^1.0.1", "micromatch": "^3.0.4", "mkdirp": "^0.5.1", - "node-forge": "^0.7.1", "node-libs-browser": "^2.0.0", "opn": "^5.1.0", "postcss": "^7.0.11", diff --git a/package.json b/package.json index 1f318987b6..de139dc6d3 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "main": "index.js", - "disabled:staticFiles": { + "staticFiles": { "staticPath": [ { - "staticPath": "tiles", + "staticPath": "tiles/", "staticOutDir": "./tiles/" } ] diff --git a/test.ts b/test.ts index c3d60788e2..b81a45148b 100644 --- a/test.ts +++ b/test.ts @@ -2,4 +2,4 @@ import AvailableBaseLayers from "./Logic/AvailableBaseLayers"; const layers = AvailableBaseLayers.AvailableLayersAt(51.2,3.2); -console.log(layers); \ No newline at end of file +console.log(layers); diff --git a/wikiIndex b/wikiIndex deleted file mode 100644 index 050c69c2e2..0000000000 --- a/wikiIndex +++ /dev/null @@ -1,204 +0,0 @@ - - -{{Software -|name = personal -|author = MapComplete builtin -|web = https://pietervdvn.github.io/MapComplete/personal.html -|repo = https://github.com/pietervdvn/MapComplete -|platform = web -|code = Typescript;HTML;CSS -|languages = en -|genre = display;editor -|screenshot = MapComplete_Screenshot.png -|description = A MapComplete theme: The personal theme allows to select one or more layers from all the layouts, creating a truly personal editor -|map = yes -|findLocation = yes -|findNearbyPOI = yes -|addPOI = yes -|editPOI = yes -|editTags = yes -| -}} - -{{Software -|name = bookcases -|author = MapComplete -|web = https://pietervdvn.github.io/MapComplete/bookcases.html -|repo = https://github.com/pietervdvn/MapComplete -|platform = web -|code = Typescript;HTML;CSS -|languages = en;nl;de -|genre = display;editor -|screenshot = MapComplete_Screenshot.png -|description = A MapComplete theme: A public bookcase is a small streetside cabinet, box, old phone boot or some other objects where books are stored -|map = yes -|findLocation = yes -|findNearbyPOI = yes -|addPOI = yes -|editPOI = yes -|editTags = yes -| -}} - -{{Software -|name = aed -|author = MapComplete -|web = https://pietervdvn.github.io/MapComplete/aed.html -|repo = https://github.com/pietervdvn/MapComplete -|platform = web -|code = Typescript;HTML;CSS -|languages = en;fr;nl;de -|genre = display;editor -|screenshot = MapComplete_Screenshot.png -|description = A MapComplete theme: On this map, one can find and mark nearby defibrillators -|map = yes -|findLocation = yes -|findNearbyPOI = yes -|addPOI = yes -|editPOI = yes -|editTags = yes -| -}} - -{{Software -|name = toilets -|author = MapComplete -|web = https://pietervdvn.github.io/MapComplete/toilets.html -|repo = https://github.com/pietervdvn/MapComplete -|platform = web -|code = Typescript;HTML;CSS -|languages = en;de -|genre = display;editor -|screenshot = MapComplete_Screenshot.png -|description = A MapComplete theme: A map of public toilets -|map = yes -|findLocation = yes -|findNearbyPOI = yes -|addPOI = yes -|editPOI = yes -|editTags = yes -| -}} - -{{Software -|name = artworks -|author = MapComplete -|web = https://pietervdvn.github.io/MapComplete/artworks.html -|repo = https://github.com/pietervdvn/MapComplete -|platform = web -|code = Typescript;HTML;CSS -|languages = en;nl;fr;de -|genre = display;editor -|screenshot = MapComplete_Screenshot.png -|description = A MapComplete theme: Welcome to Open Artwork Map, a map of statues, busts, grafittis, -|map = yes -|findLocation = yes -|findNearbyPOI = yes -|addPOI = yes -|editPOI = yes -|editTags = yes -| -}} - -{{Software -|name = cyclofix -|author = MapComplete -|web = https://pietervdvn.github.io/MapComplete/cyclofix.html -|repo = https://github.com/pietervdvn/MapComplete -|platform = web -|code = Typescript;HTML;CSS -|languages = en;nl;fr;gl;de -|genre = display;editor -|screenshot = MapComplete_Screenshot.png -|description = A MapComplete theme: The goal of this map is to present cyclists with an easy-to-use solution to find the appropriate infrastructure for their needs -|map = yes -|findLocation = yes -|findNearbyPOI = yes -|addPOI = yes -|editPOI = yes -|editTags = yes -| -}} - -{{Software -|name = ghostbikes -|author = MapComplete -|web = https://pietervdvn.github.io/MapComplete/ghostbikes.html -|repo = https://github.com/pietervdvn/MapComplete -|platform = web -|code = Typescript;HTML;CSS -|languages = en;nl;de -|genre = display;editor -|screenshot = MapComplete_Screenshot.png -|description = A MapComplete theme: A ghost bike is a memorial for a cyclist who died in a traffic accident, in the form of a white bicycle placed permanently near the accident location -|map = yes -|findLocation = yes -|findNearbyPOI = yes -|addPOI = yes -|editPOI = yes -|editTags = yes -| -}} - -{{Software -|name = nature -|author = -|web = https://pietervdvn.github.io/MapComplete/nature.html -|repo = https://github.com/pietervdvn/MapComplete -|platform = web -|code = Typescript;HTML;CSS -|languages = nl -|genre = display;editor -|screenshot = MapComplete_Screenshot.png -|description = A MapComplete theme: Deze kaart bevat informatie voor natuurliefhebbers -|map = yes -|findLocation = yes -|findNearbyPOI = yes -|addPOI = yes -|editPOI = yes -|editTags = yes -| -}} - -{{Software -|name = fietsstraten -|author = MapComlete -|web = https://pietervdvn.github.io/MapComplete/fietsstraten.html -|repo = https://github.com/pietervdvn/MapComplete -|platform = web -|code = Typescript;HTML;CSS -|languages = nl -|genre = display;editor -|screenshot = MapComplete_Screenshot.png -|description = A MapComplete theme: Een kaart met alle gekende fietsstraten -|map = yes -|findLocation = yes -|findNearbyPOI = yes -|addPOI = yes -|editPOI = yes -|editTags = yes -| -}} - -{{Software -|name = maps -|author = MapComplete -|web = https://pietervdvn.github.io/MapComplete/maps.html -|repo = https://github.com/pietervdvn/MapComplete -|platform = web -|code = Typescript;HTML;CSS -|languages = en;nl -|genre = display;editor -|screenshot = MapComplete_Screenshot.png -|description = A MapComplete theme: On this map, all the maps known by OpenStreetMap are shown -|map = yes -|findLocation = yes -|findNearbyPOI = yes -|addPOI = yes -|editPOI = yes -|editTags = yes -| -}} - - -