Custom themes now stick to the user account and can be revisited, small improvements

This commit is contained in:
Pieter Vander Vennet 2020-08-26 15:36:04 +02:00
parent bf6eae9af1
commit 4a0970a71f
23 changed files with 556 additions and 1748 deletions

View file

@ -1,7 +1,6 @@
import {LayerDefinition} from "./LayerDefinition";
import {Layout} from "./Layout";
import {All} from "./Layouts/All";
import {CustomLayout} from "../Logic/CustomLayers";
import {Groen} from "./Layouts/Groen";
import Cyclofix from "./Layouts/Cyclofix";
import {StreetWidth} from "./Layouts/StreetWidth";
@ -16,13 +15,14 @@ import * as bookcases from "../assets/themes/bookcases/Bookcases.json";
import * as aed from "../assets/themes/aed/aed.json";
import * as toilets from "../assets/themes/toilets/toilets.json";
import * as artworks from "../assets/themes/artwork/artwork.json";
import {PersonalLayout} from "../Logic/PersonalLayout";
export class AllKnownLayouts {
public static allLayers: Map<string, LayerDefinition> = undefined;
public static layoutsList: Layout[] = [
new CustomLayout(),
new PersonalLayout(),
new Natuurpunt(),
new GRB(),
new Cyclofix(),

View file

@ -10,6 +10,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {TagDependantUIElementConstructor} from "../UIElementConstructor";
import {Map} from "../Layers/Map";
import {UIElement} from "../../UI/UIElement";
import Translations from "../../UI/i18n/Translations";
export interface TagRenderingConfigJson {
@ -246,7 +247,7 @@ export class CustomLayoutFromJSON {
json.id,
{
description: t(json.description),
name: t(json.title.render),
name: Translations.WT(t(json.title.render)).txt.replace(/[^a-zA-Z0-9-_]/g, ''),
icon: icon,
minzoom: parseInt(""+json.minzoom),
title: tr(json.title),

View file

@ -14,6 +14,7 @@ export class Layout {
public icon: string = "./assets/logo.svg";
public title: UIElement;
public maintainer: string;
public version: string;
public description: string | UIElement;
public socialImage: string = "";

View file

@ -20,13 +20,13 @@ import {WelcomeMessage} from "./UI/WelcomeMessage";
import {Img} from "./UI/Img";
import {DropDown} from "./UI/Input/DropDown";
import {LayerSelection} from "./UI/LayerSelection";
import {CustomLayersPanel} from "./Logic/CustomLayersPanel";
import {CustomLayout} from "./Logic/CustomLayers";
import {Preset} from "./Customizations/LayerDefinition";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import {LayerUpdater} from "./Logic/LayerUpdater";
import {UIEventSource} from "./Logic/UIEventSource";
import {QueryParameters} from "./Logic/Web/QueryParameters";
import {PersonalLayout} from "./Logic/PersonalLayout";
import {PersonalLayersPanel} from "./Logic/PersonalLayersPanel";
export class InitUiElements {
@ -50,8 +50,8 @@ export class InitUiElements {
const layoutToUse = State.state.layoutToUse.data;
let welcome: UIElement = new WelcomeMessage();
if (layoutToUse.name === CustomLayout.NAME) {
welcome = new CustomLayersPanel();
if (layoutToUse.name === PersonalLayout.NAME) {
welcome = new PersonalLayersPanel();
}
const tabs = [

View file

@ -1,88 +0,0 @@
import {State} from "../State";
export class CustomLayersState {
static RemoveFavouriteLayer(layer: string) {
State.state.GetFilteredLayerFor(layer)?.isDisplayed?.setData(false);
const favs = State.state.favourteLayers.data;
const ind = favs.indexOf(layer);
if (ind < 0) {
return;
}
favs.splice(ind, 1);
const osmConnection = State.state.osmConnection;
const count = osmConnection.GetPreference("mapcomplete-custom-layer-count");
for (let i = 0; i < favs.length; i++) {
const layerIDescr = osmConnection.GetPreference("mapcomplete-custom-layer-" + i);
layerIDescr.setData(favs[i]);
}
count.setData("" + favs.length)
}
static AddFavouriteLayer(layer: string) {
State.state.GetFilteredLayerFor(layer)?.isDisplayed?.setData(true);
const favs = State.state.favourteLayers.data;
const ind = favs.indexOf(layer);
if (ind >= 0) {
return;
}
console.log("Adding fav layer", layer);
favs.push(layer);
const osmConnection = State.state.osmConnection;
const count = osmConnection.GetPreference("mapcomplete-custom-layer-count");
if (count.data === undefined || isNaN(Number(count.data))) {
count.data = "0";
}
const lastId = Number(count.data);
for (let i = 0; i < lastId; i++) {
const layerIDescr = osmConnection.GetPreference("mapcomplete-custom-layer-" + i);
if (layerIDescr.data === undefined || layerIDescr.data === "") {
// An earlier item was removed -> overwrite it
layerIDescr.setData(layer);
count.ping();
return;
}
}
// No empty slot found -> create a new one
const layerIDescr = osmConnection.GetPreference("mapcomplete-custom-layer-" + lastId);
layerIDescr.setData(layer);
count.setData((lastId + 1) + "");
}
static InitFavouriteLayers(state: State) {
const osmConnection = state.osmConnection;
const count = osmConnection.GetPreference("mapcomplete-custom-layer-count");
const favs = state.favourteLayers.data;
let changed = false;
count.addCallback((countStr) => {
console.log("Updating favourites")
if (countStr === undefined) {
return;
}
let countI = Number(countStr);
if (isNaN(countI)) {
countI = 999;
}
for (let i = 0; i < countI; i++) {
const layerId = osmConnection.GetPreference("mapcomplete-custom-layer-" + i).data;
if (layerId !== undefined && layerId !== "" && favs.indexOf(layerId) < 0) {
state.favourteLayers.data.push(layerId);
changed = true;
}
}
if (changed) {
state.favourteLayers.ping();
}
})
}
}

View file

@ -0,0 +1,131 @@
import {State} from "../../State";
import {UserDetails} from "./OsmConnection";
import {UIEventSource} from "../UIEventSource";
export class ChangesetHandler {
private _dryRun: boolean;
private userDetails: UIEventSource<UserDetails>;
private auth: any;
constructor(dryRun: boolean, userDetails: UIEventSource<UserDetails>, auth) {
this._dryRun = dryRun;
this.userDetails = userDetails;
this.auth = auth;
if (dryRun) {
console.log("DRYRUN ENABLED");
}
}
public UploadChangeset(generateChangeXML: (csid: string) => string,
handleMapping: (idMapping: any) => void,
continuation: () => void) {
if (this._dryRun) {
console.log("NOT UPLOADING as dryrun is true");
var changesetXML = generateChangeXML("123456");
console.log(changesetXML);
continuation();
return;
}
const self = this;
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();
}
private OpenChangeset(continuation: (changesetId: string) => void) {
const layout = State.state.layoutToUse.data;
this.auth.xhr({
method: 'PUT',
path: '/api/0.6/changeset/create',
options: {header: {'Content-Type': 'text/xml'}},
content: [`<osm><changeset>`,
`<tag k="created_by" v="MapComplete ${State.vNumber}" />`,
`<tag k="comment" v="Adding data with #MapComplete"/>`,
`<tag k="theme" v="${layout.name}"/>`,
layout.maintainer !== undefined ? `<tag k="theme-creator" v="${layout.maintainer}"/>` : "",
`</changeset></osm>`].join("")
}, function (err, response) {
if (response === undefined) {
console.log("err", err);
alert("Could not upload change (opening failed). Please file a bug report")
return;
} else {
continuation(response);
}
});
}
private AddChange(changesetId: string,
changesetXML: string,
continuation: ((changesetId: string, idMapping: any) => void)) {
this.auth.xhr({
method: 'POST',
options: {header: {'Content-Type': 'text/xml'}},
path: '/api/0.6/changeset/' + changesetId + '/upload',
content: changesetXML
}, function (err, response) {
if (response == null) {
console.log("err", err);
return;
}
const mapping = ChangesetHandler.parseUploadChangesetResponse(response);
console.log("Uploaded changeset ", changesetId);
continuation(changesetId, mapping);
});
}
private CloseChangeset(changesetId: string, continuation: (() => void)) {
console.log("closing");
this.auth.xhr({
method: 'PUT',
path: '/api/0.6/changeset/' + changesetId + '/close',
}, function (err, response) {
if (response == null) {
console.log("err", err);
}
console.log("Closed changeset ", changesetId);
if (continuation !== undefined) {
continuation();
}
});
}
private static parseUploadChangesetResponse(response: XMLDocument) {
const nodes = response.getElementsByTagName("node");
const mapping = {};
// @ts-ignore
for (const node of nodes) {
const oldId = parseInt(node.attributes.old_id.value);
const newId = parseInt(node.attributes.new_id.value);
if (oldId !== undefined && newId !== undefined &&
!isNaN(oldId) && !isNaN(newId)) {
mapping["node/" + oldId] = "node/" + newId;
}
}
return mapping;
}
}

View file

@ -1,8 +1,10 @@
// @ts-ignore
import osmAuth from "osm-auth";
import {UIEventSource} from "../UIEventSource";
import {CustomLayersState} from "../CustomLayersState";
import {State} from "../../State";
import {All} from "../../Customizations/Layouts/All";
import {OsmPreferences} from "./OsmPreferences";
import {ChangesetHandler} from "./ChangesetHandler";
export class UserDetails {
@ -22,6 +24,11 @@ export class OsmConnection {
public userDetails: UIEventSource<UserDetails>;
private _dryRun: boolean;
public _preferencesHandler: OsmPreferences;
private _changesetHandler: ChangesetHandler;
private _onLoggedIn : ((userDetails: UserDetails) => void)[] = [];
constructor(dryRun: boolean, oauth_token: UIEventSource<string>, singlePage: boolean = true) {
let pwaStandAloneMode = false;
@ -61,16 +68,18 @@ export class OsmConnection {
this.userDetails.data.dryRun = dryRun;
this._dryRun = dryRun;
this._preferencesHandler = new OsmPreferences(this.auth, this);
this._changesetHandler = new ChangesetHandler(dryRun, this.userDetails, this.auth);
if (oauth_token.data !== undefined) {
console.log(oauth_token.data)
const self = this;
this.auth.bootstrapToken(oauth_token.data,
this.auth.bootstrapToken(oauth_token.data,
(x) => {
console.log("Called back: ", x)
self.AttemptLogin();
}, this.auth);
oauth_token.setData(undefined);
}
@ -79,15 +88,27 @@ export class OsmConnection {
} else {
console.log("Not authenticated");
}
if (dryRun) {
console.log("DRYRUN ENABLED");
}
}
public UploadChangeset(generateChangeXML: (csid: string) => string,
handleMapping: (idMapping: any) => void,
continuation: () => void) {
this._changesetHandler.UploadChangeset(generateChangeXML, handleMapping, continuation);
}
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
return this._preferencesHandler.GetPreference(key, prefix);
}
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
return this._preferencesHandler.GetLongPreference(key, prefix);
}
public OnLoggedIn(action: (userDetails: UserDetails) => void){
this._onLoggedIn.push(action);
}
public LogOut() {
this.auth.logout();
this.userDetails.data.loggedIn = false;
@ -112,7 +133,6 @@ export class OsmConnection {
return;
}
self.UpdatePreferences();
self.CheckForMessagesContinuously();
// details is an XML DOM of user details
@ -143,8 +163,12 @@ export class OsmConnection {
const messages = userInfo.getElementsByTagName("messages")[0].getElementsByTagName("received")[0];
data.unreadMessages = parseInt(messages.getAttribute("unread"));
data.totalMessages = parseInt(messages.getAttribute("count"));
self.userDetails.ping();
for (const action of self._onLoggedIn) {
action(self.userDetails.data);
}
self.userDetails.ping();
});
}
@ -159,208 +183,5 @@ export class OsmConnection {
}, 5 * 60 * 1000);
}
public preferences = new UIEventSource<any>({});
public preferenceSources : any = {}
public GetPreference(key: string, prefix : string = "mapcomplete-") : UIEventSource<string>{
key = prefix+key;
if (this.preferenceSources[key] !== undefined) {
return this.preferenceSources[key];
}
if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) {
this.UpdatePreferences();
}
const pref = new UIEventSource<string>(this.preferences.data[key]);
pref.addCallback((v) => {
this.SetPreference(key, v);
});
this.preferences.addCallback((prefs) => {
if (prefs[key] !== undefined) {
pref.setData(prefs[key]);
}
});
this.preferenceSources[key] = pref;
return pref;
}
private UpdatePreferences() {
const self = this;
this.auth.xhr({
method: 'GET',
path: '/api/0.6/user/preferences'
}, function (error, value: XMLDocument) {
if(error){
console.log("Could not load preferences", error);
return;
}
const prefs = value.getElementsByTagName("preference");
for (let i = 0; i < prefs.length; i++) {
const pref = prefs[i];
const k = pref.getAttribute("k");
const v = pref.getAttribute("v");
self.preferences.data[k] = v;
}
self.preferences.ping();
});
}
private SetPreference(k:string, v:string) {
if(!this.userDetails.data.loggedIn){
console.log("Not saving preference: user not logged in");
return;
}
if (this.preferences.data[k] === v) {
console.log("Not updating preference", k, " to ", v, "not changed");
return;
}
console.log("Updating preference", k, " to ", v);
this.preferences.data[k] = v;
this.preferences.ping();
if(v === ""){
this.auth.xhr({
method: 'DELETE',
path: '/api/0.6/user/preferences/' + k,
options: {header: {'Content-Type': 'text/plain'}},
}, function (error, result) {
if (error) {
console.log("Could not remove preference", error);
return;
}
console.log("Preference removed!", result == "" ? "OK" : result);
});
}
this.auth.xhr({
method: 'PUT',
path: '/api/0.6/user/preferences/' + k,
options: {header: {'Content-Type': 'text/plain'}},
content: v
}, function (error, result) {
if (error) {
console.log("Could not set preference", error);
return;
}
console.log("Preference written!", result == "" ? "OK" : result);
});
}
private static parseUploadChangesetResponse(response: XMLDocument) {
const nodes = response.getElementsByTagName("node");
const mapping = {};
// @ts-ignore
for (const node of nodes) {
const oldId = parseInt(node.attributes.old_id.value);
const newId = parseInt(node.attributes.new_id.value);
if (oldId !== undefined && newId !== undefined &&
!isNaN(oldId) && !isNaN(newId)) {
mapping["node/" + oldId] = "node/" + newId;
}
}
return mapping;
}
public UploadChangeset(generateChangeXML: (csid: string) => string,
handleMapping: (idMapping: any) => void,
continuation: () => void) {
if (this._dryRun) {
console.log("NOT UPLOADING as dryrun is true");
var changesetXML = generateChangeXML("123456");
console.log(changesetXML);
continuation();
return;
}
const self = this;
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();
}
private OpenChangeset(continuation: (changesetId: string) => void) {
const layout = State.state.layoutToUse.data;
this.auth.xhr({
method: 'PUT',
path: '/api/0.6/changeset/create',
options: {header: {'Content-Type': 'text/xml'}},
content: [`<osm><changeset>`,
`<tag k="created_by" v="MapComplete ${State.vNumber}" />`,
`<tag k="comment" v="Adding data with #MapComplete"/>`,
`<tag k="theme" v="${layout.name}"/>`,
layout.maintainer !== undefined ? `<tag k="theme-creator" v="${layout.maintainer}"/>` : "",
`</changeset></osm>`].join("")
}, function (err, response) {
if (response === undefined) {
console.log("err", err);
alert("Could not upload change (opening failed). Please file a bug report")
return;
} else {
continuation(response);
}
});
}
private AddChange(changesetId: string,
changesetXML: string,
continuation: ((changesetId: string, idMapping: any) => void)){
this.auth.xhr({
method: 'POST',
options: { header: { 'Content-Type': 'text/xml' } },
path: '/api/0.6/changeset/'+changesetId+'/upload',
content: changesetXML
}, function (err, response) {
if (response == null) {
console.log("err", err);
return;
}
const mapping = OsmConnection.parseUploadChangesetResponse(response);
console.log("Uploaded changeset ", changesetId);
continuation(changesetId, mapping);
});
}
private CloseChangeset(changesetId: string, continuation : (() => void)) {
console.log("closing");
this.auth.xhr({
method: 'PUT',
path: '/api/0.6/changeset/'+changesetId+'/close',
}, function (err, response) {
if (response == null) {
console.log("err", err);
}
console.log("Closed changeset ", changesetId);
if(continuation !== undefined){
continuation();
}
});
}
}

176
Logic/Osm/OsmPreferences.ts Normal file
View file

@ -0,0 +1,176 @@
import {UIEventSource} from "../UIEventSource";
import {OsmConnection, UserDetails} from "./OsmConnection";
import {All} from "../../Customizations/Layouts/All";
import {Utils} from "../../Utils";
export class OsmPreferences {
private auth: any;
private userDetails: UIEventSource<UserDetails>;
public preferences = new UIEventSource<any>({});
public preferenceSources: any = {}
constructor(auth, osmConnection: OsmConnection) {
this.auth = auth;
this.userDetails = osmConnection.userDetails;
const self = this;
osmConnection.OnLoggedIn(() => self.UpdatePreferences());
}
/**
* OSM preferences can be at most 255 chars
* @param key
* @param prefix
* @constructor
*/
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
const source = new UIEventSource<string>(undefined);
const allStartWith = prefix + key + "-combined";
// Gives the number of combined preferences
const length = this.GetPreference(allStartWith + "-length", "");
console.log("Getting long pref " + prefix + key);
const self = this;
source.addCallback(str => {
if (str === undefined) {
for (const prefKey in self.preferenceSources) {
if (prefKey.startsWith(allStartWith)) {
self.GetPreference(prefKey, "").setData(undefined);
}
}
return;
}
let i = 0;
while (str !== "") {
self.GetPreference(allStartWith + "-" + i, "").setData(str.substr(0, 255));
str = str.substr(255);
i++;
}
length.setData("" + i);
});
function updateData(l: number) {
if (l === undefined) {
source.setData(undefined);
return;
}
const length = Number(l);
let str = "";
for (let i = 0; i < length; i++) {
str += self.GetPreference(allStartWith + "-" + i, "").data;
}
source.setData(str);
source.ping();
console.log("Long preference ", key, " has ", str.length, " chars");
}
length.addCallback(l => {
updateData(Number(l));
});
updateData(Number(length.data));
return source;
}
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
key = prefix + key;
if(key.length >= 255){
throw "Preferences: key length to big";
}
if (this.preferenceSources[key] !== undefined) {
return this.preferenceSources[key];
}
if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) {
this.UpdatePreferences();
}
const pref = new UIEventSource<string>(this.preferences.data[key]);
pref.addCallback((v) => {
this.SetPreference(key, v);
});
this.preferences.addCallback((prefs) => {
if (prefs[key] !== undefined) {
pref.setData(prefs[key]);
}
});
this.preferenceSources[key] = pref;
return pref;
}
private UpdatePreferences() {
const self = this;
this.auth.xhr({
method: 'GET',
path: '/api/0.6/user/preferences'
}, function (error, value: XMLDocument) {
if (error) {
console.log("Could not load preferences", error);
return;
}
const prefs = value.getElementsByTagName("preference");
for (let i = 0; i < prefs.length; i++) {
const pref = prefs[i];
const k = pref.getAttribute("k");
const v = pref.getAttribute("v");
self.preferences.data[k] = v;
}
self.preferences.ping();
});
}
private SetPreference(k: string, v: string) {
if (!this.userDetails.data.loggedIn) {
console.log("Not saving preference: user not logged in");
return;
}
if (this.preferences.data[k] === v) {
console.log("Not updating preference", k, " to ", v, "not changed");
return;
}
console.log("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15));
this.preferences.data[k] = v;
this.preferences.ping();
if (v === "") {
this.auth.xhr({
method: 'DELETE',
path: '/api/0.6/user/preferences/' + k,
options: {header: {'Content-Type': 'text/plain'}},
}, function (error, result) {
if (error) {
console.log("Could not remove preference", error);
return;
}
console.log("Preference removed!", result == "" ? "OK" : result);
});
return;
}
this.auth.xhr({
method: 'PUT',
path: '/api/0.6/user/preferences/' + k,
options: {header: {'Content-Type': 'text/plain'}},
content: v
}, function (error, result) {
if (error) {
console.log("Could not set preference", error);
return;
}
console.log("Preference written!", result == "" ? "OK" : result);
});
}
}

View file

@ -6,40 +6,38 @@ import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import Combine from "../UI/Base/Combine";
import {Img} from "../UI/Img";
import {CheckBox} from "../UI/Input/CheckBox";
import {CustomLayersState} from "./CustomLayersState";
import {VerticalCombine} from "../UI/Base/VerticalCombine";
import {FixedUiElement} from "../UI/Base/FixedUiElement";
import {CustomLayout} from "./CustomLayers";
import {SubtleButton} from "../UI/Base/SubtleButton";
import {PersonalLayout} from "./PersonalLayout";
export class CustomLayersPanel extends UIElement {
export class PersonalLayersPanel extends UIElement {
private checkboxes: UIElement[] = [];
private updateButton : UIElement;
private updateButton: UIElement;
constructor() {
super(State.state.favourteLayers);
super(State.state.favouriteLayers);
this.ListenTo(State.state.osmConnection.userDetails);
const t = Translations.t.favourite;
const favs = State.state.favourteLayers.data;
const favs = State.state.favouriteLayers.data ?? [];
this.updateButton = new SubtleButton("./assets/reload.svg", t.reload)
.onClick(() => {
State.state.layerUpdater.ForceRefresh();
CustomLayersState.InitFavouriteLayers(State.state);
State.state.layoutToUse.ping();
})
const controls = new Map<string, UIEventSource<boolean>>();
for (const layout of AllKnownLayouts.layoutsList) {
if(layout.name === CustomLayout.NAME){
if (layout.name === PersonalLayout.NAME) {
continue;
}
if (layout.hideFromOverview &&
if (layout.hideFromOverview &&
State.state.osmConnection.userDetails.data.name !== "Pieter Vander Vennet") {
continue
}
@ -86,18 +84,20 @@ export class CustomLayersPanel extends UIElement {
controls[layer.id] = cb.isEnabled;
cb.isEnabled.addCallback((isEnabled) => {
const favs = State.state.favouriteLayers;
if (isEnabled) {
CustomLayersState.AddFavouriteLayer(layer.id)
favs.data.push(layer.id);
} else {
CustomLayersState.RemoveFavouriteLayer(layer.id);
favs.data.splice(favs.data.indexOf(layer.id), 1);
}
favs.ping();
})
this.checkboxes.push(cb);
}
State.state.favourteLayers.addCallback((layers) => {
State.state.favouriteLayers.addCallback((layers) => {
for (const layerId of layers) {
controls[layerId]?.setData(true);
}

View file

@ -1,13 +1,13 @@
import {Layout} from "../Customizations/Layout";
import Translations from "../UI/i18n/Translations";
export class CustomLayout extends Layout {
export class PersonalLayout extends Layout {
public static NAME: string = "personal";
constructor() {
super(
CustomLayout.NAME,
PersonalLayout.NAME,
["en"],
Translations.t.favourite.title,
[],
@ -20,7 +20,4 @@ export class CustomLayout extends Layout {
this.icon = "./assets/star.svg"
}
}
}

View file

@ -68,9 +68,9 @@ A typical user journey would be:
## License
GPL + pingback.
GPLv3.0 + recommended pingback.
I love it to see where the project ends up. You are free to reuse the software (under GPL) but, when you have made your own change and are using it, I would like to know about it. Drop me a line, give a pingback in the issues, ...
I love it to see where the project ends up. You are free to reuse the software (under GPL) but, when you have made your own change and are using it, I would like to know about it. Drop me a line, give a pingback in the issues,...
## Dev

View file

@ -8,7 +8,6 @@ import {OsmConnection} from "./Logic/Osm/OsmConnection";
import Locale from "./UI/i18n/Locale";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import Translations from "./UI/i18n/Translations";
import {CustomLayersState} from "./Logic/CustomLayersState";
import {FilteredLayer} from "./Logic/FilteredLayer";
import {LayerUpdater} from "./Logic/LayerUpdater";
import {UIEventSource} from "./Logic/UIEventSource";
@ -24,7 +23,7 @@ export class State {
// The singleton of the global state
public static state: State;
public static vNumber = "0.0.6c";
public static vNumber = "0.0.6d";
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
@ -38,11 +37,7 @@ export class State {
public static runningFromConsole: boolean = false;
/**
THe layout to use
*/
public readonly layoutToUse = new UIEventSource<Layout>(undefined);
public layoutDefinition : string;
/**
The mapping from id -> UIEventSource<properties>
@ -60,13 +55,15 @@ export class State {
The user credentials
*/
public osmConnection: OsmConnection;
public layerUpdater : LayerUpdater;
public favouriteLayers: UIEventSource<string[]>;
public layerUpdater: LayerUpdater;
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([])
public presets: UIEventSource<Preset[]> = new UIEventSource<Preset[]>([])
/**
* The message that should be shown at the center of the screen
*/
@ -123,77 +120,96 @@ export class State {
/** After this many milliseconds without changes, saves are sent of to OSM
*/
public readonly saveTimeout = new UIEventSource<number>(30 * 1000);
/**
* Layers can be marked as favourites, they show up in a custom layout
*/
public favourteLayers: UIEventSource<string[]> = new UIEventSource<string[]>([])
public layoutDefinition: string;
constructor(layoutToUse: Layout) {
this.layoutToUse = new UIEventSource<Layout>(layoutToUse);
const self = this;
this.layoutToUse.setData(layoutToUse)
this.locationControl = new UIEventSource<{ lat: number, lon: number, zoom: number }>({
zoom: Utils.asFloat(this.zoom.data) ?? layoutToUse.startzoom,
lat: Utils.asFloat(this.lat.data) ?? layoutToUse.startLat,
lon: Utils.asFloat(this.lon.data) ?? layoutToUse.startLon
zoom: Utils.asFloat(this.zoom.data),
lat: Utils.asFloat(this.lat.data),
lon: Utils.asFloat(this.lon.data),
}).addCallback((latlonz) => {
this.zoom.setData(latlonz.zoom.toString());
this.lat.setData(latlonz.lat.toString().substr(0, 6));
this.lon.setData(latlonz.lon.toString().substr(0, 6));
})
});
this.layoutToUse.addCallback(layoutToUse => {
const lcd = self.locationControl.data;
lcd.zoom = lcd.zoom ?? layoutToUse?.startzoom;
lcd.lat = lcd.lat ?? layoutToUse?.startLat;
lcd.lon = lcd.lon ?? layoutToUse?.startLon;
self.locationControl.ping();
});
const self = this;
function featSw(key: string, deflt: (layout: Layout) => boolean): UIEventSource<boolean> {
const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined);
// I'm so sorry about someone trying to decipher this
// It takes the current layout, extracts the default value for this query paramter. A query parameter event source is then retreived and flattened
return UIEventSource.flatten(
self.layoutToUse.map((layout) =>
QueryParameters.GetQueryParameter(key, "" + deflt(layout)).map((str) => str === undefined ? deflt(layout) : str !== "false")), [queryParameterSource]);
self.layoutToUse.map((layout) => {
const defaultValue = deflt(layout);
const queryParam = QueryParameters.GetQueryParameter(key, "" + defaultValue)
return queryParam.map((str) => str === undefined ? defaultValue : (str !== "false"));
}), [queryParameterSource]);
}
this.featureSwitchUserbadge = featSw("fs-userbadge", (layoutToUse) => layoutToUse?.enableUserBadge);
this.featureSwitchSearch = featSw("fs-search", (layoutToUse) => layoutToUse?.enableSearch);
this.featureSwitchLayers = featSw("fs-layers", (layoutToUse) => layoutToUse?.enableLayers);
this.featureSwitchAddNew = featSw("fs-add-new", (layoutToUse) => layoutToUse?.enableAdd);
this.featureSwitchUserbadge = featSw("fs-userbadge", (layoutToUse) => layoutToUse?.enableUserBadge ?? true);
this.featureSwitchSearch = featSw("fs-search", (layoutToUse) => layoutToUse?.enableSearch ?? true);
this.featureSwitchLayers = featSw("fs-layers", (layoutToUse) => layoutToUse?.enableLayers ?? true);
this.featureSwitchAddNew = featSw("fs-add-new", (layoutToUse) => layoutToUse?.enableAdd ?? true);
this.featureSwitchWelcomeMessage = featSw("fs-welcome-message", () => true);
this.featureSwitchIframe = featSw("fs-iframe", () => false);
this.featureSwitchMoreQuests = featSw("fs-more-quests", () => layoutToUse?.enableMoreQuests);
this.featureSwitchShareScreen = featSw("fs-share-screen", () => layoutToUse?.enableShareScreen);
this.featureSwitchGeolocation = featSw("fs-geolocation", () => layoutToUse?.enableGeolocation);
this.featureSwitchMoreQuests = featSw("fs-more-quests", (layoutToUse) => layoutToUse?.enableMoreQuests ?? true);
this.featureSwitchShareScreen = featSw("fs-share-screen", (layoutToUse) => layoutToUse?.enableShareScreen ?? true);
this.featureSwitchGeolocation = featSw("fs-geolocation", (layoutToUse) => layoutToUse?.enableGeolocation ?? true);
this.osmConnection = new OsmConnection(
QueryParameters.GetQueryParameter("test", "false").data === "true",
QueryParameters.GetQueryParameter("oauth_token", undefined)
);
CustomLayersState.InitFavouriteLayers(this);
this.favouriteLayers = this.osmConnection.GetLongPreference("favouriteLayers").map(
str => Utils.Dedup(str?.split(";")) ?? [],
[], layers => Utils.Dedup(layers)?.join(";")
);
Locale.language.syncWith(this.osmConnection.GetPreference("language"));
Locale.language.addCallback((currentLanguage) => {
if (layoutToUse.supportedLanguages.indexOf(currentLanguage) < 0) {
const layoutToUse = self.layoutToUse.data;
if (layoutToUse === undefined) {
return;
}
if (this.layoutToUse.data.supportedLanguages.indexOf(currentLanguage) < 0) {
console.log("Resetting language to", layoutToUse.supportedLanguages[0], "as", currentLanguage, " is unsupported")
// The current language is not supported -> switch to a supported one
Locale.language.setData(layoutToUse.supportedLanguages[0]);
}
}).ping()
document.title = Translations.W(layoutToUse.title).InnerRender();
Locale.language.addCallback(e => {
document.title = Translations.W(layoutToUse.title).InnerRender();
})
this.layoutToUse.map((layoutToUse) => {
if (layoutToUse === undefined) {
return "MapComplete";
}
return Translations.W(layoutToUse.title).InnerRender()
}, [Locale.language]
).addCallback((title) => {
document.title = title
});
this.allElements = new ElementStorage();
this.changes = new Changes(this);
if(State.runningFromConsole){
if (State.runningFromConsole) {
console.warn("running from console - not initializing map. Assuming test.html");
return;
}

View file

@ -13,7 +13,7 @@ export class VerticalCombine extends UIElement {
InnerRender(): string {
let html = "";
for (const element of this._elements) {
if (!element.IsEmpty()) {
if (element!== undefined && !element.IsEmpty()) {
html += "<div>" + element.Render() + "</div>";
}
}

View file

@ -5,8 +5,12 @@ import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import Combine from "./Base/Combine";
import {SubtleButton} from "./Base/SubtleButton";
import {State} from "../State";
import {CustomLayout} from "../Logic/CustomLayers";
import {VariableUiElement} from "./Base/VariableUIElement";
import {PersonalLayout} from "../Logic/PersonalLayout";
import {FixedUiElement} from "./Base/FixedUiElement";
import {Layout} from "../Customizations/Layout";
import {CustomLayoutFromJSON} from "../Customizations/JSON/CustomLayoutFromJSON";
import {All} from "../Customizations/Layouts/All";
export class MoreScreen extends UIElement {
@ -14,6 +18,48 @@ export class MoreScreen extends UIElement {
constructor() {
super(State.state.locationControl);
this.ListenTo(State.state.osmConnection.userDetails);
this.ListenTo(State.state.osmConnection._preferencesHandler.preferences);
}
private createLinkButton(layout: Layout, customThemeDefinition: string = undefined) {
if (layout.hideFromOverview && State.state.osmConnection.userDetails.data.name !== "Pieter Vander Vennet") {
return undefined;
}
if (layout.name === State.state.layoutToUse.data.name) {
return undefined;
}
if (layout.name === PersonalLayout.NAME) {
return undefined;
}
const currentLocation = State.state.locationControl.data;
let linkText =
`./${layout.name}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
linkText = `./index.html?layout=${layout.name}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
}
if (customThemeDefinition) {
linkText = `./index.html?userlayout=${layout.name}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}`
}
let description = Translations.W(layout.description);
if (description !== undefined) {
description = new Combine(["<br/>", description]);
}
const link =
new SubtleButton(layout.icon,
new Combine([
"<b>",
Translations.W(layout.title),
"</b>",
description ?? "",
]), {url: linkText, newTab: false})
return link;
}
InnerRender(): string {
@ -28,7 +74,7 @@ export class MoreScreen extends UIElement {
return tr.requestATheme.Render();
}
return new SubtleButton("./assets/pencil.svg", tr.createYourOwnTheme, {
url: "https://pietervdvn.github.io/MapComplete/customGenerator.html",
url: "./customGenerator.html",
newTab: false
}).Render();
})
@ -53,40 +99,44 @@ export class MoreScreen extends UIElement {
for (const k in AllKnownLayouts.allSets) {
const layout = AllKnownLayouts.allSets[k]
if (layout.hideFromOverview && State.state.osmConnection.userDetails.data.name !== "Pieter Vander Vennet") {
continue
els.push(this.createLinkButton(AllKnownLayouts.allSets[k]));
}
const installedThemes = State.state.osmConnection._preferencesHandler.preferences.map(allPreferences => {
const installedThemes = [];
if(allPreferences === undefined){
return installedThemes;
}
if (layout.name === State.state.layoutToUse.data.name) {
for (const allPreferencesKey in allPreferences) {
"mapcomplete-installed-theme-Superficie-combined-length"
const themename = allPreferencesKey.match(/^mapcomplete-installed-theme-(.*)-combined-length$/);
if(themename){
installedThemes.push(themename[1]);
}
}
return installedThemes;
})
const customThemesNames = installedThemes.data ?? [];
if (customThemesNames !== []) {
els.push(Translations.t.general.customThemeIntro)
}
console.log(customThemesNames);
for (const installedThemeName of customThemesNames) {
if(installedThemeName === ""){
continue;
}
if (layout.name === CustomLayout.NAME) {
continue;
const customThemeDefinition = State.state.osmConnection.GetLongPreference("installed-theme-" + installedThemeName);
try {
const layout = CustomLayoutFromJSON.FromQueryParam(customThemeDefinition.data);
els.push(this.createLinkButton(layout, customThemeDefinition.data));
} catch (e) {
console.log(customThemeDefinition.data);
console.warn("Could not parse custom layout from preferences: ", installedThemeName, e);
}
const currentLocation = State.state.locationControl.data;
let linkText =
`./${layout.name}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
linkText = `./index.html?layout=${layout.name}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
}
let description = Translations.W(layout.description);
if (description !== undefined) {
description = new Combine(["<br/>", description]);
}
const link =
new SubtleButton(layout.icon,
new Combine([
"<b>",
Translations.W(layout.title),
"</b>",
description ?? "",
]), {url: linkText, newTab: false});
els.push(link)
}

View file

@ -22,11 +22,11 @@ export class WelcomeMessage extends UIElement {
constructor() {
super(State.state.osmConnection.userDetails);
this.languagePicker = Utils.CreateLanguagePicker(Translations.t.general.pickLanguage);
this.ListenTo(Locale.language);
this.languagePicker = Utils.CreateLanguagePicker(Translations.t.general.pickLanguage);
function fromLayout(f: (layout: Layout) => (string | UIElement)): UIElement {
return Translations.W(f(State.state.layoutToUse.data))
return Translations.W(f(State.state.layoutToUse.data));
}
this.description = fromLayout((layout) => layout.welcomeMessage);

View file

@ -901,7 +901,8 @@ export default class Translations {
nl: " of <a href='https://www.openstreetmap.org/user/new' target='_blank'>maak een nieuwe account aan</a> ",
fr: " ou <a href='https://www.openstreetmap.org/user/new' target='_blank'>registrez vous</a>"
}),
noTagsSelected: new T({en: "No tags selected"})
noTagsSelected: new T({en: "No tags selected"}),
customThemeIntro: new T({en:"<h3>Custom themes</h3>These are previously visited user-generated themes."})
},
favourite: {

View file

@ -72,5 +72,18 @@ export class Utils {
}
return str.substr(0, l - 3)+"...";
}
public static Dedup(arr: string[]):string[]{
if(arr === undefined){
return undefined;
}
const newArr = [];
for (const string of arr) {
if(newArr.indexOf(string) < 0){
newArr.push(string);
}
}
return newArr;
}
}

View file

@ -1,6 +1,8 @@
#! /bin/bash
mkdir assets/generated
ts-node createLayouts.ts
find -name '*.png' | parallel optipng '{}'
npm run build
rm -rf /home/pietervdvn/git/pietervdvn.github.io/MapComplete/*
cp -r dist/* /home/pietervdvn/git/pietervdvn.github.io/MapComplete/

View file

@ -13,10 +13,11 @@ import {InitUiElements} from "./InitUiElements";
import {StrayClickHandler} from "./Logic/Leaflet/StrayClickHandler";
import {GeoLocationHandler} from "./Logic/Leaflet/GeoLocationHandler";
import {State} from "./State";
import {CustomLayout} from "./Logic/CustomLayers";
import {CustomLayoutFromJSON} from "./Customizations/JSON/CustomLayoutFromJSON";
import {QueryParameters} from "./Logic/Web/QueryParameters";
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
import {PersonalLayout} from "./Logic/PersonalLayout";
import {OsmConnection} from "./Logic/Osm/OsmConnection";
TagRendering.injectFunction();
@ -109,7 +110,9 @@ console.log("Using layout: ", layoutToUse.name);
State.state = new State(layoutToUse);
if (layoutFromBase64 !== "false") {
State.state.layoutDefinition = hash.substr(1);
console.log(State.state.layoutDefinition)
State.state.osmConnection.OnLoggedIn(() => {
State.state.osmConnection.GetLongPreference("installed-theme-"+layoutToUse.name).setData(State.state.layoutDefinition);
})
}
InitUiElements.InitBaseMap();
@ -152,8 +155,8 @@ function setupAllLayerElements() {
setupAllLayerElements();
if (layoutToUse === AllKnownLayouts.allSets[CustomLayout.NAME]) {
State.state.favourteLayers.addCallback((favs) => {
if (layoutToUse === AllKnownLayouts.allSets[PersonalLayout.NAME]) {
State.state.favouriteLayers.addCallback((favs: string[]) => {
layoutToUse.layers = [];
for (const fav of favs) {
const layer = AllKnownLayouts.allLayers[fav];
@ -161,9 +164,10 @@ if (layoutToUse === AllKnownLayouts.allSets[CustomLayout.NAME]) {
layoutToUse.layers.push(layer);
}
setupAllLayerElements();
};
}
;
State.state.locationControl.ping();
})
}

1323
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -39,6 +39,7 @@
"@babel/polyfill": "^7.10.4",
"@types/node": "^7.0.5",
"assert": "^2.0.0",
"canvas": "^2.6.1",
"chai": "^4.2.0",
"fs": "0.0.1-security",
"marked": "^1.1.1",

View file

@ -57,5 +57,5 @@ function createTable(preferences: any) {
el.AttachTo("maindiv");
}
connection.preferences.addCallback((prefs) => createTable(prefs))
connection._preferencesHandler.preferences.addCallback((prefs) => createTable(prefs))

23
test.ts
View file

@ -1,10 +1,15 @@
import {TextField, ValidatedTextField} from "./UI/Input/TextField";
import {CustomLayoutFromJSON} from "./Customizations/JSON/CustomLayoutFromJSON";
import {And} from "./Logic/TagsFilter";
import {OsmConnection} from "./Logic/Osm/OsmConnection";
import {UIEventSource} from "./Logic/UIEventSource";
const tags = CustomLayoutFromJSON.TagsFromJson("indoor=yes&access!=private");
console.log(tags);
const m0 = new And(tags).matches([{k:"indoor",v:"yes"}, {k:"access",v: "yes"}]);
console.log("Matches 0", m0)
const m1 = new And(tags).matches([{k:"indoor",v:"yes"}, {k:"access",v: "private"}]);
console.log("Matches 1", m1)
const conn = new OsmConnection(true, new UIEventSource<string>(undefined));
conn.AttemptLogin();
conn.userDetails.addCallback(userDetails => {
if (!userDetails.loggedIn) {
return;
}
const str = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
console.log(str.length);
conn.GetLongPreference("test").setData(str);
// console.log(got.length)
});