Merge branch 'master' of github.com:pietervdvn/MapComplete

This commit is contained in:
Pieter Fiers 2020-07-29 16:03:16 +02:00
commit c6a80034ed
18 changed files with 722 additions and 155 deletions

View file

@ -16,7 +16,7 @@ export class Layout {
public name: string; public name: string;
public icon: string = "./assets/logo.svg"; public icon: string = "./assets/logo.svg";
public title: UIElement; public title: UIElement;
public description: string | UIElement = Translations.t.general.about; public description: string | UIElement;
public socialImage: string = "" public socialImage: string = ""
public layers: LayerDefinition[]; public layers: LayerDefinition[];
@ -103,22 +103,13 @@ export class WelcomeMessage extends UIElement {
} }
InnerRender(): string { InnerRender(): string {
return "<span id='welcomeMessage'>" + return "<span>" +
this.description.Render() + this.description.Render() +
(this.userDetails.data.loggedIn ? this.welcomeBack : this.plzLogIn).Render() + (this.userDetails.data.loggedIn ? this.welcomeBack : this.plzLogIn).Render() +
this.tail.Render() + this.tail.Render() +
"<br/>" + "<br/>" +
this.languagePicker.Render() + this.languagePicker.Render() +
"</span>" "</span>";
;
/*
return new VariableUiElement(
this.userDetails.map((userdetails) => {
}),
function () {
}).ListenTo(Locale.language);*/
} }
protected InnerUpdate(htmlElement: HTMLElement) { protected InnerUpdate(htmlElement: HTMLElement) {

75
InitUiElements.ts Normal file
View file

@ -0,0 +1,75 @@
import {Layout, WelcomeMessage} from "./Customizations/Layout";
import Locale from "./UI/i18n/Locale";
import Translations from "./UI/i18n/Translations";
import {TabbedComponent} from "./UI/Base/TabbedComponent";
import {ShareScreen} from "./UI/ShareScreen";
import {FixedUiElement} from "./UI/Base/FixedUiElement";
import {CheckBox} from "./UI/Input/CheckBox";
import Combine from "./UI/Base/Combine";
import {OsmConnection} from "./Logic/OsmConnection";
import {Basemap} from "./Logic/Basemap";
import {UIEventSource} from "./UI/UIEventSource";
import {UIElement} from "./UI/UIElement";
import {MoreScreen} from "./UI/MoreScreen";
export class InitUiElements {
static OnlyIf(featureSwitch: UIEventSource<string>, callback: () => void) {
featureSwitch.addCallback(() => {
if (featureSwitch.data === "false") {
return;
}
callback();
});
if (featureSwitch.data !== "false") {
callback();
}
}
private static CreateWelcomePane(layoutToUse: Layout, osmConnection: OsmConnection, bm: Basemap) {
const welcome = new WelcomeMessage(layoutToUse, Locale.CreateLanguagePicker(layoutToUse, Translations.t.general.pickLanguage), osmConnection)
const fullOptions = new TabbedComponent([
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
{header: `<img src='${'./assets/osm-logo.svg'}'>`, content: Translations.t.general.openStreetMapIntro},
{header: `<img src='${'./assets/share.svg'}'>`, content: new ShareScreen(layoutToUse, bm.Location)},
{header: `<img src='${'./assets/add.svg'}'>`, content: new MoreScreen(bm.Location)}
])
return fullOptions;
}
static InitWelcomeMessage(layoutToUse: Layout, osmConnection: OsmConnection, bm: Basemap,
fullScreenMessage: UIEventSource<UIElement>) {
const fullOptions = this.CreateWelcomePane(layoutToUse, osmConnection, bm);
const help = new FixedUiElement(`<div class='collapse-button-img'><img src='assets/help.svg' alt='help'></div>`);
const close = new FixedUiElement(`<div class='collapse-button-img'><img src='assets/close.svg' alt='close'></div>`);
new CheckBox(
new Combine([
"<span class='collapse-button'>", close, "</span>",
"<span id='welcomeMessage'>", fullOptions.onClick(() => {
}), "</span>"]),
new Combine(["<span class='open-button'>", help, "</span>"])
, true
).AttachTo("messagesbox");
const fullOptions2 = this.CreateWelcomePane(layoutToUse, osmConnection, bm);
fullScreenMessage.setData(fullOptions2)
new FixedUiElement(`<div class='collapse-button-img' class="shadow"><img src='assets/help.svg' alt='help'></div>`).onClick(() => {
fullScreenMessage.setData(fullOptions2)
}).AttachTo("help-button-mobile");
}
}

View file

@ -7,10 +7,10 @@ export class QueryParameters {
private static order: string [] = ["layout","test","z","lat","lon"]; private static order: string [] = ["layout","test","z","lat","lon"];
private static knownSources = QueryParameters.init(); private static knownSources = QueryParameters.init();
private static defaults = {}
private static addOrder(key){ private static addOrder(key){
if(this.order.indexOf(key) < 0){ if(this.order.indexOf(key) < 0){
console.log("Adding order", key)
this.order.push(key) this.order.push(key)
} }
} }
@ -41,18 +41,22 @@ export class QueryParameters {
if (QueryParameters.knownSources[key] === undefined || QueryParameters.knownSources[key].data === undefined) { if (QueryParameters.knownSources[key] === undefined || QueryParameters.knownSources[key].data === undefined) {
continue; continue;
} }
if (QueryParameters.knownSources[key].data == QueryParameters.defaults[key]) {
continue;
}
parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data)) parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data))
} }
history.replaceState(null, "", "?" + parts.join("&")); history.replaceState(null, "", "?" + parts.join("&"));
} }
public static GetQueryParameter(key: string): UIEventSource<string> { public static GetQueryParameter(key: string, deflt: string): UIEventSource<string> {
QueryParameters.defaults[key] = deflt;
if (QueryParameters.knownSources[key] !== undefined) { if (QueryParameters.knownSources[key] !== undefined) {
return QueryParameters.knownSources[key]; return QueryParameters.knownSources[key];
} }
QueryParameters.addOrder(key); QueryParameters.addOrder(key);
const source = new UIEventSource<string>(undefined); const source = new UIEventSource<string>(deflt);
QueryParameters.knownSources[key] = source; QueryParameters.knownSources[key] = source;
source.addCallback(() => QueryParameters.Serialize()) source.addCallback(() => QueryParameters.Serialize())
return source; return source;

View file

@ -0,0 +1,42 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource";
import Translations from "../i18n/Translations";
export class TabbedComponent extends UIElement {
private headers: UIElement[] = [];
private content: UIElement[] = [];
constructor(elements: { header: UIElement | string, content: UIElement | string }[]) {
super(new UIEventSource<number>(0));
const self = this;
for (let i = 0; i < elements.length; i++) {
let element = elements[i];
this.headers.push(Translations.W(element.header).onClick(() => self._source.setData(i)));
this.content.push(Translations.W(element.content));
}
}
InnerRender(): string {
let html = "";
let headerBar = "";
for (let i = 0; i < this.headers.length; i++) {
let header = this.headers[i];
headerBar += `<div class=\'tab-single-header ${i == this._source.data ? 'tab-active' : 'tab-non-active'}\'>` +
header.Render() + "</div>"
}
headerBar = "<div class='tabs-header-bar'>" + headerBar + "</div>"
const content = this.content[this._source.data].Render();
return headerBar + "<div class='tab-content'>" + content + "</div>";
}
}

View file

@ -10,7 +10,7 @@ export class VerticalCombine extends UIElement {
this._className = className; this._className = className;
} }
protected InnerRender(): string { InnerRender(): string {
let html = ""; let html = "";
for (const element of this._elements) { for (const element of this._elements) {
if (!element.IsEmpty()) { if (!element.IsEmpty()) {

View file

@ -1,4 +1,6 @@
export class Img { export class Img {
static readonly checkmark = `<svg width="26" height="18" viewBox="0 0 26 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 7.28571L10.8261 15L23 3" stroke="black" stroke-width="4" stroke-linejoin="round"/></svg>`;
static readonly no_checkmark = `<svg width="26" height="18" viewBox="0 0 26 18" fill="none" xmlns="http://www.w3.org/2000/svg"></svg>`;
static osmAbstractLogo: string = static osmAbstractLogo: string =
"<svg class='osm-logo' xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" width=\"24px\" version=\"1.1\" viewBox=\"0 0 66 64\">" + "<svg class='osm-logo' xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" width=\"24px\" version=\"1.1\" viewBox=\"0 0 66 64\">" +
@ -16,6 +18,7 @@ export class Img {
<path d="M26.3988 13.7412C26.2956 13.9661 26.1026 14.081 25.8927 14.1924C21.8198 16.3577 17.749 18.5258 13.6815 20.7013C13.492 20.8025 13.3602 20.7902 13.1795 20.6938C9.09638 18.5114 5.01059 16.3359 0.924798 14.1582C0.399637 13.8786 0.307921 13.2646 0.735251 12.838C0.829005 12.7443 0.947217 12.6705 1.06407 12.6055C1.56545 12.3279 2.07635 12.0654 2.57297 11.7789C2.74214 11.6812 2.86579 11.6921 3.03291 11.7817C6.27492 13.5155 9.52303 15.2378 12.761 16.9792C13.2352 17.2343 13.6394 17.2322 14.1129 16.9772C17.3509 15.2358 20.5996 13.5142 23.8416 11.7796C24.0095 11.69 24.1338 11.6818 24.3016 11.7789C24.7384 12.0339 25.1821 12.2794 25.6352 12.5037C25.9701 12.6691 26.2426 12.8831 26.3995 13.2304C26.3988 13.4014 26.3988 13.5716 26.3988 13.7412Z" fill="#003B8B"/> <path d="M26.3988 13.7412C26.2956 13.9661 26.1026 14.081 25.8927 14.1924C21.8198 16.3577 17.749 18.5258 13.6815 20.7013C13.492 20.8025 13.3602 20.7902 13.1795 20.6938C9.09638 18.5114 5.01059 16.3359 0.924798 14.1582C0.399637 13.8786 0.307921 13.2646 0.735251 12.838C0.829005 12.7443 0.947217 12.6705 1.06407 12.6055C1.56545 12.3279 2.07635 12.0654 2.57297 11.7789C2.74214 11.6812 2.86579 11.6921 3.03291 11.7817C6.27492 13.5155 9.52303 15.2378 12.761 16.9792C13.2352 17.2343 13.6394 17.2322 14.1129 16.9772C17.3509 15.2358 20.5996 13.5142 23.8416 11.7796C24.0095 11.69 24.1338 11.6818 24.3016 11.7789C24.7384 12.0339 25.1821 12.2794 25.6352 12.5037C25.9701 12.6691 26.2426 12.8831 26.3995 13.2304C26.3988 13.4014 26.3988 13.5716 26.3988 13.7412Z" fill="#003B8B"/>
</svg> ` </svg> `
static openFilterButton: string = `<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"> static openFilterButton: string = `<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 2L2 20M20 20L2 2" stroke="#003B8B" stroke-width="4"/> <path d="M20 2L2 20M20 20L2 2" stroke="#003B8B" stroke-width="4"/>
</svg> ` </svg> `

View file

@ -6,26 +6,26 @@ import instantiate = WebAssembly.instantiate;
export class CheckBox extends UIElement{ export class CheckBox extends UIElement{
private readonly _data: UIEventSource<boolean>; public readonly isEnabled: UIEventSource<boolean>;
private readonly _showEnabled: string|UIElement; private readonly _showEnabled: string|UIElement;
private readonly _showDisabled: string|UIElement; private readonly _showDisabled: string|UIElement;
constructor(showEnabled: string | UIElement, showDisabled: string | UIElement, data: UIEventSource<boolean> | boolean = false) { constructor(showEnabled: string | UIElement, showDisabled: string | UIElement, data: UIEventSource<boolean> | boolean = false) {
super(undefined); super(undefined);
this._data = this.isEnabled =
data instanceof UIEventSource ? data : new UIEventSource(data ?? false); data instanceof UIEventSource ? data : new UIEventSource(data ?? false);
this.ListenTo(this._data); this.ListenTo(this.isEnabled);
this._showEnabled = showEnabled; this._showEnabled = showEnabled;
this._showDisabled = showDisabled; this._showDisabled = showDisabled;
const self = this; const self = this;
this.onClick(() => { this.onClick(() => {
self._data.setData(!self._data.data); self.isEnabled.setData(!self.isEnabled.data);
}) })
} }
InnerRender(): string { InnerRender(): string {
if (this._data.data) { if (this.isEnabled.data) {
return Translations.W(this._showEnabled).Render(); return Translations.W(this._showEnabled).Render();
} else { } else {
return Translations.W(this._showDisabled).Render(); return Translations.W(this._showDisabled).Render();

View file

@ -3,6 +3,7 @@ import { FilteredLayer } from "../Logic/FilteredLayer";
import { CheckBox } from "./Input/CheckBox"; import { CheckBox } from "./Input/CheckBox";
import Combine from "./Base/Combine"; import Combine from "./Base/Combine";
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import {Img} from "./Img";
export class LayerSelection extends UIElement{ export class LayerSelection extends UIElement{
@ -25,9 +26,7 @@ export class LayerSelection extends UIElement{
this._checkboxes.push(new CheckBox( this._checkboxes.push(new CheckBox(
new Combine([checkbox, icon, name]), new Combine([checkbox, icon, name]),
new Combine([ new Combine([
`<svg width="26" height="18" viewBox="0 0 26 18" fill="none" xmlns="http://www.w3.org/2000/svg"> Img.checkmark,
<path d="M3 7.28571L10.8261 15L23 3" stroke="#ffffff" stroke-width="4" stroke-linejoin="round"/>
</svg>`,
icon, icon,
layer.layerDef.name]), layer.layerDef.name]),
layer.isDisplayed)); layer.isDisplayed));

58
UI/MoreScreen.ts Normal file
View file

@ -0,0 +1,58 @@
import {UIElement} from "./UIElement";
import {VerticalCombine} from "./Base/VerticalCombine";
import Translations from "./i18n/Translations";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import {FixedUiElement} from "./Base/FixedUiElement";
import {Utils} from "../Utils";
import {link} from "fs";
import {UIEventSource} from "./UIEventSource";
import {VariableUiElement} from "./Base/VariableUIElement";
export class MoreScreen extends UIElement {
private currentLocation: UIEventSource<{ zoom: number, lat: number, lon: number }>;
constructor(currentLocation: UIEventSource<{ zoom: number, lat: number, lon: number }>) {
super(currentLocation);
this.currentLocation = currentLocation;
}
InnerRender(): string {
const tr = Translations.t.general.morescreen;
const els: UIElement[] = []
for (const k in AllKnownLayouts.allSets) {
if (k === "all") {
continue;
}
const layout = AllKnownLayouts.allSets[k]
const linkText =
`https://pietervdvn.github.io/MapComplete/${layout.name}.html?z=${this.currentLocation.data.zoom}&lat=${this.currentLocation.data.lat}&lon=${this.currentLocation.data.lon}
`
const link = new FixedUiElement(
`
<span class="switch-layout">
<a href="${linkText}" target="_blank">
<img src='${layout.icon}'>
<div><b>${Utils.Upper(layout.name)}</b><br/>
${Translations.W(layout.description).Render()}</div>
</a>
</span>
`
);
els.push(link)
}
return new VerticalCombine([
tr.intro,
new VerticalCombine(els),
tr.streetcomplete
]).Render();
}
}

160
UI/ShareScreen.ts Normal file
View file

@ -0,0 +1,160 @@
import {UIElement} from "./UIElement";
import {Layout} from "../Customizations/Layout";
import Translations from "./i18n/Translations";
import {FixedUiElement} from "./Base/FixedUiElement";
import Combine from "./Base/Combine";
import {VariableUiElement} from "./Base/VariableUIElement";
import {UIEventSource} from "./UIEventSource";
import {CheckBox} from "./Input/CheckBox";
import {VerticalCombine} from "./Base/VerticalCombine";
import {QueryParameters} from "../Logic/QueryParameters";
import {Img} from "./Img";
export class ShareScreen extends UIElement {
private _shareButton: UIElement;
private _options: UIElement;
private _iframeCode: UIElement;
private _link: UIElement;
private _linkStatus: UIElement;
constructor(layout: Layout, currentLocation: UIEventSource<{ zoom: number, lat: number, lon: number }>) {
super(undefined)
const tr = Translations.t.general.sharescreen;
const optionCheckboxes: UIElement[] = []
const optionParts: (UIEventSource<string>)[] = [];
const includeLocation = new CheckBox(
new Combine([Img.checkmark, "Include current location"]),
new Combine([Img.no_checkmark, "Include current location"]),
true
)
optionCheckboxes.push(includeLocation);
optionParts.push(includeLocation.isEnabled.map((includeL) => {
if (includeL) {
return `z=${currentLocation.data.zoom}&lat=${currentLocation.data.lat}&lon=${currentLocation.data.lon}`
} else {
return null;
}
}, [currentLocation]));
const switches = [{urlName: "fs-userbadge", human: "Enable the login-button"},
{urlName: "fs-search", human: "Enable search bar"},
{urlName: "fs-welcome-message", human: "Enable the welcome message"},
{urlName: "fs-layers", human: "Enable layer control"},
{urlName: "fs-add-new", human: "Enable the 'add new POI' button"}
]
for (const swtch of switches) {
const checkbox = new CheckBox(
new Combine([Img.checkmark, swtch.human]),
new Combine([Img.no_checkmark, swtch.human]),
true
);
optionCheckboxes.push(checkbox);
optionParts.push(checkbox.isEnabled.map((isEn) => {
if (isEn) {
return null;
} else {
return `${swtch.urlName}=false`
}
}))
}
this._options = new VerticalCombine(optionCheckboxes)
const url = currentLocation.map(() => {
let literalText = "https://pietervdvn.github.io/MapComplete/" + layout.name + ".html"
const parts = [];
for (const part of optionParts) {
if (part.data === null) {
continue;
}
parts.push(part.data);
}
if (parts.length === 0) {
return literalText;
}
return literalText + "?" + parts.join("&");
}, optionParts);
this._iframeCode = new VariableUiElement(
url.map((url) => {
return `<span class='literal-code iframe-code-block'>
&lt;iframe src="${url}" title="${layout.name} with MapComplete"&gt;&lt;/iframe&gt
</span>`
})
);
this._link = new VariableUiElement(
url.map((url) => {
return `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%"readonly>`
})
);
const status = new UIEventSource(" ");
this._linkStatus = new VariableUiElement(status);
const self = this;
this._link.onClick(async () => {
const shareData = {
title: Translations.W(layout.name).InnerRender(),
text: Translations.W(layout.description).InnerRender(),
url: self._link.data,
}
function rejected() {
status.setData("Copying to clipboard...")
var copyText = document.getElementById("code-link--copyable");
// @ts-ignore
copyText.select();
// @ts-ignore
copyText.setSelectionRange(0, 99999); /*For mobile devices*/
document.execCommand("copy");
status.setData("Copied to clipboard")
}
try {
navigator.share(shareData)
.then(() => {
status.setData("Thanks for sharing!")
}, rejected)
.catch(rejected)
} catch (err) {
rejected();
}
});
}
InnerRender(): string {
const tr = Translations.t.general.sharescreen;
return new VerticalCombine([
tr.intro,
this._link,
this._linkStatus,
tr.addToHomeScreen,
tr.embedIntro,
this._options,
this._iframeCode
]).Render()
}
}

View file

@ -136,6 +136,8 @@ export abstract class UIElement extends UIEventSource<string>{
public IsEmpty(): boolean { public IsEmpty(): boolean {
return this.InnerRender() === ""; return this.InnerRender() === "";
} }
} }

View file

@ -160,7 +160,11 @@ export default class Translations {
// title: new T({en: 'Bike station', nl: 'Fietsstation', fr: 'Station vélo'}), Old, non-dynamic title // title: new T({en: 'Bike station', nl: 'Fietsstation', fr: 'Station vélo'}), Old, non-dynamic title
titlePump: new T({en: 'Bike pump', nl: 'Fietspomp', fr: 'TODO: fr'}), titlePump: new T({en: 'Bike pump', nl: 'Fietspomp', fr: 'TODO: fr'}),
titleRepair: new T({en: 'Bike repair station', nl: 'Herstelpunt', fr: 'TODO: fr'}), titleRepair: new T({en: 'Bike repair station', nl: 'Herstelpunt', fr: 'TODO: fr'}),
titlePumpAndRepair: new T({en: 'Bike station (pump & repair)', nl: 'Herstelpunt met pomp', fr: 'TODO: fr'}), titlePumpAndRepair: new T({
en: 'Bike station (pump & repair)',
nl: 'Herstelpunt met pomp',
fr: 'TODO: fr'
}),
manometer: { manometer: {
question: new T({ question: new T({
en: 'Does the pump have a pressure indicator or manometer?', en: 'Does the pump have a pressure indicator or manometer?',
@ -168,7 +172,11 @@ export default class Translations {
fr: 'Est-ce que la pompe à un manomètre integré?' fr: 'Est-ce que la pompe à un manomètre integré?'
}), }),
yes: new T({en: 'There is a manometer', nl: 'Er is een luchtdrukmeter', fr: 'Il y a un manomètre'}), yes: new T({en: 'There is a manometer', nl: 'Er is een luchtdrukmeter', fr: 'Il y a un manomètre'}),
no: new T({en: 'There is no manometer', nl: 'Er is geen luchtdrukmeter', fr: 'Il n\'y a pas de manomètre'}), no: new T({
en: 'There is no manometer',
nl: 'Er is geen luchtdrukmeter',
fr: 'Il n\'y a pas de manomètre'
}),
broken: new T({ broken: new T({
en: 'There is manometer but it is broken', en: 'There is manometer but it is broken',
nl: 'Er is een luchtdrukmeter maar die is momenteel defect', nl: 'Er is een luchtdrukmeter maar die is momenteel defect',
@ -301,10 +309,21 @@ export default class Translations {
titleRepair: new T({en: "Bike repair", nl: "Fietsenmaker", fr: "Réparateur de vélo"}), titleRepair: new T({en: "Bike repair", nl: "Fietsenmaker", fr: "Réparateur de vélo"}),
titleShop: new T({en: "Bike shop", nl: "Fietswinkel", fr: "Magasin de vélo"}), titleShop: new T({en: "Bike shop", nl: "Fietswinkel", fr: "Magasin de vélo"}),
titleNamed: new T({en: "Bike repair/shop {name}", nl: "Fietszaak {name}", fr: "Magasin et réparateur de vélo {name}"}), titleNamed: new T({
titleRepairNamed: new T({en: "Bike repair {name}", nl: "Fietsenmaker {name}", fr: "Réparateur de vélo {name}"}), en: "Bike repair/shop {name}",
titleShopNamed: new T({en: "Bike shop {name}", nl: "Fietswinkel {name}", fr: "Magasin de vélo {name}"}), nl: "Fietszaak {name}",
fr: "Magasin et réparateur de vélo {name}"
}),
titleRepairNamed: new T({
en: "Bike repair {name}",
nl: "Fietsenmaker {name}",
fr: "Réparateur de vélo {name}"
}),
titleShopNamed: new T({
en: "Bike shop {name}",
nl: "Fietswinkel {name}",
fr: "Magasin de vélo {name}"
}),
retail: { retail: {
@ -313,7 +332,11 @@ export default class Translations {
nl: "Verkoopt deze winkel fietsen?", nl: "Verkoopt deze winkel fietsen?",
fr: "Est-ce que ce magasin vend des vélos?" fr: "Est-ce que ce magasin vend des vélos?"
}), }),
yes: new T({en: "This shop sells bikes", nl: "Deze winkel verkoopt fietsen", fr: "Ce magasin vend des vélos"}), yes: new T({
en: "This shop sells bikes",
nl: "Deze winkel verkoopt fietsen",
fr: "Ce magasin vend des vélos"
}),
no: new T({ no: new T({
en: "This shop doesn't sell bikes", en: "This shop doesn't sell bikes",
nl: "Deze winkel verkoopt geen fietsen", nl: "Deze winkel verkoopt geen fietsen",
@ -451,15 +474,43 @@ export default class Translations {
} }
}, },
nonBikeShop: { nonBikeShop: {
name: new T({en: "shop that sells/repairs bikes", nl: "winkel die fietsen verkoopt/herstelt", fr: "TODO: fr"}), name: new T({
en: "shop that sells/repairs bikes",
nl: "winkel die fietsen verkoopt/herstelt",
fr: "TODO: fr"
}),
title: new T({en: "Shop that sells/repairs bikes", nl: "Winkel die fietsen verkoopt/herstelt", fr: "TODO: fr"}), title: new T({
titleRepair: new T({en: "Shop that repairs bikes", nl: "Winkel die fietsen herstelt", fr: "TODO: fr"}), en: "Shop that sells/repairs bikes",
titleShop: new T({en: "Shop that sells bikes", nl: "Winkel die fietsen verkoopt", fr: "TODO: fr"}), nl: "Winkel die fietsen verkoopt/herstelt",
fr: "TODO: fr"
}),
titleRepair: new T({
en: "Shop that repairs bikes",
nl: "Winkel die fietsen herstelt",
fr: "TODO: fr"
}),
titleShop: new T({
en: "Shop that sells bikes",
nl: "Winkel die fietsen verkoopt",
fr: "TODO: fr"
}),
titleNamed: new T({en: "{name} (sells/repairs bikes)", nl: "{name} (verkoopt/herstelt fietsen)", fr: "TODO: fr"}), titleNamed: new T({
titleRepairNamed: new T({en: "{name} (repairs bikes)", nl: "{name} (herstelt fietsen)", fr: "TODO: fr"}), en: "{name} (sells/repairs bikes)",
titleShopNamed: new T({en: "{name} (sells bikes)", nl: "{name} (verkoopt fietsen)", fr: "TODO: fr"}), nl: "{name} (verkoopt/herstelt fietsen)",
fr: "TODO: fr"
}),
titleRepairNamed: new T({
en: "{name} (repairs bikes)",
nl: "{name} (herstelt fietsen)",
fr: "TODO: fr"
}),
titleShopNamed: new T({
en: "{name} (sells bikes)",
nl: "{name} (verkoopt fietsen)",
fr: "TODO: fr"
}),
}, },
drinking_water: { drinking_water: {
title: new T({ title: new T({
@ -686,6 +737,43 @@ export default class Translations {
en: "Website: <a href='{website}' target='_blank'>{website}</a>", en: "Website: <a href='{website}' target='_blank'>{website}</a>",
nl: "Website: <a href='{website}' target='_blank'>{website}</a>" nl: "Website: <a href='{website}' target='_blank'>{website}</a>"
}) })
},
openStreetMapIntro: new T({
en: "<h3>An Open Map</h3>" +
"<p></p>Wouldn't it be cool if there was a single map, which everyone could freely use and edit?" +
"A single place to store all geo-information? Then, all those websites with different, small and incompatible maps (which are always outdated) wouldn't be needed anymore.</p>" +
"<p><b><a href='https://OpenStreetMap.org' target='_blank'>OpenStreetMap</a></b> is this map. The map data can be used for free (with <a href='https://osm.org/copyright' target='_blank'>attribution and publication of changes to that data</a>)." +
" On top of that, everyone can freely add new data and fix errors. This website uses OpenStreetMap as well. All the data is from there, and your answers and corrections are added there as well.</p>" +
"<p>A ton of people and application already use OpenStreetMap: <a href='https://maps.me/' traget='_blank'>Maps.me</a>, <a href='https://osmAnd.net' traget='_blank'>OsmAnd</a>, but also the maps at Facebook, Intsagram, Apple-maps and Bing-maps are (partly) powered by OpenStreetMap." +
"If you change something here, it'll be reflected in those applications too - after their next update!</p>",
nl: "<h3>Een open kaart</h3>" +
"<p>Zou het niet fantastisch zijn als er een open kaart zou zijn, die door iedereen aangepast én gebruikt kon worden? Waar iedereen zijn interesses aan zou kunnen toevoegen?" +
"Dan zouden er geen duizend-en-één verschillende kleine kaartjes, websites, ... meer nodig zijn</p>" +
"<p><b><a href='https://OpenStreetMap.org' target='_blank'>OpenStreetMap</a></b> is deze open kaart. Je mag de kaartdata gratis gebruiken (mits <a href='https://osm.org/copyright' target='_blank'>bronvermelding en herpublicatie van aanpassingen</a>). Daarenboven mag je de kaart ook gratis aanpassen als je een account maakt." +
"Ook deze website is gebaseerd op OpenStreetMap. Als je hier een vraag beantwoord, gaat het antwoord daar ook naartoe</p>" +
"<p>Tenslotte zijn er reeds vele gebruikers van OpenStreetMap. Denk maar <a href='https://maps.me/' traget='_blank'>Maps.me</a>, <a href='https://osmAnd.net' traget='_blank'>OsmAnd</a>, verschillende gespecialiseerde routeplanners, de achtergrondkaarten op Facebook, Instagram,...<br/> Zelfs Apple Maps en Bing-Maps gebruiken OpenStreetMap in hun kaarten!</p>" +
"<p></p>Kortom, als je hier een antwoord geeft of een fout aanpast, zal dat na een tijdje ook in al dié applicaties te zien zijn.</p>"
}),
sharescreen: {
intro: new T({
en: "<h3>Share this map</h3> Share this map by copying the link below and sending it to friends and family:"
}),
addToHomeScreen: new T({
en: "<h3>Add to your home screen</h3>You can easily add this website to your home screen for a native feel. Click the 'add to home screen button' in the URL bar to do this."
}),
embedIntro: new T({
en: "<h3>Embed on your website</h3>Please, embed this map into your website. <br/>We encourage you to do it - you don't even have to ask permission. <br/> It is free, and always will be. The more people using this, the more valuable it becomes."
})
},
morescreen: {
intro:new T({
en:"<h3>More quests</h3>Do you enjoy collecting geodata? <br/>There are more layers available.",
}),
streetcomplete: new T({
en: "Another, similar application is <a href='https://play.google.com/store/apps/details?id=de.westnordost.streetcomplete' target='_blank'>StreetComplete</a>"
})
} }
} }
} }

100
assets/share.svg Normal file
View file

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="100"
height="100"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="share.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="-15.237738"
inkscape:cy="36.323203"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="0"
inkscape:window-y="1050"
inkscape:window-maximized="1">
<sodipodi:guide
position="13.229167,23.859748"
orientation="1,0"
id="guide815"
inkscape:locked="false" />
<sodipodi:guide
position="14.944824,13.229167"
orientation="0,1"
id="guide817"
inkscape:locked="false" />
<sodipodi:guide
position="19.182291,3.4395834"
orientation="1,0"
id="guide852"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
style="fill:none;stroke:#000000;stroke-width:2.43863511;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 19.212364,278.17517 -11.9689358,5.52059 11.9388628,5.50669"
id="path819"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.53329796;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.97014926"
id="path820"
cx="7.2434282"
cy="283.69574"
r="3.9119694" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.53329796;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.97014926"
id="path820-3"
cx="19.48818"
cy="289.22873"
r="3.9119689" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.53329796;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.97014926"
id="path820-3-6"
cx="19.48818"
cy="277.56281"
r="3.9119689" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

133
index.css
View file

@ -21,8 +21,6 @@ form {
} }
#leafletDiv { #leafletDiv {
height: 100%; height: 100%;
} }
@ -343,18 +341,14 @@ form {
#welcomeMessage { #welcomeMessage {
display: inline-block; display: inline-block;
background-color: white;
padding: 1em;
margin-left: 3.5em; margin-left: 3.5em;
padding-left: 1em;
padding-bottom: 2em;
border-radius: 2em; border-radius: 2em;
border-top-left-radius: 0; border-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
max-width: calc(max(35vw, 30em)); max-width: 40em;
width: 45vw;
max-height: calc(100vh - 15em); max-height: calc(100vh - 15em);
overflow-y: auto; overflow-y: auto;
box-shadow: 0 0 10px #00000066;
} }
#messagesbox { #messagesbox {
@ -647,32 +641,15 @@ form {
height: auto; height: auto;
} }
#bottomRight { #top-right {
display: block ruby;
position: absolute; position: absolute;
display: block;
margin: auto; right: 0.5em;
right: 1%; top: 0.5em;
bottom: 1.5em;
height: auto;
min-height: 1em;
width: auto;
font-size: large;
padding: 2em;
border-radius: 2em;
z-index: 5000; z-index: 5000;
opacity: 1;
background-color: white;
transition: all 500ms linear;
text-align: center;
horiz-align: center;
font-weight: bold;
} }
@ -844,7 +821,6 @@ form {
.license-picker { .license-picker {
background-color: orange;
float: left; float: left;
} }
@ -1134,3 +1110,100 @@ form {
padding-right: 0.3em; padding-right: 0.3em;
border-radius: 1.5em; border-radius: 1.5em;
} }
/******** TabbedElement ****/
.tabs-header-bar {
padding-left: 1em;
padding-top: 10px; /* For the shadow */
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: start;
background-color: white;
}
.tab-single-header img {
height: 3em;
width: 3em;
padding: 0.5em;
}
.tab-content {
padding: 1em;
z-index: 5002;
background-color: white;
position: relative;
}
.tab-single-header {
border-top-left-radius: 1em;
border-top-right-radius: 1em;
box-shadow: 0 0 10px black;
z-index: 5000;
border-bottom: 1px solid white;
padding-bottom: 1px;
}
.tab-active {
background-color: white;
z-index: 5001;
}
.tab-non-active {
background-color: #e5f5ff;
opacity: 0.5;
border-bottom: 1px solid lightgray;
}
/****** ShareScreen *****/
.literal-code {
display: inline-block;
background-color: lightgray;
padding: 0.5em;
}
.iframe-code-block {
}
.iframe-escape {
background-color: white;
border-radius: 2em;
display: block;
}
.iframe-escape img{
padding: 1em;
width: 2em;
height: 2em;
}
/** Switch layout **/
.switch-layout a{
display: flex;
flex-wrap: nowrap;
flex-direction: row;
font-size: large;
margin: 0.5em;
background-color: #e5f5ff;
border-radius: 1em;
align-items: center;
text-decoration: none;
color: black;
}
.switch-layout a img{
width: 3em;
max-height: 3em;
margin-right: 0.5em;
padding: 0.5em;
}

View file

@ -28,8 +28,6 @@
<div id="topleft-tools"> <div id="topleft-tools">
<div id="userbadge-and-search"> <div id="userbadge-and-search">
<div id="userbadge" class="shadow"> <div id="userbadge" class="shadow">
Loading... If this message persists, check if javascript is enabled and if no extension (uMatrix) is
blocking it.
</div> </div>
<div id="searchbox" class="shadow"></div> <div id="searchbox" class="shadow"></div>
</div> </div>
@ -42,8 +40,8 @@
<div id="filter__selection"></div> <div id="filter__selection"></div>
</div> </div>
<div id="centermessage">Loading...</div> <div id="centermessage">Loading MapComplete, hang on...</div>
<div id="bottomRight" style="display: none">ADD</div> <div id="top-right"></div>
<div id="geolocate-button"></div> <div id="geolocate-button"></div>
<div id="leafletDiv"></div> <div id="leafletDiv"></div>

View file

@ -32,6 +32,9 @@ import {QueryParameters} from "./Logic/QueryParameters";
import {Utils} from "./Utils"; import {Utils} from "./Utils";
import {LocalStorageSource} from "./Logic/LocalStorageSource"; import {LocalStorageSource} from "./Logic/LocalStorageSource";
import {Button} from "./UI/Base/Button"; import {Button} from "./UI/Base/Button";
import {TabbedComponent} from "./UI/Base/TabbedComponent";
import {ShareScreen} from "./UI/ShareScreen";
import {InitUiElements} from "./InitUiElements";
// --------------------- Special actions based on the parameters ----------------- // --------------------- Special actions based on the parameters -----------------
@ -44,7 +47,7 @@ if (location.href.startsWith("http://buurtnatuur.be")) {
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
// Set to true if testing and changes should NOT be saved // Set to true if testing and changes should NOT be saved
const testing = QueryParameters.GetQueryParameter("test"); const testing = QueryParameters.GetQueryParameter("test", "true");
testing.setData(testing.data ?? "true") testing.setData(testing.data ?? "true")
// If you have a testfile somewhere, enable this to spoof overpass // If you have a testfile somewhere, enable this to spoof overpass
// This should be hosted independantly, e.g. with `cd assets; webfsd -p 8080` + a CORS plugin to disable cors rules // This should be hosted independantly, e.g. with `cd assets; webfsd -p 8080` + a CORS plugin to disable cors rules
@ -54,7 +57,7 @@ if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
// ----------------- SELECT THE RIGHT QUESTSET ----------------- // ----------------- SELECT THE RIGHT QUESTSET -----------------
let defaultLayout = "all" let defaultLayout = "bookcases"
const path = window.location.pathname.split("/").slice(-1)[0]; const path = window.location.pathname.split("/").slice(-1)[0];
if (path !== "index.html") { if (path !== "index.html") {
@ -76,7 +79,7 @@ for (const k in AllKnownLayouts.allSets) {
} }
} }
defaultLayout = QueryParameters.GetQueryParameter("layout").data ?? defaultLayout; defaultLayout = QueryParameters.GetQueryParameter("layout", defaultLayout).data;
const layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout] ?? AllKnownLayouts["all"]; const layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout] ?? AllKnownLayouts["all"];
console.log("Using layout: ", layoutToUse.name); console.log("Using layout: ", layoutToUse.name);
@ -102,13 +105,21 @@ const fullScreenMessage = new UIEventSource<UIElement>(undefined);
// The latest element that was selected - used to generate the right UI at the right place // The latest element that was selected - used to generate the right UI at the right place
const selectedElement = new UIEventSource<{ feature: any }>(undefined); const selectedElement = new UIEventSource<{ feature: any }>(undefined);
const zoom = QueryParameters.GetQueryParameter("z") const zoom = QueryParameters.GetQueryParameter("z", "" + layoutToUse.startzoom)
.syncWith(LocalStorageSource.Get("zoom")); .syncWith(LocalStorageSource.Get("zoom"));
const lat = QueryParameters.GetQueryParameter("lat") const lat = QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat)
.syncWith(LocalStorageSource.Get("lat")); .syncWith(LocalStorageSource.Get("lat"));
const lon = QueryParameters.GetQueryParameter("lon") const lon = QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon)
.syncWith(LocalStorageSource.Get("lon")); .syncWith(LocalStorageSource.Get("lon"));
const featureSwitchUserbadge = QueryParameters.GetQueryParameter("fs-userbadge", "true");
const featureSwitchSearch = QueryParameters.GetQueryParameter("fs-search", "true");
const featureSwitchWelcomeMessage = QueryParameters.GetQueryParameter("fs-welcome-message", "true");
const featureSwitchLayers = QueryParameters.GetQueryParameter("fs-layers", "true");
const featureSwitchEmbedded = QueryParameters.GetQueryParameter("fs-embedded", "true");
const featureSwitchAddNew = QueryParameters.GetQueryParameter("fs-add-new", "true");
const featureSwitchIframe = QueryParameters.GetQueryParameter("fs-iframe", "false");
const locationControl = new UIEventSource<{ lat: number, lon: number, zoom: number }>({ const locationControl = new UIEventSource<{ lat: number, lon: number, zoom: number }>({
zoom: Utils.asFloat(zoom.data) ?? layoutToUse.startzoom, zoom: Utils.asFloat(zoom.data) ?? layoutToUse.startzoom,
@ -126,7 +137,7 @@ locationControl.addCallback((latlonz) => {
// ----------------- Prepare the important objects ----------------- // ----------------- Prepare the important objects -----------------
const osmConnection = new OsmConnection( const osmConnection = new OsmConnection(
QueryParameters.GetQueryParameter("test").data === "true" QueryParameters.GetQueryParameter("test", "false").data === "true"
); );
@ -169,6 +180,7 @@ const bm = new Basemap("leafletDiv", locationControl, new VariableUiElement(
)); ));
// ------------- Setup the layers ------------------------------- // ------------- Setup the layers -------------------------------
const addButtons: { const addButtons: {
name: UIElement, name: UIElement,
@ -228,7 +240,9 @@ if (flayers.length > 1) {
layerControl = new Combine([layerSelection, backgroundMapPicker]); layerControl = new Combine([layerSelection, backgroundMapPicker]);
} }
new CheckBox(layerControl, closedFilterButton).AttachTo("filter__selection"); InitUiElements.OnlyIf(featureSwitchLayers, () => {
new CheckBox(layerControl, closedFilterButton).AttachTo("filter__selection");
});
// ------------------ Setup various other UI elements ------------ // ------------------ Setup various other UI elements ------------
@ -240,7 +254,8 @@ Locale.language.addCallback(e => {
}) })
new StrayClickHandler(bm, selectedElement, fullScreenMessage, () => { InitUiElements.OnlyIf(featureSwitchAddNew, () => {
new StrayClickHandler(bm, selectedElement, fullScreenMessage, () => {
return new SimpleAddUI(bm.Location, return new SimpleAddUI(bm.Location,
bm.LastClickLocation, bm.LastClickLocation,
changes, changes,
@ -249,7 +264,9 @@ new StrayClickHandler(bm, selectedElement, fullScreenMessage, () => {
osmConnection.userDetails, osmConnection.userDetails,
addButtons); addButtons);
} }
); );
});
/** /**
* Show the questions and information for the selected element * Show the questions and information for the selected element
@ -283,40 +300,31 @@ selectedElement.addCallback((feature) => {
const pendingChanges = new PendingChanges(changes, secondsTillChangesAreSaved,); const pendingChanges = new PendingChanges(changes, secondsTillChangesAreSaved,);
new UserBadge(osmConnection.userDetails, InitUiElements.OnlyIf(featureSwitchUserbadge, () => {
new UserBadge(osmConnection.userDetails,
pendingChanges, pendingChanges,
Locale.CreateLanguagePicker(layoutToUse), Locale.CreateLanguagePicker(layoutToUse),
bm) bm)
.AttachTo('userbadge'); .AttachTo('userbadge');
new SearchAndGo(bm).AttachTo("searchbox");
const welcome = new WelcomeMessage(layoutToUse,
Locale.CreateLanguagePicker(layoutToUse, Translations.t.general.pickLanguage),
osmConnection).onClick(() => {
}); });
const help = new FixedUiElement(`<div class='collapse-button-img'><img src='assets/help.svg' alt='help'></div>`); InitUiElements.OnlyIf((featureSwitchSearch), () => {
const close = new FixedUiElement(`<div class='collapse-button-img'><img src='assets/close.svg' alt='close'></div>`); new SearchAndGo(bm).AttachTo("searchbox");
new CheckBox( });
new Combine([
new Combine(["<span class='collapse-button'>", close, "</span>"]),
welcome]),
new Combine(["<span class='open-button'>", help, "</span>"])
, true
).AttachTo("messagesbox");
new FullScreenMessageBoxHandler(fullScreenMessage, () => { new FullScreenMessageBoxHandler(fullScreenMessage, () => {
selectedElement.setData(undefined) selectedElement.setData(undefined)
}).update(); }).update();
const welcome2 = new WelcomeMessage(layoutToUse, Locale.CreateLanguagePicker(layoutToUse, Translations.t.general.pickLanguage), osmConnection) InitUiElements.OnlyIf(featureSwitchWelcomeMessage, () => {
fullScreenMessage.setData(welcome2) InitUiElements.InitWelcomeMessage(layoutToUse, osmConnection, bm, fullScreenMessage)
new FixedUiElement(`<div class='collapse-button-img' class="shadow"><img src='assets/help.svg' alt='help'></div>`).onClick(() => { });
fullScreenMessage.setData(welcome2)
}) if (window != window.top || featureSwitchIframe.data !== "false") {
.AttachTo("help-button-mobile") new FixedUiElement(`<a href='${window.location}' target='_blank'><span class='iframe-escape'><img src='assets/pencil.svg'></span></a>`).AttachTo("top-right")
}
new CenterMessageBox( new CenterMessageBox(
minZoom, minZoom,

View file

@ -5,9 +5,7 @@
<link href="index.css" rel="stylesheet"/> <link href="index.css" rel="stylesheet"/>
</head> </head>
<body> <body>
<span class="image-delete-container">
<div id="maindiv">'maindiv' not attached</div> <div id="maindiv">'maindiv' not attached</div>
</span>
<div id="extradiv">'extradiv' not attached</div> <div id="extradiv">'extradiv' not attached</div>
<script src="./test.ts"></script> <script src="./test.ts"></script>
</body> </body>

38
test.ts
View file

@ -1,36 +1,4 @@
import { And, Tag, Or } from "./Logic/TagsFilter"; import {MoreScreen} from "./UI/MoreScreen";
import { Overpass } from "./Logic/Overpass"; import {UIEventSource} from "./UI/UIEventSource";
new MoreScreen(new UIEventSource<{zoom: number, lat: number, lon: number}>({zoom: 16, lat: 51.3, lon: 3.2})).AttachTo("maindiv")
function anyValueExcept(key: string, exceptValue: string) {
return new And([
new Tag(key, "*"),
new Tag(key, exceptValue, true)
])
}
const sellsBikes = new Tag("service:bicycle:retail", "yes")
const repairsBikes = anyValueExcept("service:bicycle:repair", "no")
const rentsBikes = new Tag("service:bicycle:rental", "yes")
const hasPump = new Tag("service:bicycle:pump", "yes")
const hasDiy = new Tag("service:bicycle:diy", "yes")
const sellsSecondHand = anyValueExcept("service:bicycle:repair", "no")
const hasBikeServices = new Or([
sellsBikes,
repairsBikes,
rentsBikes,
hasPump,
hasDiy,
sellsSecondHand
])
const overpassFilter = new And([
new Tag("shop", "bicycle", true),
hasBikeServices
])
const overpass = new Overpass(overpassFilter)
// console.log(overpass.buildQuery('bbox:51.12246976163816,3.1045767593383795,51.289518504257174,3.2848313522338866'))
console.log(overpassFilter.asOverpass())