Merge develop
This commit is contained in:
commit
620a1f8df2
27 changed files with 1206 additions and 1151 deletions
|
@ -64,7 +64,6 @@ export class FromJSON {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LayoutFromJSON(json: LayoutConfigJson): Layout {
|
public static LayoutFromJSON(json: LayoutConfigJson): Layout {
|
||||||
console.log(json)
|
|
||||||
const tr = FromJSON.Translation;
|
const tr = FromJSON.Translation;
|
||||||
|
|
||||||
const layers = json.layers.map(FromJSON.Layer);
|
const layers = json.layers.map(FromJSON.Layer);
|
||||||
|
@ -105,6 +104,10 @@ export class FromJSON {
|
||||||
if (typeof (json) === "string") {
|
if (typeof (json) === "string") {
|
||||||
return new Translation({"*": json});
|
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 = {};
|
const tr = {};
|
||||||
let keyCount = 0;
|
let keyCount = 0;
|
||||||
for (let key in json) {
|
for (let key in json) {
|
||||||
|
@ -115,7 +118,6 @@ export class FromJSON {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const transl = new Translation(tr);
|
const transl = new Translation(tr);
|
||||||
transl.addCallback(latest => console.log("tr callback changed to", latest));
|
|
||||||
return transl;
|
return transl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ export class FilteredLayer {
|
||||||
|
|
||||||
private _showOnPopup: (tags: UIEventSource<any>, feature: any) => UIElement;
|
private _showOnPopup: (tags: UIEventSource<any>, feature: any) => UIElement;
|
||||||
|
|
||||||
private static readonly grid = codegrid.CodeGrid();
|
private static readonly grid = codegrid.CodeGrid("./tiles/");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
layerDef: LayerDefinition,
|
layerDef: LayerDefinition,
|
||||||
|
@ -52,12 +52,20 @@ export class FilteredLayer {
|
||||||
|
|
||||||
this._wayHandling = layerDef.wayHandling;
|
this._wayHandling = layerDef.wayHandling;
|
||||||
this._showOnPopup = showOnPopup;
|
this._showOnPopup = showOnPopup;
|
||||||
this._style = layerDef.style;
|
this._style = (tags) => {
|
||||||
if (this._style === undefined) {
|
if(layerDef.style === undefined){
|
||||||
this._style = function () {
|
|
||||||
return {icon: {iconUrl: "./assets/bug.svg"}, color: "#000"};
|
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.name = name;
|
||||||
this.filters = layerDef.overpassFilter;
|
this.filters = layerDef.overpassFilter;
|
||||||
this._maxAllowedOverlap = layerDef.maxAllowedOverlapPercentage;
|
this._maxAllowedOverlap = layerDef.maxAllowedOverlapPercentage;
|
||||||
|
@ -102,14 +110,17 @@ export class FilteredLayer {
|
||||||
|
|
||||||
if (this.filters.matches(tags)) {
|
if (this.filters.matches(tags)) {
|
||||||
const centerPoint = GeoOperations.centerpoint(feature);
|
const centerPoint = GeoOperations.centerpoint(feature);
|
||||||
feature.properties["_surface"] = ""+GeoOperations.surfaceAreaInSqMeters(feature);
|
feature.properties["_surface"] = "" + GeoOperations.surfaceAreaInSqMeters(feature);
|
||||||
const lat = ""+centerPoint.geometry.coordinates[1];
|
const lat = centerPoint.geometry.coordinates[1];
|
||||||
const lon = ""+centerPoint.geometry.coordinates[0]
|
const lon = centerPoint.geometry.coordinates[0]
|
||||||
feature.properties["_lon"] = lat;
|
feature.properties["_lon"] = "" + lat; // We expect a string here for lat/lon
|
||||||
feature.properties["_lat"] = lon;
|
feature.properties["_lat"] = "" + lon;
|
||||||
|
// But the codegrid SHOULD be a number!
|
||||||
FilteredLayer.grid.getCode(lat, lon, (error, code) => {
|
FilteredLayer.grid.getCode(lat, lon, (error, code) => {
|
||||||
if (error === null) {
|
if (error === null) {
|
||||||
feature.properties["_country"] = code;
|
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) {
|
pointToLayer: function (feature, latLng) {
|
||||||
const style = self._style(feature.properties);
|
const style = self._style(feature.properties);
|
||||||
let marker;
|
let marker;
|
||||||
if (style.icon === undefined) {
|
if (style.icon === undefined) {
|
||||||
marker = L.circle(latLng, {
|
marker = L.circle(latLng, {
|
||||||
radius: 25,
|
radius: 25,
|
||||||
color: style.color
|
color: style.color
|
||||||
});
|
});
|
||||||
|
|
||||||
} else if (style.icon.iconUrl.startsWith("$circle")) {
|
} else if (style.icon.iconUrl.startsWith("$circle")) {
|
||||||
marker = L.circle(latLng, {
|
marker = L.circle(latLng, {
|
||||||
radius: 25,
|
radius: 25,
|
||||||
color: style.color
|
color: style.color
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (style.icon.iconSize === undefined) {
|
if (style.icon.iconSize === undefined) {
|
||||||
style.icon.iconSize = [50, 50]
|
style.icon.iconSize = [50, 50]
|
||||||
}
|
}
|
||||||
|
|
||||||
marker = L.marker(latLng, {
|
marker = L.marker(latLng, {
|
||||||
icon: new L.icon(style.icon),
|
icon: new L.icon(style.icon),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let eventSource = State.state.allElements.addOrGetElement(feature);
|
let eventSource = State.state.allElements.addOrGetElement(feature);
|
||||||
const popup = L.popup({}, marker);
|
const popup = L.popup({}, marker);
|
||||||
let uiElement: UIElement;
|
let uiElement: UIElement;
|
||||||
|
|
|
@ -95,10 +95,10 @@ export class Tag extends TagsFilter {
|
||||||
this.key = key
|
this.key = key
|
||||||
this.value = value
|
this.value = value
|
||||||
if(key === undefined || key === ""){
|
if(key === undefined || key === ""){
|
||||||
throw "Invalid key";
|
throw "Invalid key: undefined or empty";
|
||||||
}
|
}
|
||||||
if(value === undefined){
|
if(value === undefined){
|
||||||
throw "Invalid value";
|
throw "Invalid value: value is undefined";
|
||||||
}
|
}
|
||||||
if(value === "*"){
|
if(value === "*"){
|
||||||
console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`)
|
console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`)
|
||||||
|
|
2
State.ts
2
State.ts
|
@ -22,7 +22,7 @@ export class State {
|
||||||
// The singleton of the global state
|
// The singleton of the global state
|
||||||
public static state: 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
|
// The user journey states thresholds when a new feature gets unlocked
|
||||||
public static userJourney = {
|
public static userJourney = {
|
||||||
|
|
35
UI/Input/CombinedInputElement.ts
Normal file
35
UI/Input/CombinedInputElement.ts
Normal file
|
@ -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<T> extends InputElement<T> {
|
||||||
|
private readonly _a: InputElement<T>;
|
||||||
|
private readonly _b: UIElement;
|
||||||
|
private readonly _combined: UIElement;
|
||||||
|
public readonly IsSelected: UIEventSource<boolean>;
|
||||||
|
|
||||||
|
constructor(a: InputElement<T>, b: InputElement<T>) {
|
||||||
|
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<T> {
|
||||||
|
return this._a.GetValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
InnerRender(): string {
|
||||||
|
return this._combined.Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
IsValid(t: T): boolean {
|
||||||
|
return this._a.IsValid(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,13 +6,11 @@ export default class MultiLingualTextFields extends InputElement<any> {
|
||||||
private _fields: Map<string, TextField> = new Map<string, TextField>();
|
private _fields: Map<string, TextField> = new Map<string, TextField>();
|
||||||
private readonly _value: UIEventSource<any>;
|
private readonly _value: UIEventSource<any>;
|
||||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
|
||||||
constructor(languages: UIEventSource<string[]>,
|
constructor(languages: UIEventSource<string[]>,
|
||||||
textArea: boolean = false,
|
textArea: boolean = false,
|
||||||
value: UIEventSource<Map<string, UIEventSource<string>>> = undefined) {
|
value: UIEventSource<Map<string, UIEventSource<string>>> = undefined) {
|
||||||
super(undefined);
|
super(undefined);
|
||||||
this._value = value ?? new UIEventSource({});
|
this._value = value ?? new UIEventSource({});
|
||||||
|
|
||||||
this._value.addCallbackAndRun(latestData => {
|
this._value.addCallbackAndRun(latestData => {
|
||||||
if (typeof (latestData) === "string") {
|
if (typeof (latestData) === "string") {
|
||||||
console.warn("Refusing string for multilingual input", latestData);
|
console.warn("Refusing string for multilingual input", latestData);
|
||||||
|
|
|
@ -3,7 +3,6 @@ import TagInput from "./SingleTagInput";
|
||||||
import {MultiInput} from "./MultiInput";
|
import {MultiInput} from "./MultiInput";
|
||||||
|
|
||||||
export class MultiTagInput extends MultiInput<string> {
|
export class MultiTagInput extends MultiInput<string> {
|
||||||
|
|
||||||
|
|
||||||
constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) {
|
constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) {
|
||||||
super("Add a new tag",
|
super("Add a new tag",
|
||||||
|
|
|
@ -11,8 +11,7 @@ export class RadioButton<T> extends InputElement<T> {
|
||||||
private readonly value: UIEventSource<T>;
|
private readonly value: UIEventSource<T>;
|
||||||
private readonly _elements: InputElement<T>[]
|
private readonly _elements: InputElement<T>[]
|
||||||
private readonly _selectFirstAsDefault: boolean;
|
private readonly _selectFirstAsDefault: boolean;
|
||||||
|
|
||||||
|
|
||||||
constructor(elements: InputElement<T>[],
|
constructor(elements: InputElement<T>[],
|
||||||
selectFirstAsDefault = true) {
|
selectFirstAsDefault = true) {
|
||||||
super(undefined);
|
super(undefined);
|
||||||
|
@ -20,7 +19,6 @@ export class RadioButton<T> extends InputElement<T> {
|
||||||
this._selectFirstAsDefault = selectFirstAsDefault;
|
this._selectFirstAsDefault = selectFirstAsDefault;
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
|
||||||
this.value =
|
this.value =
|
||||||
UIEventSource.flatten(this._selectedElementIndex.map(
|
UIEventSource.flatten(this._selectedElementIndex.map(
|
||||||
(selectedIndex) => {
|
(selectedIndex) => {
|
||||||
|
|
60
UI/Input/SimpleDatePicker.ts
Normal file
60
UI/Input/SimpleDatePicker.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import {InputElement} from "./InputElement";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
|
||||||
|
export default class SimpleDatePicker extends InputElement<string> {
|
||||||
|
|
||||||
|
private readonly value: UIEventSource<string>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
value?: UIEventSource<string>
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.value = value ?? new UIEventSource<string>(undefined);
|
||||||
|
const self = this;
|
||||||
|
this.value.addCallbackAndRun(v => {
|
||||||
|
if(v === undefined){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.SetValue(v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
InnerRender(): string {
|
||||||
|
return `<span id="${this.id}"><input type='date' id='date-${this.id}'></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string> {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
|
||||||
|
IsValid(t: string): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ export default class SingleTagInput extends InputElement<string> {
|
||||||
constructor(value: UIEventSource<string> = undefined) {
|
constructor(value: UIEventSource<string> = undefined) {
|
||||||
super(undefined);
|
super(undefined);
|
||||||
this._value = value ?? new UIEventSource<string>("");
|
this._value = value ?? new UIEventSource<string>("");
|
||||||
|
|
||||||
this.helpMessage = new VariableUiElement(this._value.map(tagDef => {
|
this.helpMessage = new VariableUiElement(this._value.map(tagDef => {
|
||||||
try {
|
try {
|
||||||
FromJSON.Tag(tagDef, "");
|
FromJSON.Tag(tagDef, "");
|
||||||
|
|
|
@ -11,12 +11,14 @@ export class TextField extends InputElement<string> {
|
||||||
private readonly _isArea: boolean;
|
private readonly _isArea: boolean;
|
||||||
private readonly _textAreaRows: number;
|
private readonly _textAreaRows: number;
|
||||||
|
|
||||||
|
private readonly _isValid: (string, country) => boolean;
|
||||||
|
|
||||||
constructor(options?: {
|
constructor(options?: {
|
||||||
placeholder?: string | UIElement,
|
placeholder?: string | UIElement,
|
||||||
value?: UIEventSource<string>,
|
value?: UIEventSource<string>,
|
||||||
textArea?: boolean,
|
textArea?: boolean,
|
||||||
textAreaRows?: number,
|
textAreaRows?: number,
|
||||||
isValid?: ((s: string) => boolean)
|
isValid?: ((s: string, country?: string) => boolean)
|
||||||
}) {
|
}) {
|
||||||
super(undefined);
|
super(undefined);
|
||||||
const self = this;
|
const self = this;
|
||||||
|
@ -25,9 +27,8 @@ export class TextField extends InputElement<string> {
|
||||||
this._isArea = options.textArea ?? false;
|
this._isArea = options.textArea ?? false;
|
||||||
this.value = options?.value ?? new UIEventSource<string>(undefined);
|
this.value = options?.value ?? new UIEventSource<string>(undefined);
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
this._fromString = options.fromString ?? ((str) => (str))
|
|
||||||
this._textAreaRows = options.textAreaRows;
|
this._textAreaRows = options.textAreaRows;
|
||||||
|
this._isValid = options.isValid ?? ((str, country) => true);
|
||||||
|
|
||||||
this._placeholder = Translations.W(options.placeholder ?? "");
|
this._placeholder = Translations.W(options.placeholder ?? "");
|
||||||
this.ListenTo(this._placeholder._source);
|
this.ListenTo(this._placeholder._source);
|
||||||
|
@ -36,23 +37,20 @@ export class TextField extends InputElement<string> {
|
||||||
self.IsSelected.setData(true)
|
self.IsSelected.setData(true)
|
||||||
});
|
});
|
||||||
this.value.addCallback((t) => {
|
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) {
|
if (field === undefined || field === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.isValid) {
|
field.className = self.IsValid(t) ? "" : "invalid";
|
||||||
field.className = options.isValid(t) ? "" : "invalid";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t === undefined || t === null) {
|
if (t === undefined || t === null) {
|
||||||
// @ts-ignore
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
field.value = t;
|
field.value = t;
|
||||||
});
|
});
|
||||||
this.dumbMode = false;
|
this.dumbMode = false;
|
||||||
this.SetClass("deadbeef")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GetValue(): UIEventSource<string> {
|
GetValue(): UIEventSource<string> {
|
||||||
|
@ -72,16 +70,37 @@ export class TextField extends InputElement<string> {
|
||||||
`</form></span>`;
|
`</form></span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
Update() {
|
|
||||||
super.Update();
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerUpdate() {
|
InnerUpdate() {
|
||||||
const field = document.getElementById("txt-" + this.id);
|
const field = document.getElementById("txt-" + this.id);
|
||||||
const self = this;
|
const self = this;
|
||||||
field.oninput = () => {
|
field.oninput = () => {
|
||||||
|
|
||||||
|
// How much characters are on the right, not including spaces?
|
||||||
// @ts-ignore
|
// @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) {
|
if (this.value.data !== undefined && this.value.data !== null) {
|
||||||
|
@ -103,7 +122,7 @@ export class TextField extends InputElement<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public SetCursorPosition(i: number) {
|
public SetCursorPosition(i: number) {
|
||||||
const field = document.getElementById('text-' + this.id);
|
const field = document.getElementById('txt-' + this.id);
|
||||||
if(field === undefined || field === null){
|
if(field === undefined || field === null){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -114,11 +133,14 @@ export class TextField extends InputElement<string> {
|
||||||
field.focus();
|
field.focus();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
field.setSelectionRange(i, i);
|
field.setSelectionRange(i, i);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IsValid(t: string): boolean {
|
IsValid(t: string): boolean {
|
||||||
return !(t === undefined || t === null);
|
if (t === undefined || t === null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this._isValid(t, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,16 @@ import {InputElement} from "./InputElement";
|
||||||
import {TextField} from "./TextField";
|
import {TextField} from "./TextField";
|
||||||
import {UIElement} from "../UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
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<string>) => InputElement<string>
|
||||||
|
}
|
||||||
|
|
||||||
export default class ValidatedTextField {
|
export default class ValidatedTextField {
|
||||||
|
|
||||||
|
@ -13,12 +23,8 @@ export default class ValidatedTextField {
|
||||||
private static tp(name: string,
|
private static tp(name: string,
|
||||||
explanation: string,
|
explanation: string,
|
||||||
isValid?: ((s: string, country?: string) => boolean),
|
isValid?: ((s: string, country?: string) => boolean),
|
||||||
reformat?: ((s: string, country?: string) => string)): {
|
reformat?: ((s: string, country?: string) => string),
|
||||||
name: string,
|
inputHelper?: (value: UIEventSource<string>) => InputElement<string>): TextFieldDef {
|
||||||
explanation: string,
|
|
||||||
isValid: ((s: string, country?: string) => boolean),
|
|
||||||
reformat?: ((s: string, country?: string) => string)
|
|
||||||
} {
|
|
||||||
|
|
||||||
if (isValid === undefined) {
|
if (isValid === undefined) {
|
||||||
isValid = () => true;
|
isValid = () => true;
|
||||||
|
@ -33,17 +39,36 @@ export default class ValidatedTextField {
|
||||||
name: name,
|
name: name,
|
||||||
explanation: explanation,
|
explanation: explanation,
|
||||||
isValid: isValid,
|
isValid: isValid,
|
||||||
reformat: reformat
|
reformat: reformat,
|
||||||
|
inputHelper: inputHelper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static tpList = [
|
public static tpList: TextFieldDef[] = [
|
||||||
ValidatedTextField.tp(
|
ValidatedTextField.tp(
|
||||||
"string",
|
"string",
|
||||||
"A basic string"),
|
"A basic string"),
|
||||||
ValidatedTextField.tp(
|
ValidatedTextField.tp(
|
||||||
"date",
|
"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(
|
ValidatedTextField.tp(
|
||||||
"wikidata",
|
"wikidata",
|
||||||
"A wikidata identifier, e.g. Q42"),
|
"A wikidata identifier, e.g. Q42"),
|
||||||
|
@ -82,17 +107,40 @@ export default class ValidatedTextField {
|
||||||
(str) => EmailValidator.validate(str)),
|
(str) => EmailValidator.validate(str)),
|
||||||
ValidatedTextField.tp(
|
ValidatedTextField.tp(
|
||||||
"url",
|
"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(
|
ValidatedTextField.tp(
|
||||||
"phone",
|
"phone",
|
||||||
"A phone number",
|
"A phone number",
|
||||||
(str, country: any) => {
|
(str, country: any) => {
|
||||||
|
if (str === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return parsePhoneNumberFromString(str, country?.toUpperCase())?.isValid() ?? false
|
return parsePhoneNumberFromString(str, country?.toUpperCase())?.isValid() ?? false
|
||||||
},
|
},
|
||||||
(str, country: any) => {
|
(str, country: any) => parsePhoneNumberFromString(str, country?.toUpperCase()).formatInternational()
|
||||||
console.log("country formatting", country)
|
|
||||||
return parsePhoneNumberFromString(str, country?.toUpperCase()).formatInternational()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -112,15 +160,50 @@ export default class ValidatedTextField {
|
||||||
}
|
}
|
||||||
return new DropDown<string>("", values)
|
return new DropDown<string>("", values)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AllTypes = ValidatedTextField.allTypesDict();
|
public static AllTypes = ValidatedTextField.allTypesDict();
|
||||||
|
|
||||||
public static InputForType(type: string): TextField {
|
public static InputForType(type: string, options?: {
|
||||||
|
placeholder?: string | UIElement,
|
||||||
return new TextField({
|
value?: UIEventSource<string>,
|
||||||
placeholder: type,
|
textArea?: boolean,
|
||||||
isValid: ValidatedTextField.AllTypes[type]
|
textAreaRows?: number,
|
||||||
})
|
isValid?: ((s: string, country: string) => boolean),
|
||||||
|
country?: string
|
||||||
|
}): InputElement<string> {
|
||||||
|
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<string> = 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<number> {
|
public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined): InputElement<number> {
|
||||||
|
@ -179,13 +262,20 @@ export default class ValidatedTextField {
|
||||||
|
|
||||||
static Mapped<T>(fromString: (str) => T, toString: (T) => string, options?: {
|
static Mapped<T>(fromString: (str) => T, toString: (T) => string, options?: {
|
||||||
placeholder?: string | UIElement,
|
placeholder?: string | UIElement,
|
||||||
|
type?: string,
|
||||||
value?: UIEventSource<string>,
|
value?: UIEventSource<string>,
|
||||||
startValidated?: boolean,
|
startValidated?: boolean,
|
||||||
textArea?: boolean,
|
textArea?: boolean,
|
||||||
textAreaRows?: number,
|
textAreaRows?: number,
|
||||||
isValid?: ((string: string) => boolean)
|
isValid?: ((string: string) => boolean),
|
||||||
|
country?: string
|
||||||
}): InputElement<T> {
|
}): InputElement<T> {
|
||||||
const textField = new TextField(options);
|
let textField: InputElement<string>;
|
||||||
|
if (options.type) {
|
||||||
|
textField = ValidatedTextField.InputForType(options.type, options);
|
||||||
|
} else {
|
||||||
|
textField = new TextField(options);
|
||||||
|
}
|
||||||
return new InputElementMap(
|
return new InputElementMap(
|
||||||
textField, (a, b) => a === b,
|
textField, (a, b) => a === b,
|
||||||
fromString, toString
|
fromString, toString
|
||||||
|
|
|
@ -64,7 +64,9 @@ export class ShareScreen extends UIElement {
|
||||||
if (State.state !== undefined) {
|
if (State.state !== undefined) {
|
||||||
|
|
||||||
const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = (State.state.bm as Basemap).CurrentLayer;
|
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(
|
const includeCurrentBackground = new CheckBox(
|
||||||
new Combine([Img.checkmark, currentBackground]),
|
new Combine([Img.checkmark, currentBackground]),
|
||||||
new Combine([Img.no_checkmark, currentBackground]),
|
new Combine([Img.no_checkmark, currentBackground]),
|
||||||
|
@ -143,9 +145,13 @@ export class ShareScreen extends UIElement {
|
||||||
|
|
||||||
let hash = "";
|
let hash = "";
|
||||||
if (layoutDefinition !== undefined) {
|
if (layoutDefinition !== undefined) {
|
||||||
hash = ("#" + layoutDefinition)
|
|
||||||
literalText = "https://pietervdvn.github.io/MapComplete/index.html"
|
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);
|
}, 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(
|
this._iframeCode = new VariableUiElement(
|
||||||
url.map((url) => {
|
url.map((url) => {
|
||||||
return `<span class='literal-code iframe-code-block'>
|
return `<span class='literal-code iframe-code-block'>
|
||||||
<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>
|
||||||
</span>`
|
</span>`
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -298,7 +298,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private InputForFreeForm(freeform : {
|
private InputForFreeForm(freeform: {
|
||||||
key: string,
|
key: string,
|
||||||
template: string | Translation,
|
template: string | Translation,
|
||||||
renderTemplate: string | Translation,
|
renderTemplate: string | Translation,
|
||||||
|
@ -313,7 +313,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
|
||||||
.replace("$$$", "$string$")
|
.replace("$$$", "$string$")
|
||||||
.split("$");
|
.split("$");
|
||||||
let type = prepost[1];
|
let type = prepost[1];
|
||||||
|
|
||||||
let isTextArea = false;
|
let isTextArea = false;
|
||||||
if(type === "text"){
|
if(type === "text"){
|
||||||
isTextArea = true;
|
isTextArea = true;
|
||||||
|
@ -325,27 +325,14 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
|
||||||
throw "Unkown type: "+type;
|
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 =
|
const pickString =
|
||||||
(string: any) => {
|
(string: any) => {
|
||||||
if (string === "" || string === undefined) {
|
if (string === "" || string === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (!isValid(string, this._source.data._country)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const tag = new Tag(freeform.key, string);
|
||||||
const tag = new Tag(freeform.key, formatter(string, this._source.data._country));
|
|
||||||
|
|
||||||
if (tag.value.length > 255) {
|
|
||||||
return undefined; // Too long
|
|
||||||
}
|
|
||||||
|
|
||||||
if (freeform.extraTags === undefined) {
|
if (freeform.extraTags === undefined) {
|
||||||
return tag;
|
return tag;
|
||||||
|
@ -371,9 +358,16 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
|
||||||
}
|
}
|
||||||
return undefined;
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ export class UserBadge extends UIElement {
|
||||||
|
|
||||||
let dryrun = "";
|
let dryrun = "";
|
||||||
if (user.dryRun) {
|
if (user.dryRun) {
|
||||||
dryrun = " <span class='alert'>TESTING</span>";
|
dryrun = new FixedUiElement("TESTING").SetClass("alert").Render();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.home !== undefined) {
|
if (user.home !== undefined) {
|
||||||
|
@ -98,7 +98,7 @@ export class UserBadge extends UIElement {
|
||||||
const settings =
|
const settings =
|
||||||
"<a href='https://www.openstreetmap.org/user/" + encodeURIComponent(user.name) + "/account' target='_blank'>" +
|
"<a href='https://www.openstreetmap.org/user/" + encodeURIComponent(user.name) + "/account' target='_blank'>" +
|
||||||
"<img class='small-userbadge-icon' src='./assets/gear.svg' alt='settings'>" +
|
"<img class='small-userbadge-icon' src='./assets/gear.svg' alt='settings'>" +
|
||||||
"</a> ";
|
"</a>";
|
||||||
|
|
||||||
const userIcon = "<a href='https://www.openstreetmap.org/user/" + encodeURIComponent(user.name) + "' target='_blank'><img id='profile-pic' src='" + user.img + "' alt='profile-pic'/></a>";
|
const userIcon = "<a href='https://www.openstreetmap.org/user/" + encodeURIComponent(user.name) + "' target='_blank'><img id='profile-pic' src='" + user.img + "' alt='profile-pic'/></a>";
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,8 @@
|
||||||
"nl": "Is deze drinkwaterkraan nog steeds werkende?"
|
"nl": "Is deze drinkwaterkraan nog steeds werkende?"
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"en": "The operational status is <i>{operational_status</i>"
|
"en": "The operational status is <i>{operational_status</i>",
|
||||||
|
"nl": "Deze waterkraan-status is <i>{operational_status}</i>"
|
||||||
},
|
},
|
||||||
"freeform": {
|
"freeform": {
|
||||||
"key": "operational_status"
|
"key": "operational_status"
|
||||||
|
|
|
@ -1,44 +1,73 @@
|
||||||
<svg width="98" height="122" viewBox="0 0 98 122" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<path d="M53.0445 112.094C51.2614 115.981 45.7386 115.981 43.9555 112.094L13.2124 45.085C11.6928 41.7729 14.1129 38 17.7569 38L79.2431 38C82.8871 38 85.3072 41.7729 83.7876 45.085L53.0445 112.094Z" fill="#171615"/>
|
<svg
|
||||||
<circle cx="49" cy="49" r="49" fill="#171615"/>
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
<g filter="url(#filter0_d)">
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
<path d="M56.4468 12.3002H49.8793V6.79007C49.8793 6.35374 49.5103 6 49.0552 6H47.2158C46.7606 6 46.3916 6.35374 46.3916 6.79007V12.3002H39.8242C39.369 12.3002 39 12.654 39 13.0903V14.8536C39 15.29 39.369 15.6437 39.8242 15.6437H46.3916V32.2099C46.3916 32.6463 46.7606 33 47.2158 33H49.0552C49.5103 33 49.8793 32.6463 49.8793 32.2099V15.6437H56.4468C56.902 15.6437 57.271 15.29 57.271 14.8536V13.0903C57.271 12.654 56.902 12.3002 56.4468 12.3002Z" fill="#FFFCFC"/>
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
</g>
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
<g filter="url(#filter1_d)">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path d="M45.0763 63.8434H29.7114L38.0923 45.2455H46.4731H52.526H59.0444M59.0444 45.2455L56.7164 40H59.9756H63.2348M59.0444 45.2455L59.9756 47.6299L61.838 51.4448L66.9596 63.8434M59.0444 45.2455L54.6212 52.3985L51.9672 56.6903M50.1979 59.5516L51.9672 56.6903M41.3515 50.4911L46.0075 60.9822L36.6955 40L39.4891 45.7224M48.8011 61.9359L51.9672 56.6903" stroke="white" stroke-width="2"/>
|
id="svg69"
|
||||||
<path d="M50.0922 63.032C50.0922 64.6108 48.8456 65.8701 47.3329 65.8701C45.8203 65.8701 44.5737 64.6108 44.5737 63.032C44.5737 61.4533 45.8203 60.194 47.3329 60.194C48.8456 60.194 50.0922 61.4533 50.0922 63.032Z" stroke="white"/>
|
version="1.1"
|
||||||
<path d="M40.2801 62.0784C40.2801 68.1329 35.494 73 29.6401 73C23.7861 73 19 68.1329 19 62.0784C19 56.0238 23.7861 51.1567 29.6401 51.1567C35.494 51.1567 40.2801 56.0238 40.2801 62.0784Z" stroke="white" stroke-width="2"/>
|
fill="none"
|
||||||
</g>
|
viewBox="0 0 98 122"
|
||||||
<g filter="url(#filter2_d)">
|
height="122"
|
||||||
<circle cx="66" cy="63" r="11" stroke="white" stroke-width="2"/>
|
width="98">
|
||||||
</g>
|
|
||||||
<defs>
|
<defs
|
||||||
<filter id="filter0_d" x="35" y="6" width="26.271" height="35" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
id="defs17" />
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
<metadata
|
||||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
id="metadata73">
|
||||||
<feOffset dy="4"/>
|
<rdf:RDF>
|
||||||
<feGaussianBlur stdDeviation="2"/>
|
<cc:Work
|
||||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
rdf:about="">
|
||||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
<dc:format>image/svg+xml</dc:format>
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
<dc:type
|
||||||
</filter>
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
<filter id="filter1_d" x="14" y="39" width="64.12" height="43" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
<dc:title></dc:title>
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
</cc:Work>
|
||||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
</rdf:RDF>
|
||||||
<feOffset dy="4"/>
|
</metadata>
|
||||||
<feGaussianBlur stdDeviation="2"/>
|
<g
|
||||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
id="layer3">
|
||||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
<g
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
id="g875">
|
||||||
</filter>
|
<path
|
||||||
<filter id="filter2_d" x="50" y="51" width="32" height="32" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
style="fill:#171615"
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
d="m 53.0445,112.094 c -1.7831,3.887 -7.3059,3.887 -9.089,0 L 13.2124,45.085 C 11.6928,41.7729 14.1129,38 17.7569,38 h 61.4862 c 3.644,0 6.0641,3.7729 4.5445,7.085 z"
|
||||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
id="path2" />
|
||||||
<feOffset dy="4"/>
|
<circle
|
||||||
<feGaussianBlur stdDeviation="2"/>
|
style="fill:#171615"
|
||||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
cx="49"
|
||||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
cy="49"
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
r="49"
|
||||||
</filter>
|
id="circle4" />
|
||||||
</defs>
|
<path
|
||||||
|
style="stroke:#ffffff;stroke-width:2;"
|
||||||
|
id="path10"
|
||||||
|
d="M 45.0763,63.8434 H 29.7114 l 8.3809,-18.5979 h 8.3808 6.0529 6.5184 m 0,0 L 56.7164,40 h 3.2592 3.2592 m -4.1904,5.2455 0.9312,2.3844 1.8624,3.8149 5.1216,12.3986 m -7.9152,-18.5979 -4.4232,7.153 -2.654,4.2918 m -1.7693,2.8613 1.7693,-2.8613 M 41.3515,50.4911 46.0075,60.9822 36.6955,40 l 2.7936,5.7224 m 9.312,16.2135 3.1661,-5.2456" />
|
||||||
|
<path
|
||||||
|
style="stroke:#ffffff;"
|
||||||
|
id="path12"
|
||||||
|
d="m 50.0922,63.032 c 0,1.5788 -1.2466,2.8381 -2.7593,2.8381 -1.5126,0 -2.7592,-1.2593 -2.7592,-2.8381 0,-1.5787 1.2466,-2.838 2.7592,-2.838 1.5127,0 2.7593,1.2593 2.7593,2.838 z" />
|
||||||
|
<path
|
||||||
|
style="stroke:#ffffff;stroke-width:2;"
|
||||||
|
id="path14"
|
||||||
|
d="M 40.2801,62.0784 C 40.2801,68.1329 35.494,73 29.6401,73 23.7861,73 19,68.1329 19,62.0784 c 0,-6.0546 4.7861,-10.9217 10.6401,-10.9217 5.8539,0 10.64,4.8671 10.64,10.9217 z" />
|
||||||
|
<g
|
||||||
|
id="g20">
|
||||||
|
<circle
|
||||||
|
cx="66"
|
||||||
|
cy="63"
|
||||||
|
r="11"
|
||||||
|
id="circle18"
|
||||||
|
style="stroke:#ffffff;stroke-width:2" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g8">
|
||||||
|
<path
|
||||||
|
d="M56.4468 12.3002H49.8793V6.79007C49.8793 6.35374 49.5103 6 49.0552 6H47.2158C46.7606 6 46.3916 6.35374 46.3916 6.79007V12.3002H39.8242C39.369 12.3002 39 12.654 39 13.0903V14.8536C39 15.29 39.369 15.6437 39.8242 15.6437H46.3916V32.2099C46.3916 32.6463 46.7606 33 47.2158 33H49.0552C49.5103 33 49.8793 32.6463 49.8793 32.2099V15.6437H56.4468C56.902 15.6437 57.271 15.29 57.271 14.8536V13.0903C57.271 12.654 56.902 12.3002 56.4468 12.3002Z"
|
||||||
|
id="path6"
|
||||||
|
fill="#FFFCFC" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.8 KiB |
|
@ -18,8 +18,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"en": "",
|
"en": "A map, meant for tourists which is permanently installed in the public space",
|
||||||
"nl": ""
|
"nl": "Een permantent geinstalleerde kaart"
|
||||||
},
|
},
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
{
|
{
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 45 KiB |
|
@ -23,10 +23,8 @@
|
||||||
{
|
{
|
||||||
"id": "Toilet",
|
"id": "Toilet",
|
||||||
"name": {
|
"name": {
|
||||||
"render": {
|
"en": "Toilets",
|
||||||
"en": "Toilets",
|
"de": "Toiletten"
|
||||||
"de": "Toiletten"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"overpassTags": "amenity=toilets",
|
"overpassTags": "amenity=toilets",
|
||||||
"title": {
|
"title": {
|
||||||
|
@ -59,10 +57,8 @@
|
||||||
"amenity=toilets"
|
"amenity=toilets"
|
||||||
],
|
],
|
||||||
"description": {
|
"description": {
|
||||||
"render": {
|
|
||||||
"en": "A publicly accessible toilet or restroom",
|
"en": "A publicly accessible toilet or restroom",
|
||||||
"de": "Eine öffentlich zugängliche Toilette"
|
"de": "Eine öffentlich zugängliche Toilette"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -13,17 +13,13 @@ import Translations from "./UI/i18n/Translations";
|
||||||
import {TagRendering} from "./UI/TagRendering";
|
import {TagRendering} from "./UI/TagRendering";
|
||||||
|
|
||||||
TagRendering.injectFunction();
|
TagRendering.injectFunction();
|
||||||
|
|
||||||
|
|
||||||
console.log("Building the layouts")
|
console.log("Building the layouts")
|
||||||
|
|
||||||
|
|
||||||
function enc(str: string): string {
|
function enc(str: string): string {
|
||||||
return encodeURIComponent(str.toLowerCase());
|
return encodeURIComponent(str.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate(layout: Layout) {
|
function validate(layout: Layout) {
|
||||||
console.log("Validationg ", layout.id)
|
|
||||||
const translations: Translation[] = [];
|
const translations: Translation[] = [];
|
||||||
const queue: any[] = [layout]
|
const queue: any[] = [layout]
|
||||||
|
|
||||||
|
@ -49,10 +45,13 @@ function validate(layout: Layout) {
|
||||||
missing[ln] = 0;
|
missing[ln] = 0;
|
||||||
present[ln] = 0;
|
present[ln] = 0;
|
||||||
for (const translation of translations) {
|
for (const translation of translations) {
|
||||||
|
if (translation.translations["*"] !== undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const txt = translation.translations[ln];
|
const txt = translation.translations[ln];
|
||||||
const isMissing = txt === undefined || txt === "" || txt.toLowerCase().indexOf("todo") >= 0;
|
const isMissing = txt === undefined || txt === "" || txt.toLowerCase().indexOf("todo") >= 0;
|
||||||
if (isMissing) {
|
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]++
|
missing[ln]++
|
||||||
} else {
|
} else {
|
||||||
present[ln]++;
|
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) {
|
for (const ln of layout.supportedLanguages) {
|
||||||
const amiss = missing[ln];
|
const amiss = missing[ln];
|
||||||
const ok = present[ln];
|
const ok = present[ln];
|
||||||
const total = amiss + ok;
|
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)
|
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{
|
} catch (e) {
|
||||||
|
console.error("Could not read icon", iconPath, "due to", e)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newname;
|
return newname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +161,6 @@ function createManifest(layout: Layout, relativePath: string) {
|
||||||
const icons = [];
|
const icons = [];
|
||||||
|
|
||||||
let icon = layout.icon;
|
let icon = layout.icon;
|
||||||
console.log(icon)
|
|
||||||
if (icon.endsWith(".svg")) {
|
if (icon.endsWith(".svg")) {
|
||||||
// This is an svg. Lets create the needed pngs!
|
// This is an svg. Lets create the needed pngs!
|
||||||
const sizes = [72, 96, 120, 128, 144, 152, 180, 192, 384, 512];
|
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];
|
const layout = all[layoutName];
|
||||||
validate(layout)
|
validate(layout)
|
||||||
console.log("Generating manifest")
|
|
||||||
const manif = JSON.stringify(createManifest(layout, "/MapComplete"));
|
const manif = JSON.stringify(createManifest(layout, "/MapComplete"));
|
||||||
|
|
||||||
const manifestLocation = encodeURIComponent(layout.id.toLowerCase()) + ".webmanifest";
|
const manifestLocation = encodeURIComponent(layout.id.toLowerCase()) + ".webmanifest";
|
||||||
writeFile(manifestLocation, manif, err);
|
writeFile(manifestLocation, manif, err);
|
||||||
|
|
||||||
const landing = createLandingPage(layout);
|
const landing = createLandingPage(layout);
|
||||||
console.log("Generating html-file for ", layout.id)
|
|
||||||
writeFile(enc(layout.id) + ".html", landing, err)
|
writeFile(enc(layout.id) + ".html", landing, err)
|
||||||
console.log("done")
|
|
||||||
|
|
||||||
wikiPage += "\n\n"+generateWikiEntry(layout);
|
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")
|
console.log("Counting all translations")
|
||||||
Translations.CountTranslations();
|
Translations.CountTranslations();
|
||||||
console.log("All done!");
|
console.log("All done!");
|
|
@ -9,6 +9,8 @@ ts-node createLayouts.ts || { echo 'Creating layouts failed' ; exit 1; }
|
||||||
find -name '*.png' | parallel optipng '{}'
|
find -name '*.png' | parallel optipng '{}'
|
||||||
npm run build || { echo 'Npm build failed' ; exit 1; }
|
npm run build || { echo 'Npm build failed' ; exit 1; }
|
||||||
|
|
||||||
|
rm -rf .cache
|
||||||
|
|
||||||
if [[ $1 == "groen" ]]
|
if [[ $1 == "groen" ]]
|
||||||
then
|
then
|
||||||
echo "DEPLOYING TO BUURTNATUUR!"
|
echo "DEPLOYING TO BUURTNATUUR!"
|
||||||
|
|
|
@ -71,6 +71,8 @@ body {
|
||||||
|
|
||||||
.invalid {
|
.invalid {
|
||||||
box-shadow: 0 0 10px #ff5353;
|
box-shadow: 0 0 10px #ff5353;
|
||||||
|
display: block;
|
||||||
|
height: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow {
|
.shadow {
|
||||||
|
|
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -4636,9 +4636,10 @@
|
||||||
"integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ=="
|
"integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ=="
|
||||||
},
|
},
|
||||||
"node-forge": {
|
"node-forge": {
|
||||||
"version": "0.7.6",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
|
||||||
"integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw=="
|
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node-libs-browser": {
|
"node-libs-browser": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
|
@ -5218,7 +5219,6 @@
|
||||||
"json5": "^1.0.1",
|
"json5": "^1.0.1",
|
||||||
"micromatch": "^3.0.4",
|
"micromatch": "^3.0.4",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"node-forge": "^0.7.1",
|
|
||||||
"node-libs-browser": "^2.0.0",
|
"node-libs-browser": "^2.0.0",
|
||||||
"opn": "^5.1.0",
|
"opn": "^5.1.0",
|
||||||
"postcss": "^7.0.11",
|
"postcss": "^7.0.11",
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
"repository": "https://github.com/pietervdvn/MapComplete",
|
"repository": "https://github.com/pietervdvn/MapComplete",
|
||||||
"description": "A small website to edit OSM easily",
|
"description": "A small website to edit OSM easily",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"disabled:staticFiles": {
|
"staticFiles": {
|
||||||
"staticPath": [
|
"staticPath": [
|
||||||
{
|
{
|
||||||
"staticPath": "tiles",
|
"staticPath": "tiles/",
|
||||||
"staticOutDir": "./tiles/"
|
"staticOutDir": "./tiles/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
2
test.ts
2
test.ts
|
@ -2,4 +2,4 @@ import AvailableBaseLayers from "./Logic/AvailableBaseLayers";
|
||||||
|
|
||||||
|
|
||||||
const layers = AvailableBaseLayers.AvailableLayersAt(51.2,3.2);
|
const layers = AvailableBaseLayers.AvailableLayersAt(51.2,3.2);
|
||||||
console.log(layers);
|
console.log(layers);
|
||||||
|
|
204
wikiIndex
204
wikiIndex
|
@ -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 <b>ghost bike</b> 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
|
|
||||||
|
|
|
||||||
}}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue