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,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"
}
}
}