Add cyclestreet theme, various bugfixes

This commit is contained in:
Pieter Vander Vennet 2020-08-27 18:44:16 +02:00
parent 72a744f60d
commit 60c15e9c8d
23 changed files with 412 additions and 211 deletions

View file

@ -15,6 +15,7 @@ import * as bookcases from "../assets/themes/bookcases/Bookcases.json";
import * as aed from "../assets/themes/aed/aed.json"; import * as aed from "../assets/themes/aed/aed.json";
import * as toilets from "../assets/themes/toilets/toilets.json"; import * as toilets from "../assets/themes/toilets/toilets.json";
import * as artworks from "../assets/themes/artwork/artwork.json"; import * as artworks from "../assets/themes/artwork/artwork.json";
import * as cyclestreets from "../assets/themes/cyclestreets/cyclestreets.json";
import {PersonalLayout} from "../Logic/PersonalLayout"; import {PersonalLayout} from "../Logic/PersonalLayout";
export class AllKnownLayouts { export class AllKnownLayouts {
@ -31,6 +32,7 @@ export class AllKnownLayouts {
CustomLayoutFromJSON.LayoutFromJSON(aed), CustomLayoutFromJSON.LayoutFromJSON(aed),
CustomLayoutFromJSON.LayoutFromJSON(toilets), CustomLayoutFromJSON.LayoutFromJSON(toilets),
CustomLayoutFromJSON.LayoutFromJSON(artworks), CustomLayoutFromJSON.LayoutFromJSON(artworks),
CustomLayoutFromJSON.LayoutFromJSON(cyclestreets),
new MetaMap(), new MetaMap(),
new StreetWidth(), new StreetWidth(),

View file

@ -135,6 +135,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
this._questionElement = this.InputElementFor(options); this._questionElement = this.InputElementFor(options);
const save = () => { const save = () => {
const selection = self._questionElement.GetValue().data; const selection = self._questionElement.GetValue().data;
console.log("Tagrendering: saving tags ", selection);
if (selection) { if (selection) {
State.state.changes.addTag(tags.data.id, selection); State.state.changes.addTag(tags.data.id, selection);
} }
@ -152,10 +153,10 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
return ""; return "";
} }
if (csCount < State.userJourney.tagsVisibleAndWikiLinked) { if (csCount < State.userJourney.tagsVisibleAndWikiLinked) {
const tagsStr = tags.asHumanString(false); const tagsStr = tags.asHumanString(false, true);
return new FixedUiElement(tagsStr).SetClass("subtle").Render(); return new FixedUiElement(tagsStr).SetClass("subtle").Render();
} }
return tags.asHumanString(true); return tags.asHumanString(true, true);
} }
) )
); );
@ -318,7 +319,6 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
return true; return true;
} }
} }
return this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined; return this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined;
} }

View file

@ -11,12 +11,11 @@ export class LayerUpdater {
public readonly sufficentlyZoomed: UIEventSource<boolean> = new UIEventSource<boolean>(false); public readonly sufficentlyZoomed: UIEventSource<boolean> = new UIEventSource<boolean>(false);
public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false); public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false);
public readonly retries: UIEventSource<number> = new UIEventSource<number>(0); public readonly retries: UIEventSource<number> = new UIEventSource<number>(0);
/** /**
* The previous bounds for which the query has been run * The previous bounds for which the query has been run
*/ */
private previousBounds: Bounds; private previousBounds: Bounds;
/** /**
* The most important layer should go first, as that one gets first pick for the questions * The most important layer should go first, as that one gets first pick for the questions
* @param map * @param map

View file

@ -14,56 +14,75 @@ export class Changes {
private static _nextId = -1; // New assined ID's are negative private static _nextId = -1; // New assined ID's are negative
addTag(elementId: string, tagsFilter : TagsFilter){ addTag(elementId: string, tagsFilter: TagsFilter) {
if(tagsFilter instanceof Tag){ const changes = this.tagToChange(tagsFilter);
const tag = tagsFilter as Tag;
if(typeof tag.value !== "string"){ if (changes.length == 0) {
throw "Invalid value"
}
this.addChange(elementId, tag.key, tag.value);
return; return;
} }
const eventSource = State.state.allElements.getElement(elementId);
if(tagsFilter instanceof And){ const elementTags = eventSource.data;
const pending : {elementId:string, key: string, value: string}[] = [];
for (const change of changes) {
if (elementTags[change.k] !== change.v) {
elementTags[change.k] = change.v;
pending.push({elementId: elementTags.id, key: change.k, value: change.v});
}
}
if(pending.length === 0){
return;
}
eventSource.ping();
this.uploadAll([], pending);
}
private tagToChange(tagsFilter: TagsFilter) {
let changes: { k: string, v: string }[] = [];
if (tagsFilter instanceof Tag) {
const tag = tagsFilter as Tag;
if (typeof tag.value !== "string") {
throw "Invalid value"
}
return [this.checkChange(tag.key, tag.value)];
}
if (tagsFilter instanceof And) {
const and = tagsFilter as And; const and = tagsFilter as And;
for (const tag of and.and) { for (const tag of and.and) {
this.addTag(elementId, tag); changes = changes.concat(this.tagToChange(tag));
} }
return; return changes;
} }
console.log("Unsupported tagsfilter element to addTag", tagsFilter); console.log("Unsupported tagsfilter element to addTag", tagsFilter);
throw "Unsupported tagsFilter element"; throw "Unsupported tagsFilter element";
} }
/** /**
* Adds a change to the pending changes * Adds a change to the pending changes
* @param elementId * @param elementId
* @param key * @param key
* @param value * @param value
*/ */
addChange(elementId: string, key: string, value: string) { private checkChange(key: string, value: string): { k: string, v: string } {
if (key === undefined || key === null) { if (key === undefined || key === null) {
console.log("Invalid key"); console.log("Invalid key");
return; return undefined;
} }
if (value === undefined || value === null) { if (value === undefined || value === null) {
console.log("Invalid value for ",key); console.log("Invalid value for ", key);
return; return undefined;
} }
if(key.startsWith(" ") || value.startsWith(" ") || value.endsWith(" ") || key.endsWith(" ")){ if (key.startsWith(" ") || value.startsWith(" ") || value.endsWith(" ") || key.endsWith(" ")) {
console.warn("Tag starts with or ends with a space - trimming anyway") console.warn("Tag starts with or ends with a space - trimming anyway")
} }
key = key.trim(); key = key.trim();
value = value.trim(); value = value.trim();
const eventSource = State.state.allElements.getElement(elementId); return {k: key, v: value};
eventSource.data[key] = value;
eventSource.ping();
this.uploadAll([], [{elementId: eventSource.data.id, key: key, value: value}]);
} }
/** /**
@ -109,37 +128,90 @@ export class Changes {
return geojson; return geojson;
} }
private uploadChangesWithLatestVersions(
knownElements, newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) {
// Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements
// We apply the changes on them
for (const change of pending) {
if (parseInt(change.elementId.split("/")[1]) < 0) {
// This is a new element - we should apply this on one of the new elements
for (const newElement of newElements) {
if (newElement.type + "/" + newElement.id === change.elementId) {
newElement.addTag(change.key, change.value);
}
}
} else {
knownElements[change.elementId].addTag(change.key, change.value);
}
}
// Small sanity check for duplicate information
let changedElements = [];
for (const elementId in knownElements) {
const element = knownElements[elementId];
if (element.changed) {
changedElements.push(element);
}
}
if (changedElements.length == 0 && newElements.length == 0) {
console.log("No changes in any object");
return;
}
console.log("Beginning upload...");
// At last, we build the changeset and upload
State.state.osmConnection.UploadChangeset(
function (csId) {
let modifications = "";
for (const element of changedElements) {
if (!element.changed) {
continue;
}
modifications += element.ChangesetXML(csId) + "\n";
}
let creations = "";
for (const newElement of newElements) {
creations += newElement.ChangesetXML(csId);
}
let changes = `<osmChange version='0.6' generator='Mapcomplete ${State.vNumber}'>`;
if (creations.length > 0) {
changes +=
"<create>" +
creations +
"</create>";
}
if (modifications.length > 0) {
changes +=
"<modify>" +
modifications +
"</modify>";
}
changes += "</osmChange>";
return changes;
});
};
private uploadAll( private uploadAll(
newElements: OsmObject[], newElements: OsmObject[],
pending: { elementId: string; key: string; value: string }[] pending: { elementId: string; key: string; value: string }[]
) { ) {
const self = this; const self = this;
const knownElements = {}; // maps string --> OsmObject
function DownloadAndContinue(neededIds, continuation: (() => void)) {
// local function which downloads all the objects one by one
// this is one big loop, running one download, then rerunning the entire function
if (neededIds.length == 0) {
continuation();
return;
}
const neededId = neededIds.pop();
if (neededId in knownElements) { let neededIds: string[] = [];
DownloadAndContinue(neededIds, continuation);
return;
}
console.log("Downloading ", neededId);
OsmObject.DownloadObject(neededId,
function (element) {
knownElements[neededId] = element; // assign the element for later, continue downloading the next element
DownloadAndContinue(neededIds, continuation);
}
);
}
const neededIds = [];
for (const change of pending) { for (const change of pending) {
const id = change.elementId; const id = change.elementId;
if (parseFloat(id.split("/")[1]) < 0) { if (parseFloat(id.split("/")[1]) < 0) {
@ -149,80 +221,9 @@ export class Changes {
} }
} }
neededIds = Utils.Dedup(neededIds);
DownloadAndContinue(neededIds, function () { OsmObject.DownloadAll(neededIds, {}, (knownElements) => {
// Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements' self.uploadChangesWithLatestVersions(knownElements, newElements, pending)
// We apply the changes on them
for (const change of pending) {
if (parseInt(change.elementId.split("/")[1]) < 0) {
// This is a new element - we should apply this on one of the new elements
for (const newElement of newElements) {
if (newElement.type + "/" + newElement.id === change.elementId) {
newElement.addTag(change.key, change.value);
}
}
} else {
knownElements[change.elementId].addTag(change.key, change.value);
}
}
// Small sanity check for duplicate information
let changedElements = [];
for (const elementId in knownElements) {
const element = knownElements[elementId];
if (element.changed) {
changedElements.push(element);
}
}
if (changedElements.length == 0 && newElements.length == 0) {
console.log("No changes in any object");
return;
}
console.log("Beginning upload...");
// At last, we build the changeset and upload
State.state.osmConnection.UploadChangeset(
function (csId) {
let modifications = "";
for (const element of changedElements) {
if (!element.changed) {
continue;
}
modifications += element.ChangesetXML(csId) + "\n";
}
let creations = "";
for (const newElement of newElements) {
creations += newElement.ChangesetXML(csId);
}
let changes = `<osmChange version='0.6' generator='Mapcomplete ${State.vNumber}'>`;
if (creations.length > 0) {
changes +=
"<create>" +
creations +
"</create>";
}
if (modifications.length > 0) {
changes +=
"<modify>" +
modifications +
"</modify>";
}
changes += "</osmChange>";
return changes;
},
() => {
});
}); });
} }

View file

@ -10,11 +10,11 @@ export class ChangesetHandler {
public currentChangeset: UIEventSource<string>; public currentChangeset: UIEventSource<string>;
constructor(dryRun: boolean, osmConnection: OsmConnection, auth) { constructor(layoutName: string, dryRun: boolean, osmConnection: OsmConnection, auth) {
this._dryRun = dryRun; this._dryRun = dryRun;
this.userDetails = osmConnection.userDetails; this.userDetails = osmConnection.userDetails;
this.auth = auth; this.auth = auth;
this.currentChangeset = osmConnection.GetPreference("current-open-changeset"); this.currentChangeset = osmConnection.GetPreference("current-open-changeset-" + layoutName);
if (dryRun) { if (dryRun) {
console.log("DRYRUN ENABLED"); console.log("DRYRUN ENABLED");
@ -26,7 +26,6 @@ export class ChangesetHandler {
continuation: () => void) { continuation: () => void) {
if (this._dryRun) { if (this._dryRun) {
console.log("NOT UPLOADING as dryrun is true");
var changesetXML = generateChangeXML("123456"); var changesetXML = generateChangeXML("123456");
console.log(changesetXML); console.log(changesetXML);
continuation(); continuation();
@ -40,7 +39,9 @@ export class ChangesetHandler {
// We have to open a new changeset // We have to open a new changeset
this.OpenChangeset((csId) => { this.OpenChangeset((csId) => {
this.currentChangeset.setData(csId); this.currentChangeset.setData(csId);
self.AddChange(csId, generateChangeXML(csId), const changeset = generateChangeXML(csId);
console.log(changeset);
self.AddChange(csId, changeset,
() => { () => {
}, },
(e) => { (e) => {
@ -67,23 +68,6 @@ export class ChangesetHandler {
) )
} }
/*
this.OpenChangeset(
function (csId) {
var changesetXML = generateChangeXML(csId);
self.AddChange(csId, changesetXML,
function (csId, mapping) {
self.CloseChangeset(csId, continuation);
handleMapping(mapping);
}
);
}
);*/
this.userDetails.data.csCount++;
this.userDetails.ping();
} }

View file

@ -29,7 +29,11 @@ export class OsmConnection {
private _onLoggedIn : ((userDetails: UserDetails) => void)[] = []; private _onLoggedIn : ((userDetails: UserDetails) => void)[] = [];
constructor(dryRun: boolean, oauth_token: UIEventSource<string>, singlePage: boolean = true, useDevServer:boolean = false) { constructor(dryRun: boolean, oauth_token: UIEventSource<string>,
// Used to keep multiple changesets open and to write to the correct changeset
layoutName: string,
singlePage: boolean = true,
useDevServer:boolean = false) {
let pwaStandAloneMode = false; let pwaStandAloneMode = false;
try { try {
@ -72,7 +76,7 @@ export class OsmConnection {
this.preferencesHandler = new OsmPreferences(this.auth, this); this.preferencesHandler = new OsmPreferences(this.auth, this);
this.changesetHandler = new ChangesetHandler(dryRun, this, this.auth); this.changesetHandler = new ChangesetHandler(layoutName, dryRun, this, this.auth);
if (oauth_token.data !== undefined) { if (oauth_token.data !== undefined) {
console.log(oauth_token.data) console.log(oauth_token.data)
const self = this; const self = this;
@ -94,7 +98,7 @@ export class OsmConnection {
public UploadChangeset(generateChangeXML: (csid: string) => string, public UploadChangeset(generateChangeXML: (csid: string) => string,
continuation: () => void) { continuation: () => void = () => {}) {
this.changesetHandler.UploadChangeset(generateChangeXML, continuation); this.changesetHandler.UploadChangeset(generateChangeXML, continuation);
} }
@ -119,6 +123,7 @@ export class OsmConnection {
public AttemptLogin() { public AttemptLogin() {
const self = this; const self = this;
console.log("Trying to log in...");
this.auth.xhr({ this.auth.xhr({
method: 'GET', method: 'GET',
path: '/api/0.6/user/details' path: '/api/0.6/user/details'

View file

@ -60,6 +60,7 @@ export abstract class OsmObject {
return tags; return tags;
} }
Download(continuation: ((element: OsmObject) => void)) { Download(continuation: ((element: OsmObject) => void)) {
const self = this; const self = this;
$.getJSON("https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id, $.getJSON("https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id,
@ -96,6 +97,31 @@ export abstract class OsmObject {
return 'version="'+this.version+'"'; return 'version="'+this.version+'"';
} }
abstract ChangesetXML(changesetId: string): string; abstract ChangesetXML(changesetId: string): string;
public static DownloadAll(neededIds, knownElements: any = {}, continuation: ((knownObjects : any) => void)) {
// local function which downloads all the objects one by one
// this is one big loop, running one download, then rerunning the entire function
if (neededIds.length == 0) {
continuation(knownElements);
return;
}
const neededId = neededIds.pop();
if (neededId in knownElements) {
OsmObject.DownloadAll(neededIds, knownElements, continuation);
return;
}
console.log("Downloading ", neededId);
OsmObject.DownloadObject(neededId,
function (element) {
knownElements[neededId] = element; // assign the element for later, continue downloading the next element
OsmObject.DownloadAll(neededIds,knownElements, continuation);
}
);
}
} }

View file

@ -9,7 +9,7 @@ export abstract class TagsFilter {
return this.matches(TagUtils.proprtiesToKV(properties)); return this.matches(TagUtils.proprtiesToKV(properties));
} }
abstract asHumanString(linkToWiki: boolean); abstract asHumanString(linkToWiki: boolean, shorten: boolean);
} }
@ -90,7 +90,12 @@ export class Tag extends TagsFilter {
} }
matches(tags: { k: string; v: string }[]): boolean { matches(tags: { k: string; v: string }[]): boolean {
if (this.value === "") {
return true
}
for (const tag of tags) { for (const tag of tags) {
if (Tag.regexOrStrMatches(this.key, tag.k)) { if (Tag.regexOrStrMatches(this.key, tag.k)) {
if (tag.v === "") { if (tag.v === "") {
@ -109,10 +114,6 @@ export class Tag extends TagsFilter {
return Tag.regexOrStrMatches(this.value, tag.v) !== this.invertValue return Tag.regexOrStrMatches(this.value, tag.v) !== this.invertValue
} }
} }
if (this.value === "") {
return true
}
return this.invertValue return this.invertValue
} }
@ -150,15 +151,17 @@ export class Tag extends TagsFilter {
return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags)); return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags));
} }
asHumanString(linkToWiki: boolean) { asHumanString(linkToWiki: boolean, shorten: boolean) {
let v = "" let v = ""
if (typeof (this.value) === "string") { if (typeof (this.value) === "string") {
v = this.value; v = this.value;
}else{ } else {
// value is a regex // value is a regex
v = this.value.source; v = this.value.source;
} }
v = Utils.EllipsesAfter(v, 25); if (shorten) {
v = Utils.EllipsesAfter(v, 25);
}
if (linkToWiki) { if (linkToWiki) {
return `<a href='https://wiki.openstreetmap.org/wiki/Key:${this.key}' target='_blank'>${this.key}</a>` + return `<a href='https://wiki.openstreetmap.org/wiki/Key:${this.key}' target='_blank'>${this.key}</a>` +
`=` + `=` +
@ -221,8 +224,8 @@ export class Or extends TagsFilter {
return new Or(newChoices); return new Or(newChoices);
} }
asHumanString(linkToWiki: boolean) { asHumanString(linkToWiki: boolean, shorten: boolean) {
return this.or.map(t => t.asHumanString(linkToWiki)).join("|"); return this.or.map(t => t.asHumanString(linkToWiki, shorten)).join("|");
} }
} }
@ -282,8 +285,8 @@ export class And extends TagsFilter {
return new And(newChoices); return new And(newChoices);
} }
asHumanString(linkToWiki: boolean) { asHumanString(linkToWiki: boolean, shorten: boolean) {
return this.and.map(t => t.asHumanString(linkToWiki)).join("&"); return this.and.map(t => t.asHumanString(linkToWiki, shorten)).join("&");
} }
} }
@ -308,8 +311,8 @@ export class Not extends TagsFilter{
return new Not(this.not.substituteValues(tags)); return new Not(this.not.substituteValues(tags));
} }
asHumanString(linkToWiki: boolean) { asHumanString(linkToWiki: boolean, shorten: boolean) {
return "!" + this.not.asHumanString(linkToWiki); return "!" + this.not.asHumanString(linkToWiki, shorten);
} }
} }

View file

@ -107,7 +107,7 @@ A theme has translations into the preset.json (`assets/themes/themename/themenam
### High-level overview ### High-level overview
The website is purely static. This means that there is no database here, nor one is needed as all the data is kept in OpenStreetMap or Wikimedia (for images). The website is purely static. This means that there is no database here, nor one is needed as all the data is kept in OpenStreetMap, Wikimedia (for images), IMGUR. Settings are saved in the preferences-space of the OSM-website, amended by some local-storage if the user is not logged-in.
When viewing, the data is loaded from overpass. The data is then converted (in the browser) to geojson, which is rendered by Leaflet. When viewing, the data is loaded from overpass. The data is then converted (in the browser) to geojson, which is rendered by Leaflet.
@ -130,7 +130,13 @@ Images are fetched from:
Images are uplaoded to imgur, as their API was way easier to handle. The URL is written into the changes Images are uplaoded to imgur, as their API was way easier to handle. The URL is written into the changes
The idea is that one in a while, the images are transfered to wikipedia The idea is that once in a while, the images are transfered to wikipedia or that we hook up wikimedia directly (but I need some help in getting their API working).
### Uploading changes
In order to avoid lots of small changesets, a changeset is opened and kept open. The changeset number is saved into the users preferences on OSM.
Whenever a change is made -even adding a single tag- the change is uploaded into this changeset. If that fails, the changeset is probably closed and we open a new changeset.
# Privacy # Privacy

View file

@ -175,6 +175,7 @@ export class State {
this.osmConnection = new OsmConnection( this.osmConnection = new OsmConnection(
testParam === "true", testParam === "true",
QueryParameters.GetQueryParameter("oauth_token", undefined), QueryParameters.GetQueryParameter("oauth_token", undefined),
layoutToUse.name,
true, true,
testParam === "dev" testParam === "dev"
); );

View file

@ -35,9 +35,13 @@ export class TabbedComponent extends UIElement {
headerBar = "<div class='tabs-header-bar'>" + headerBar + "</div>" headerBar = "<div class='tabs-header-bar'>" + headerBar + "</div>"
const content = this.content[this._source.data].Render(); const content = this.content[this._source.data];
return headerBar + "<div class='tab-content'>" + content.Render() + "</div>";
}
return headerBar + "<div class='tab-content'>" + content + "</div>"; protected InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
this.content[this._source.data].Update();
} }
} }

View file

@ -13,7 +13,7 @@ export class Preview extends UIElement {
private reloadButton: Button; private reloadButton: Button;
private otherPreviews: VariableUiElement; private otherPreviews: VariableUiElement;
constructor(url: UIEventSource<string>, config: UIEventSource<LayoutConfigJson>) { constructor(url: UIEventSource<string>, testurl: UIEventSource<string>, config: UIEventSource<LayoutConfigJson>) {
super(undefined); super(undefined);
this.config = config; this.config = config;
this.url = url; this.url = url;

View file

@ -85,8 +85,7 @@ class MappingGenerator extends UIElement {
} }
InnerRender(): string { InnerRender(): string {
const combine = new VerticalCombine(this.elements); const combine = new VerticalCombine(this.elements).SetClass("bordered");
combine.clss = "bordered";
return combine.Render(); return combine.Render();
} }
} }
@ -186,8 +185,7 @@ class TagRenderingGenerator
} }
InnerRender(): string { InnerRender(): string {
const combine = new VerticalCombine(this.elements); const combine = new VerticalCombine(this.elements).SetClass("bordered");
combine.clss = "bordered";
return combine.Render(); return combine.Render();
} }
} }
@ -235,8 +233,7 @@ class PresetGenerator extends UIElement {
} }
InnerRender(): string { InnerRender(): string {
const combine = new VerticalCombine(this.elements); const combine = new VerticalCombine(this.elements).SetClass("bordered");
combine.clss = "bordered";
return combine.Render(); return combine.Render();
} }
@ -460,7 +457,7 @@ class AllLayerComponent extends UIElement {
header: "<img src='./assets/add.svg'>", header: "<img src='./assets/add.svg'>",
content: new Button("Add a new layer", () => { content: new Button("Add a new layer", () => {
config.data.layers.push({ config.data.layers.push({
id: "", name: "",
title: { title: {
key: "*", key: "*",
render: "Title" render: "Title"
@ -506,6 +503,7 @@ export class ThemeGenerator extends UIElement {
public readonly themeObject: UIEventSource<LayoutConfigJson>; public readonly themeObject: UIEventSource<LayoutConfigJson>;
private readonly allQuestionFields: UIElement[]; private readonly allQuestionFields: UIElement[];
public url: UIEventSource<string>; public url: UIEventSource<string>;
public testurl: UIEventSource<string>;
private loginButton: Button private loginButton: Button
@ -535,7 +533,9 @@ export class ThemeGenerator extends UIElement {
if (window.location.hostname === "127.0.0.1") { if (window.location.hostname === "127.0.0.1") {
baseUrl = "http://127.0.0.1:1234"; baseUrl = "http://127.0.0.1:1234";
} }
this.url = base64.map((data) => `${baseUrl}/index.html?test=true&userlayout=${this.themeObject.data.name}#${data}`); this.url = base64.map((data) => `${baseUrl}/index.html?userlayout=${this.themeObject.data.name}#${data}`);
this.testurl = base64.map((data) => `${baseUrl}/index.html?test=true&userlayout=${this.themeObject.data.name}#${data}`);
const self = this; const self = this;
pingThemeObject = () => {self.themeObject.ping()}; pingThemeObject = () => {self.themeObject.ping()};

View file

@ -57,7 +57,7 @@ export class ValidatedTextField {
return undefined; return undefined;
} }
} }
return new And(tags).asHumanString(false); return new And(tags).asHumanString(false, false);
}, },
value: value, value: value,
startValidated: true startValidated: true

View file

@ -23,8 +23,10 @@ export class MoreScreen extends UIElement {
} }
private createLinkButton(layout: Layout, customThemeDefinition: string = undefined) { private createLinkButton(layout: Layout, customThemeDefinition: string = undefined) {
if (layout.hideFromOverview && State.state.osmConnection.userDetails.data.name !== "Pieter Vander Vennet") { if (layout.hideFromOverview) {
return undefined; if (State.state.osmConnection.GetPreference("hidden-theme-" + layout.name + "-enabled").data !== "true") {
return undefined;
}
} }
if (layout.name === State.state.layoutToUse.data.name) { if (layout.name === State.state.layoutToUse.data.name) {
return undefined; return undefined;

View file

@ -6,7 +6,7 @@ export abstract class UIElement extends UIEventSource<string>{
public readonly id: string; public readonly id: string;
public readonly _source: UIEventSource<any>; public readonly _source: UIEventSource<any>;
public clss : string = "" private clss: string[] = []
private _hideIfEmpty = false; private _hideIfEmpty = false;
@ -41,6 +41,7 @@ export abstract class UIElement extends UIEventSource<string>{
public onClick(f: (() => void)) { public onClick(f: (() => void)) {
this._onClick = f; this._onClick = f;
this.SetClass("clickable")
this.Update(); this.Update();
return this; return this;
} }
@ -107,7 +108,7 @@ export abstract class UIElement extends UIEventSource<string>{
} }
Render(): string { Render(): string {
return `<span class='uielement ${this.clss}' id='${this.id}'>${this.InnerRender()}</span>` return `<span class='uielement ${this.clss.join(" ")}' id='${this.id}'>${this.InnerRender()}</span>`
} }
AttachTo(divId: string) { AttachTo(divId: string) {
@ -142,7 +143,9 @@ export abstract class UIElement extends UIEventSource<string>{
} }
public SetClass(clss: string): UIElement { public SetClass(clss: string): UIElement {
this.clss = clss; if (this.clss.indexOf(clss) < 0) {
this.clss.push(clss);
}
return this; return this;
} }

View file

@ -7,6 +7,7 @@ import {UserDetails} from "../Logic/Osm/OsmConnection";
import {State} from "../State"; import {State} from "../State";
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import {SubtleButton} from "./Base/SubtleButton";
/** /**
* Handles and updates the user badge * Handles and updates the user badge
@ -23,7 +24,10 @@ export class UserBadge extends UIElement {
super(State.state.osmConnection.userDetails); super(State.state.osmConnection.userDetails);
this._userDetails = State.state.osmConnection.userDetails; this._userDetails = State.state.osmConnection.userDetails;
this._languagePicker = Utils.CreateLanguagePicker(); this._languagePicker = Utils.CreateLanguagePicker();
this._loginButton = Translations.t.general.loginWithOpenStreetMap.Clone().onClick(() => State.state.osmConnection.AttemptLogin()); this._loginButton = Translations.t.general.loginWithOpenStreetMap
.Clone()
.SetClass("userbadge-login")
.onClick(() => State.state.osmConnection.AttemptLogin());
this._logout = new FixedUiElement("<img src='assets/logout.svg' class='small-userbadge-icon' alt='logout'>") this._logout = new FixedUiElement("<img src='assets/logout.svg' class='small-userbadge-icon' alt='logout'>")
.onClick(() => { .onClick(() => {
State.state.osmConnection.LogOut(); State.state.osmConnection.LogOut();

View file

@ -7,6 +7,7 @@ import Translations from "./i18n/Translations";
import {VariableUiElement} from "./Base/VariableUIElement"; import {VariableUiElement} from "./Base/VariableUIElement";
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import Combine from "./Base/Combine";
export class WelcomeMessage extends UIElement { export class WelcomeMessage extends UIElement {
@ -30,28 +31,37 @@ export class WelcomeMessage extends UIElement {
} }
this.description = fromLayout((layout) => layout.welcomeMessage); this.description = fromLayout((layout) => layout.welcomeMessage);
this.plzLogIn = fromLayout((layout) => layout.gettingStartedPlzLogin); this.plzLogIn =
this.plzLogIn.onClick(()=> State.state.osmConnection.AttemptLogin()); fromLayout((layout) => layout.gettingStartedPlzLogin
.onClick(() => {State.state.osmConnection.AttemptLogin()})
);
this.welcomeBack = fromLayout((layout) => layout.welcomeBackMessage); this.welcomeBack = fromLayout((layout) => layout.welcomeBackMessage);
this.tail = fromLayout((layout) => layout.welcomeTail); this.tail = fromLayout((layout) => layout.welcomeTail);
} }
protected InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
console.log("Innerupdating welcome message")
this.plzLogIn.Update();
}
InnerRender(): string { InnerRender(): string {
let loginStatus = ""; let loginStatus = undefined;
if (State.state.featureSwitchUserbadge.data) { if (State.state.featureSwitchUserbadge.data) {
loginStatus = (State.state.osmConnection.userDetails.data.loggedIn ? this.welcomeBack : this.plzLogIn).Render(); loginStatus = (State.state.osmConnection.userDetails.data.loggedIn ? this.welcomeBack :
loginStatus = loginStatus + "<br/>" this.plzLogIn);
} }
return "<span>" + return new Combine([
this.description.Render() + this.description,
"<br/></br>" + "<br/></br>",
loginStatus + // TODO this button is broken - figure out why loginStatus,
this.tail.Render() + this.tail,
"<br/>" + "<br/>",
this.languagePicker.Render() + this.languagePicker
"</span>"; ]).Render()
} }

View file

@ -4,20 +4,135 @@
"id": "Fietsstraat", "id": "Fietsstraat",
"title": { "title": {
"render": "{name}", "render": "{name}",
"key": "name" "key": "*"
}, },
"icon": { "icon": {
"key": "" "key": "*",
"render": "./assets/themes/cyclestreets/F111.svg"
}, },
"color": { "color": {
"key": "", "key": "*",
"render": "#0000ff" "render": "#0000ff"
}, },
"description": "Een fietsstraat is een straat", "description": "Een fietsstraat is een straat waar gemotoriseerd verkeer een fietser niet mag inhalen.",
"minzoom": "13", "minzoom": "16",
"presets": [], "presets": [],
"tagRenderings": [], "tagRenderings": [],
"overpassTags": "cyclestreet=yes" "overpassTags": "cyclestreet=yes",
"width": {
"key": "*",
"addExtraTags": "",
"mappings": [],
"question": "",
"render": "10",
"type": "nat"
},
"name": "Fietsstraat"
},
{
"id": "",
"title": {
"key": "*",
"render": "Toekomstige fietsstraat",
"mappings": [
{
"then": "{name} wordt fietsstraat",
"if": "name=*"
}
]
},
"icon": {
"key": "*",
"render": "https://upload.wikimedia.org/wikipedia/commons/6/65/Belgian_road_sign_F113.svg"
},
"color": {
"key": "*",
"render": "#09f9dd"
},
"width": {
"key": "*",
"render": "5"
},
"description": "Deze straat wordt binnenkort een fietsstraat",
"minzoom": "16",
"wayHandling": 0,
"presets": [],
"tagRenderings": [{
"key": "cyclestreet:start_date",
"render": "Deze straat wordt fietsstraat op {cyclestreet:start_date}",
"type": "date",
"question": "Wanneer wordt deze straat een fietsstraat?"
}],
"name": "Toekomstige fietsstraat",
"overpassTags": "proposed:cyclestreet=yes"
},
{
"name": "Alle straten",
"title": {
"key": "*",
"render": "Straat",
"mappings": [
{
"then": "{name}",
"if": "name=*"
}
]
},
"icon": {
"key": "*",
"render": "./assets/pencil.svg"
},
"color": {
"key": "*",
"render": "#aaaaaa",
"mappings": [
{
"then": "#0000ff",
"if": "cyclestreet=yes"
},
{
"then": "#09f9dd",
"if": "proposed:cyclestreet=yes"
}
]
},
"width": {
"key": "*",
"render": "5"
},
"description": "Laag waar je een straat als fietsstraat kan markeren",
"wayHandling": 0,
"presets": [],
"tagRenderings": [
{
"mappings": [
{
"then": "Deze straat is een fietsstraat",
"if": "cyclestreet=yes&proposed:cyclestreet="
},
{
"then": "Deze straat wordt binnenkort een fietsstraat",
"if": "proposed:cyclestreet=yes&cyclestreet="
},
{
"if": "cyclestreet=&proposed:cyclestreet=",
"then": "Deze straat is geen fietsstraat"
}
],
"type": "text",
"question": "Is deze straat een fietsstraat?",
},
{
"key": "cyclestreet:start_date",
"render": "Deze straat wordt fietsstraat op {cyclestreet:start_date}",
"type": "date",
"question": "Wanneer wordt deze straat een fietsstraat?",
"condition": "proposed:cyclestreet=yes"
}
],
"overpassTags": "highway~=residential|tertiary|unclassified",
"minzoom": "13"
} }
], ],
"language": "nl", "language": "nl",
@ -27,6 +142,7 @@
"name": "Fietsstraten", "name": "Fietsstraten",
"title": "Fietsstraten", "title": "Fietsstraten",
"startLon": "3.2228", "startLon": "3.2228",
"icon": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Belgian_road_sign_F111.svg/400px-Belgian_road_sign_F111.svg.png", "icon": "./assets/themes/cyclestreets/F111.svg",
"description": "Een fietsstraat is een straat waar automobilisten geen fietsers mogen inhalen en waar een maximumsnelheid van 30km/h geldt. " "description": "Een fietsstraat is een straat waar automobilisten geen fietsers mogen inhalen en waar een maximumsnelheid van 30km/h geldt.<br/><br/>Op deze open kaart kan je alle gekende fietsstraten zien en kan je ontbrekende fietsstraten aanduiden.",
"widenFactor": 0.03
} }

View file

@ -11,7 +11,7 @@ import {FixedUiElement} from "./UI/Base/FixedUiElement";
import {State} from "./State"; import {State} from "./State";
import {TextField} from "./UI/Input/TextField"; import {TextField} from "./UI/Input/TextField";
const connection = new OsmConnection(true, new UIEventSource<string>(undefined), false); const connection = new OsmConnection(true, new UIEventSource<string>(undefined), "customThemeGenerator", false);
connection.AttemptLogin(); connection.AttemptLogin();
let hash = window.location.hash?.substr(1); let hash = window.location.hash?.substr(1);
@ -67,7 +67,7 @@ const loadFromTextField = new Button("Load", () => {
}); });
new Combine([ new Combine([
new Preview(themeGenerator.url, themeGenerator.themeObject), new Preview(themeGenerator.url, themeGenerator.testurl, themeGenerator.themeObject),
loadFrom, loadFrom,
loadFromTextField, loadFromTextField,
"<span class='alert'>Loading from the text field will erase the current theme</span>", "<span class='alert'>Loading from the text field will erase the current theme</span>",

View file

@ -109,6 +109,10 @@ form {
padding-bottom: 0.15em; padding-bottom: 0.15em;
} }
.clickable {
pointer-events: all;
}
.activate-osm-authentication { .activate-osm-authentication {
cursor: pointer; cursor: pointer;
@ -126,6 +130,8 @@ form {
border-radius: 0; border-radius: 0;
} }
#home { #home {
cursor: pointer; cursor: pointer;
} }
@ -181,7 +187,6 @@ form {
border-radius: 2em; border-radius: 2em;
border-bottom-right-radius: 1.5em; border-bottom-right-radius: 1.5em;
border-top-right-radius: 1.5em; border-top-right-radius: 1.5em;
transition: all 500ms linear;
margin: 0; margin: 0;
margin-bottom: 0.5em; margin-bottom: 0.5em;
@ -189,6 +194,25 @@ form {
pointer-events: all; pointer-events: all;
} }
.userbadge-login {
font-weight: bold;
font-size: large;
background-color: #e5f5ff !important;
height:3em;
display: inline-block;
text-align: center;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
border-bottom-right-radius: 1.5em;
border-top-right-radius: 1.5em;
margin: 0;
min-width: 20em;
pointer-events: all;
}
#userbadge-and-search { #userbadge-and-search {
display: inline-block; display: inline-block;
width: min-content; width: min-content;

View file

@ -107,10 +107,15 @@ if (layoutToUse === undefined) {
console.log("Using layout: ", layoutToUse.name); console.log("Using layout: ", layoutToUse.name);
State.state = new State(layoutToUse); State.state = new State(layoutToUse);
if (layoutToUse.hideFromOverview) {
State.state.osmConnection.GetPreference("hidden-theme-" + layoutToUse.name + "-enabled").setData("true");
}
if (layoutFromBase64 !== "false") { if (layoutFromBase64 !== "false") {
State.state.layoutDefinition = hash.substr(1); State.state.layoutDefinition = hash.substr(1);
State.state.osmConnection.OnLoggedIn(() => { State.state.osmConnection.OnLoggedIn(() => {
State.state.osmConnection.GetLongPreference("installed-theme-"+layoutToUse.name).setData(State.state.layoutDefinition); State.state.osmConnection.GetLongPreference("installed-theme-" + layoutToUse.name).setData(State.state.layoutDefinition);
}) })
} }
InitUiElements.InitBaseMap(); InitUiElements.InitBaseMap();

View file

@ -4,6 +4,12 @@
"target": "es5", "target": "es5",
"sourceMap": true, "sourceMap": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"lib": [
"dom",
"es5",
"scripthost",
"es2015.collection"
]
}, },
"exclude": [ "exclude": [
"node_modules" "node_modules"