Add custom theme for advanced users

This commit is contained in:
Pieter Vander Vennet 2020-07-31 04:58:58 +02:00
parent 004eead4ee
commit 9c42839f01
44 changed files with 635 additions and 326 deletions

View file

@ -11,55 +11,53 @@ import {StreetWidth} from "./Layouts/StreetWidth";
import {Natuurpunt} from "./Layouts/Natuurpunt";
import {ClimbingTrees} from "./Layouts/ClimbingTrees";
import {Smoothness} from "./Layouts/Smoothness";
import {LayerDefinition} from "./LayerDefinition";
import {CustomLayers} from "../Logic/CustomLayers";
export class AllKnownLayouts {
public static allSets = AllKnownLayouts.AllLayouts();
public static allLayers: Map<string, LayerDefinition> = undefined;
public static layoutsList: Layout[] = [
new Groen(),
new GRB(),
new Cyclofix(),
new Bookcases(),
new WalkByBrussels(),
new MetaMap(),
new StreetWidth(),
new Natuurpunt(),
new ClimbingTrees(),
new Artworks(),
new Smoothness(),
new CustomLayers()
/*new Toilets(),
*/
];
public static allSets: Map<string, Layout> = AllKnownLayouts.AllLayouts();
private static AllLayouts(): Map<string, Layout> {
const layouts: Layout[] = [
new Groen(),
new GRB(),
new Cyclofix(),
new Bookcases(),
new WalkByBrussels(),
new MetaMap(),
new StreetWidth(),
new Natuurpunt(),
new ClimbingTrees(),
new Artworks(),
new Smoothness()
/*new Toilets(),
*/
];
const all = new All();
const knownKeys = []
for (const layout of layouts) {
this.allLayers = new Map<string, LayerDefinition>();
for (const layout of this.layoutsList) {
for (const layer of layout.layers) {
const key = layer.overpassFilter.asOverpass().join("");
if (knownKeys.indexOf(key) >= 0) {
const key = layer.id;
if (this.allLayers[layer.id] !== undefined) {
continue;
}
knownKeys.push(key);
this.allLayers[layer.id] = layer;
all.layers.push(layer);
}
}
layouts.push(all)
const allSets: Map<string, Layout> = new Map();
for (const layout of layouts) {
for (const layout of this.layoutsList) {
allSets[layout.name] = layout;
}
allSets[all.name] = all;
return allSets;
}
public static GetSets(layoutNames): any {
const all = new All();
for (const name of layoutNames) {
all.layers = all.layers.concat(AllKnownLayouts.allSets[name].layers);
}
return all;
}
}

View file

@ -51,6 +51,7 @@ export class LayerDefinition {
* ])
*/
overpassFilter: TagsFilter;
public readonly id: string;
/**
* This UIElement is rendered as title element in the popup
@ -90,7 +91,7 @@ export class LayerDefinition {
static WAYHANDLING_CENTER_ONLY = 1;
static WAYHANDLING_CENTER_AND_WAY = 2;
constructor(options: {
constructor(id: string, options: {
name: string,
description: string | UIElement,
presets: {
@ -111,6 +112,7 @@ export class LayerDefinition {
icon: any
}
} = undefined) {
this.id = id;
if (options === undefined) {
return;
}
@ -127,6 +129,4 @@ export class LayerDefinition {
this.wayHandling = options.wayHandling ?? LayerDefinition.WAYHANDLING_DEFAULT;
}
}

View file

@ -10,7 +10,7 @@ import FixedText from "../Questions/FixedText";
export class Artwork extends LayerDefinition {
constructor() {
super();
super("artwork");
this.name = "artwork";
const t = Translations.t.artwork;
this.title = t.title;

View file

@ -16,7 +16,7 @@ export default class BikeCafes extends LayerDefinition {
private readonly to = Translations.t.cyclofix.cafe
constructor() {
super()
super("bikecafe")
this.name = this.to.name
this.icon = "./assets/bike/cafe.svg"
this.overpassFilter = new And([

View file

@ -20,7 +20,7 @@ export default class BikeOtherShops extends LayerDefinition {
private readonly to = Translations.t.cyclofix.nonBikeShop
constructor() {
super();
super("bikeOtherShop");
this.name = this.to.name
this.icon = "./assets/bike/non_bike_repair_shop.svg"
this.overpassFilter = new And([

View file

@ -15,7 +15,7 @@ export default class BikeParkings extends LayerDefinition {
private readonly accessCargoDesignated = new Tag("cargo_bike", "designated");
constructor() {
super();
super("bikeparking");
this.name = Translations.t.cyclofix.parking.name;
this.icon = "./assets/bike/parking.svg";
this.overpassFilter = new Tag("amenity", "bicycle_parking");

View file

@ -21,7 +21,7 @@ export default class BikeShops extends LayerDefinition {
private readonly repairsBikes = new Tag("service:bicycle:repair", "yes")
constructor() {
super();
super("bikeshop");
this.name = Translations.t.cyclofix.shop.name
this.icon = "./assets/bike/repair_shop.svg"
this.overpassFilter = new Tag("shop", "bicycle");

View file

@ -27,7 +27,7 @@ export default class BikeStations extends LayerDefinition {
private readonly to = Translations.t.cyclofix.station
constructor() {
super();
super("bikestation");
this.name = Translations.t.cyclofix.station.name;
this.icon = "./assets/bike/repair_station_pump.svg";

View file

@ -10,7 +10,7 @@ export class Birdhide extends LayerDefinition {
constructor() {
super({
super("birdhide",{
name: "vogelkijkplaats",
description: "Een plaats om vogels te kijken, zoals een vogelkijkhut of kijkwand",
overpassFilter: Birdhide.birdhide,

View file

@ -9,7 +9,7 @@ import T from "../../UI/i18n/Translation";
export class Bookcases extends LayerDefinition {
constructor() {
super();
super("bookcases");
this.name = "boekenkast";
this.presets = [{

View file

@ -10,7 +10,7 @@ import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWi
export class Bos extends LayerDefinition {
constructor() {
super();
super("bos");
this.name = "Bos";
this.icon = "";

View file

@ -8,7 +8,7 @@ export class ClimbingTree extends LayerDefinition {
constructor() {
super();
super("climbingtree");
const t = Translations.t.climbingTrees.layer;
this.title = new FixedText(t.title);
const icon = "assets/walkbybrussels/tree.svg";

View file

@ -10,7 +10,7 @@ import Translations from "../../UI/i18n/Translations";
export class DrinkingWater extends LayerDefinition {
constructor() {
super();
super("drinkingwater");
this.name = Translations.t.cyclofix.drinking_water.title;
this.icon = "./assets/bike/drinking_water.svg";

View file

@ -8,7 +8,7 @@ import L from "leaflet";
export class GhostBike extends LayerDefinition {
constructor() {
super();
super("ghost bike");
this.name = "ghost bike";
this.overpassFilter = new Tag("memorial", "ghost_bike")
this.title = new FixedText("Ghost bike");

View file

@ -5,7 +5,7 @@ import {TagRenderingOptions} from "../TagRendering";
export class GrbToFix extends LayerDefinition {
constructor() {
super();
super("grb");
this.name = "grb";
this.presets = [];

View file

@ -6,7 +6,7 @@ import {And, Tag} from "../../Logic/TagsFilter";
export class InformationBoard extends LayerDefinition {
constructor() {
super({
super("informationBoard",{
name: "Informatiebord",
description: "Een informatiebord of kaart",
minzoom: 12,

View file

@ -6,7 +6,7 @@ import {And, Tag} from "../../Logic/TagsFilter";
export class Map extends LayerDefinition {
constructor() {
super();
super("map");
this.name = "Map";
this.title = new FixedText("Map");
this.minzoom = 12;

View file

@ -11,7 +11,7 @@ import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWi
export class NatureReserves extends LayerDefinition {
constructor(moreQuests: boolean = false) {
super();
super("natureReserve");
this.name = "Natuurgebied";
this.icon = "";
this.overpassFilter =

View file

@ -45,7 +45,7 @@ export class Park extends LayerDefinition {
constructor() {
super();
super("park");
this.name = "Park";
this.icon = undefined;
this.overpassFilter =

View file

@ -1,5 +1,4 @@
import {LayerDefinition} from "../LayerDefinition";
import {Quests} from "../../Quests";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import L from "leaflet";
import {Tag} from "../../Logic/TagsFilter";
@ -7,7 +6,7 @@ import {Tag} from "../../Logic/TagsFilter";
export class Toilets extends LayerDefinition{
constructor() {
super();
super("toilets");
this.name="toilet";
this.newElementTags = [new Tag( "amenity", "toilets")];

View file

@ -8,7 +8,7 @@ import {TagRenderingOptions} from "../TagRendering";
export class Viewpoint extends LayerDefinition {
constructor() {
super({
super("viewpoint",{
name: "Bezienswaardigheid",
description: "Wil je een foto toevoegen van iets dat geen park, bos of natuurgebied is? Dit kan hiermee",
presets: [{

View file

@ -113,7 +113,7 @@ export class Widths extends LayerDefinition {
constructor(carWidth: number,
cyclistWidth: number,
pedestrianWidth: number) {
super();
super("width");
this.carWidth = carWidth;
this.cyclistWidth = cyclistWidth;
this.pedestrianWidth = pedestrianWidth;

View file

@ -1,93 +0,0 @@
import {UIEventSource} from "./UI/UIEventSource";
import {Changes} from "./Logic/Osm/Changes";
import {State} from "./State";
export class Helpers {
static DoEvery(millis: number, f: (() => void)) {
window.setTimeout(
function () {
f();
Helpers.DoEvery(millis, f);
}
, millis)
}
static SetupAutoSave() {
const changes = State.state.changes;
const millisTillChangesAreSaved = State.state.secondsTillChangesAreSaved;
const saveAfterXMillis = State.state.secondsTillChangesAreSaved.data * 1000;
changes.pendingChangesES.addCallback(function () {
var c = changes.pendingChangesES.data;
if (c > 10) {
millisTillChangesAreSaved.setData(0);
changes.uploadAll(undefined);
return;
}
if (c > 0) {
millisTillChangesAreSaved.setData(saveAfterXMillis);
}
});
millisTillChangesAreSaved.addCallback((time) => {
if (time <= 0 && changes.pendingChangesES.data > 0) {
changes.uploadAll(undefined);
}
}
)
Helpers.DoEvery(
1000,
() => {
millisTillChangesAreSaved
.setData(millisTillChangesAreSaved.data - 1000)
});
}
/*
* Registers an action that:
* -> Upload everything to OSM
* -> Asks the user not to close. The 'not to close' dialog should profide enough time to upload
* -> WHen uploading is done, the window is closed anyway
*/
static LastEffortSave() {
const changes = State.state.changes;
window.addEventListener("beforeunload", function (e) {
// Quickly save everyting!
if (changes.pendingChangesES.data == 0) {
return "";
}
changes.uploadAll(function () {
window.close()
});
var confirmationMessage = "Nog even geduld - je laatset wijzigingen worden opgeslaan!";
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Webkit, Safari, Chrome
});
document.addEventListener('visibilitychange',() => {
if(document.visibilityState === "visible"){
return;
}
if (changes.pendingChangesES.data == 0) {
return;
}
console.log("Upmoading: loss of focus")
changes.uploadAll(function () {
window.close()
});
})
}
}

View file

@ -16,9 +16,13 @@ import {ElementStorage} from "./Logic/ElementStorage";
import {Preset} from "./UI/SimpleAddUI";
import {Changes} from "./Logic/Osm/Changes";
import {OsmConnection} from "./Logic/Osm/OsmConnection";
import {Basemap} from "./Logic/Leaflet/Basemap";
import {BaseLayers, Basemap} from "./Logic/Leaflet/Basemap";
import {State} from "./State";
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";
export class InitUiElements {
@ -47,7 +51,8 @@ export class InitUiElements {
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
{header: `<img src='${'./assets/osm-logo.svg'}'>`, content: Translations.t.general.openStreetMapIntro},
{header: `<img src='${'./assets/share.svg'}'>`, content: new ShareScreen()},
{header: `<img src='${'./assets/add.svg'}'>`, content: new MoreScreen()}
{header: `<img src='${'./assets/add.svg'}'>`, content: new MoreScreen()},
{header: `<img src='${'./assets/star.svg'}'>`, content: new CustomLayersPanel()},
])
return fullOptions;
@ -89,14 +94,38 @@ export class InitUiElements {
}
static InitLayerSelection(layerSetup) {
const closedFilterButton = `<button id="filter__button" class="filter__button shadow">${Img.closedFilterButton}</button>`;
const openFilterButton = `<button id="filter__button" class="filter__button">${Img.openFilterButton}</button>`;
let baseLayerOptions = BaseLayers.baseLayers.map((layer) => {
return {value: layer, shown: layer.name}
});
const backgroundMapPicker = new Combine([new DropDown(`Background map`, baseLayerOptions, State.state.bm.CurrentLayer), openFilterButton]);
const layerSelection = new Combine([`<p class="filter__label">Maplayers</p>`, new LayerSelection(layerSetup.flayers)]);
let layerControl = backgroundMapPicker;
if (layerSetup.flayers.length > 1) {
layerControl = new Combine([layerSelection, backgroundMapPicker]);
}
InitUiElements.OnlyIf(State.state.featureSwitchLayers, () => {
const checkbox = new CheckBox(layerControl, closedFilterButton);
checkbox.AttachTo("filter__selection");
State.state.bm.Location.addCallback(() => {
checkbox.isEnabled.setData(false);
});
});
}
static InitLayers(): {
minZoom: number
flayers: FilteredLayer[],
presets: Preset[]
} {
const addButtons: Preset[]
= [];
const addButtons: Preset[] = [];
const flayers: FilteredLayer[] = []

26
Logic/CustomLayers.ts Normal file
View file

@ -0,0 +1,26 @@
import {Layout} from "../Customizations/Layout";
import Translations from "../UI/i18n/Translations";
export class CustomLayers extends Layout {
public static NAME: string = "personal";
constructor() {
super(
CustomLayers.NAME,
["en"],
Translations.t.favourite.title,
[],
12,
0,
0,
Translations.t.favourite.description,
);
this.icon = "./assets/star.svg"
}
}

View file

@ -0,0 +1,96 @@
import {UIElement} from "../UI/UIElement";
import {State} from "../State";
import Translations from "../UI/i18n/Translations";
import {UIEventSource} from "../UI/UIEventSource";
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";
export class CustomLayersPanel extends UIElement {
private checkboxes: UIElement[];
constructor() {
super(State.state.favourteLayers);
this.ListenTo(State.state.osmConnection.userDetails);
const t = Translations.t.favourite;
this.checkboxes = [];
const controls = new Map<string, UIEventSource<boolean>>();
const favs = State.state.favourteLayers.data;
for (const layout of AllKnownLayouts.layoutsList) {
const header =
new Combine([
`<div class="custom-layer-panel-header-img"><img src='${layout.icon}'></div>`,
"<span><b>",
layout.title,
"</b><br/>",
layout.description ?? "",
"</span>",
], 'custom-layer-panel-header')
this.checkboxes.push(header);
for (const layer of layout.layers) {
const image = (layer.icon ? `<img src='${layer.icon}'>` : Img.checkmark);
const cb = new CheckBox(
new Combine([
image,
"<b>", layer.name ?? "", "</b> ", layer.description ?? ""
]),
new Combine([
"<span style='opacity: 0'>",
image, "</span>", "<b>", layer.name ?? "", "</b> ", layer.description ?? ""
]),
controls[layer.id] ?? (favs.indexOf(layer.id) >= 0)
);
cb.clss = "custom-layer-checkbox"
controls[layer.id] = cb.isEnabled;
cb.isEnabled.addCallback((isEnabled) => {
if (isEnabled) {
CustomLayersState.AddFavouriteLayer(layer.id)
} else {
CustomLayersState.RemoveFavouriteLayer(layer.id);
}
})
this.checkboxes.push(cb);
}
}
State.state.favourteLayers.addCallback((layers) => {
for (const layerId of layers) {
controls[layerId].setData(true);
}
})
}
InnerRender(): string {
const t = Translations.t.favourite;
const userDetails = State.state.osmConnection.userDetails.data;
if(!userDetails.loggedIn){
return "";
}
if(userDetails.csCount <= 100){
return "";
}
return new VerticalCombine([
t.panelIntro,
new FixedUiElement("<a href='./index.html?layout=personal'>GO</a>"),
...this.checkboxes
], "custom-layer-panel").Render();
}
}

View file

@ -0,0 +1,94 @@
import {State} from "../State";
export class CustomLayersState {
static RemoveFavouriteLayer(layer: string) {
const favs = State.state.favourteLayers.data;
const ind = favs.indexOf(layer);
if (ind < 0) {
return;
}
console.log("REmovign fav layer", layer);
favs.splice(ind, 1);
State.state.favourteLayers.ping();
const osmConnection = State.state.osmConnection;
const count = osmConnection.GetPreference("mapcomplete-custom-layer-count");
if (favs.length === 0) {
count.setData("0")
} else 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 === layer) {
// We found the value to remove - mark with a tombstone
layerIDescr.setData("-");
return;
}
}
}
static AddFavouriteLayer(layer: string) {
const favs = State.state.favourteLayers.data;
const ind = favs.indexOf(layer);
if (ind >= 0) {
return;
}
console.log("Adding fav layer", layer);
favs.push(layer);
State.state.favourteLayers.ping();
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 InitFavouriteLayer() {
const osmConnection = State.state.osmConnection;
const count = osmConnection.GetPreference("mapcomplete-custom-layer-count");
const favs = State.state.favourteLayers.data;
let changed = false;
count.addCallback((countStr) => {
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.state.favourteLayers.data.push(layerId);
changed = true;
}
}
if (changed) {
State.state.favourteLayers.ping();
}
})
}
}

View file

@ -1,8 +1,8 @@
import L from "leaflet";
import {UIEventSource} from "../../UI/UIEventSource";
import {UIElement} from "../../UI/UIElement";
import {Helpers} from "../../Helpers";
import {State} from "../../State";
import {Utils} from "../../Utils";
export class GeoLocationHandler extends UIElement {
@ -108,7 +108,7 @@ export class GeoLocationHandler extends UIElement {
if (!self._isActive.data) {
self._isActive.setData(true);
Helpers.DoEvery(60000, () => {
Utils.DoEvery(60000, () => {
if (document.visibilityState !== "visible") {
console.log("Not starting gps: document not visible")

View file

@ -8,6 +8,7 @@ import {OsmNode, OsmObject} from "./OsmObject";
import {And, Tag, TagsFilter} from "../TagsFilter";
import {ElementStorage} from "../ElementStorage";
import {State} from "../../State";
import {Utils} from "../../Utils";
export class Changes {
@ -22,9 +23,11 @@ export class Changes {
constructor(
changesetComment: string,
login: OsmConnection,
allElements: ElementStorage) {
state: State) {
this._changesetComment = changesetComment;
this.SetupAutoSave(state);
this.LastEffortSave();
}
addTag(elementId: string, tagsFilter : TagsFilter){
@ -52,7 +55,6 @@ export class Changes {
* @param value
*/
addChange(elementId: string, key: string, value: string) {
console.log("Received change",key, value)
if (key === undefined || key === null) {
console.log("Invalid key");
return;
@ -256,5 +258,78 @@ console.log("Received change",key, value)
optionalContinuationWrapped);
});
}
/*
* Registers an action that:
* -> Upload everything to OSM
* -> Asks the user not to close. The 'not to close' dialog should profide enough time to upload
* -> WHen uploading is done, the window is closed anyway
*/
private LastEffortSave() {
const self = this;
window.addEventListener("beforeunload", function (e) {
// Quickly save everyting!
if (self.pendingChangesES.data == 0) {
return "";
}
self.uploadAll(function () {
window.close()
});
var confirmationMessage = "Nog even geduld - je laatset wijzigingen worden opgeslaan!";
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Webkit, Safari, Chrome
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === "visible") {
return;
}
if (this.pendingChangesES.data == 0) {
return;
}
console.log("Upmoading: loss of focus")
this.uploadAll(function () {
window.close()
});
})
}
private SetupAutoSave(state: State) {
const millisTillChangesAreSaved = state.secondsTillChangesAreSaved;
const saveAfterXMillis = state.secondsTillChangesAreSaved.data * 1000;
this.pendingChangesES.addCallback(function () {
var c = this.pendingChangesES.data;
if (c > 10) {
millisTillChangesAreSaved.setData(0);
this.uploadAll(undefined);
return;
}
if (c > 0) {
millisTillChangesAreSaved.setData(saveAfterXMillis);
}
});
millisTillChangesAreSaved.addCallback((time) => {
if (time <= 0 && this.pendingChangesES.data > 0) {
this.uploadAll(undefined);
}
}
)
Utils.DoEvery(
1000,
() => {
millisTillChangesAreSaved
.setData(millisTillChangesAreSaved.data - 1000)
});
}
}

View file

@ -1,6 +1,7 @@
// @ts-ignore
import osmAuth from "osm-auth";
import {UIEventSource} from "../../UI/UIEventSource";
import {CustomLayersState} from "../CustomLayersState";
export class UserDetails {
@ -215,6 +216,7 @@ export class OsmConnection {
self.preferences.data[k] = v;
}
self.preferences.ping();
CustomLayersState.InitFavouriteLayer();
});
}

View file

@ -52,7 +52,6 @@ export class QueryParameters {
public static GetQueryParameter(key: string, deflt: string): UIEventSource<string> {
if (deflt !== undefined) {
console.log(key, "-->", deflt)
QueryParameters.defaults[key] = deflt;
}
if (QueryParameters.knownSources[key] !== undefined) {

View file

@ -9,6 +9,10 @@ import {ElementStorage} from "./Logic/ElementStorage";
import {Changes} from "./Logic/Osm/Changes";
import {Basemap} from "./Logic/Leaflet/Basemap";
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";
/**
* Contains the global state: a bunch of UI-event sources
@ -95,6 +99,11 @@ 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[]>([])
constructor(layoutToUse: Layout) {
this.layoutToUse = new UIEventSource<Layout>(layoutToUse);
@ -130,5 +139,57 @@ export class State {
this.featureSwitchIframe = featSw("fs-iframe", () => false);
this.osmConnection = new OsmConnection(
QueryParameters.GetQueryParameter("test", "false").data === "true",
QueryParameters.GetQueryParameter("oauth_token", undefined)
);
Locale.language.syncWith(this.osmConnection.GetPreference("language"));
Locale.language.addCallback((currentLanguage) => {
if (layoutToUse.supportedLanguages.indexOf(currentLanguage) < 0) {
console.log("Resetting languate 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.allElements = new ElementStorage();
this.changes = new Changes(
"Beantwoorden van vragen met #MapComplete voor vragenset #" + this.layoutToUse.data.name,
this);
if (document.getElementById("leafletDiv") === null) {
console.warn("leafletDiv not found - not initializing map. Assuming test.html");
return;
}
this.bm = new Basemap("leafletDiv", this.locationControl, new VariableUiElement(
this.locationControl.map((location) => {
const mapComplete = "<a href='https://github.com/pietervdvn/MapComplete' target='_blank'>Mapcomple</a> " +
" " +
"<a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'><img src='./assets/bug.svg' alt='Report bug' class='small-userbadge-icon'></a>";
let editHere = "";
if (location !== undefined) {
editHere = " | " +
"<a href='https://www.openstreetmap.org/edit?editor=id#map=" + location.zoom + "/" + location.lat + "/" + location.lon + "' target='_blank'>" +
"<img src='./assets/pencil.svg' alt='edit here' class='small-userbadge-icon'>" +
"</a>"
}
return mapComplete + editHere;
})
));
}
}

View file

@ -3,10 +3,12 @@ import Translations from "../i18n/Translations";
export default class Combine extends UIElement {
private uiElements: (string | UIElement)[];
private className: string = undefined;
private clas: string = undefined;
constructor(uiElements: (string | UIElement)[]) {
constructor(uiElements: (string | UIElement)[], className: string = undefined) {
super(undefined);
this.className = className;
this.uiElements = uiElements;
}
@ -19,6 +21,10 @@ export default class Combine extends UIElement {
elements += element;
}
}
if(this.className !== undefined){
elements = `<span class='${this.className}'>${elements}</span>`;
}
return elements;
}

20
UI/Base/Image.ts Normal file
View file

@ -0,0 +1,20 @@
import {UIElement} from "../UIElement";
export class Image extends UIElement{
private src: string;
private style: string = "";
constructor(src: string, style: string = "") {
super(undefined);
this.style = style;
this.src = src;
}
InnerRender(): string {
if(this.src === undefined){
return "";
}
return `<img src='${this.src}' style='${this.style}'>`;
}
}

View file

@ -27,8 +27,10 @@ export class TabbedComponent extends UIElement {
for (let i = 0; i < this.headers.length; i++) {
let header = this.headers[i];
headerBar += `<div class=\'tab-single-header ${i == this._source.data ? 'tab-active' : 'tab-non-active'}\'>` +
header.Render() + "</div>"
if (!this.content[i].IsEmpty()) {
headerBar += `<div class=\'tab-single-header ${i == this._source.data ? 'tab-active' : 'tab-non-active'}\'>` +
header.Render() + "</div>"
}
}

View file

@ -92,7 +92,7 @@ export class SimpleAddUI extends UIElement {
const self = this;
return () => {
const loc = State.state.bm.lastClickLocation.data;
const loc = State.state.bm.LastClickLocation.data;
let feature = State.state.changes.createElement(option.tags, loc.lat, loc.lon);
option.layerToAddTo.AddNewElement(feature);
State.state.selectedElement.setData({feature: feature});
@ -107,7 +107,7 @@ export class SimpleAddUI extends UIElement {
if(userDetails.data.dryRun){
this.CreatePoint(this._confirmPreset.data)();
return;
return "";
}
return new Combine([

View file

@ -9,6 +9,7 @@ import {Basemap} from "../Logic/Leaflet/Basemap";
import {State} from "../State";
import {PendingChanges} from "./PendingChanges";
import Locale from "./i18n/Locale";
import {Utils} from "../Utils";
/**
* Handles and updates the user badge
@ -25,7 +26,7 @@ export class UserBadge extends UIElement {
super(State.state.osmConnection.userDetails);
this._userDetails = State.state.osmConnection.userDetails;
this._pendingChanges = new PendingChanges();
this._languagePicker = Locale.CreateLanguagePicker();
this._languagePicker = Utils.CreateLanguagePicker();
this._logout = new FixedUiElement("<img src='assets/logout.svg' class='small-userbadge-icon' alt='logout'>")
.onClick(() => {

View file

@ -6,6 +6,7 @@ import {State} from "../State";
import {Layout} from "../Customizations/Layout";
import Translations from "./i18n/Translations";
import {VariableUiElement} from "./Base/VariableUIElement";
import {Utils} from "../Utils";
export class WelcomeMessage extends UIElement {
private readonly layout: Layout;
@ -20,7 +21,7 @@ export class WelcomeMessage extends UIElement {
constructor() {
super(State.state.osmConnection.userDetails);
this.languagePicker = Locale.CreateLanguagePicker(Translations.t.general.pickLanguage);
this.languagePicker = Utils.CreateLanguagePicker(Translations.t.general.pickLanguage);
this.ListenTo(Locale.language);
function fromLayout(f: (layout: Layout) => (string | UIElement)): UIElement {

View file

@ -7,15 +7,17 @@ import {State} from "../../State";
export default class Locale {
public static language: UIEventSource<string> = LocalStorageSource.Get('language', "en");
public static CreateLanguagePicker(label: string | UIElement = "") {
return new DropDown(label, State.state.layoutToUse.data.supportedLanguages.map(lang => {
return {value: lang, shown: lang}
}
), Locale.language);
public static language: UIEventSource<string> = Locale.setup();
private static setup() {
const source = LocalStorageSource.Get('language', "en");
// @ts-ignore
window.setLanguage = function (language: string) {
source.setData(language)
}
return source;
}
}

View file

@ -793,8 +793,8 @@ export default class Translations {
}),
header: new T({
en: "<h2>No data</h2>You clicked somewhere where no data is known yet.<br/>",
nl: "<h2>Geen selectie</h2>Je klikte ergens waar er nog geen data is.<br/>",
en: "<h2>Add a point?</h2>You clicked somewhere where no data is known yet.<br/>",
nl: "<h2>Punt toevoegen?</h2>Je klikte ergens waar er nog geen data is.<br/>",
fr: "<h2>Pas de données</h2> vous avez cliqué sur un endroit ou il n'y a pas encore de données. <br/>"
}),
@ -814,8 +814,8 @@ export default class Translations {
fr: "Chargement des donnés. Patientez un instant avant d'ajouter un nouveau point"
}),
confirmIntro: new T({
en: "<h3>Add a {title} here?</h3>The point you create here will be visible for everyone. Please, only add things on to the map if they truly exist. A lot of applications use this data.",
nl: "<h3>Voeg hier een {title} toe?</h3>Het punt dat je hier toevoegt, is zichtbaar voor iedereen. Veel applicaties gebruiken deze data, voeg dus enkel punten toe die echt bestaan.",
en: "<h3>Add a {title} here?</h3>The point you create here will be <b>visible for everyone</b>. Please, only add things on to the map if they truly exist. A lot of applications use this data.",
nl: "<h3>Voeg hier een {title} toe?</h3>Het punt dat je hier toevoegt, is <b>zichtbaar voor iedereen</b>. Veel applicaties gebruiken deze data, voeg dus enkel punten toe die echt bestaan.",
fr: "<h3>Ajouter un/une {title} ici?</h3>Le point que vous ajouter sera visible par tout le monde. Merci d'etre sûr que ce point existe réellement. Beaucoup d'autres applications reposent sur ces données.",
})
@ -952,6 +952,16 @@ export default class Translations {
nl: "Ga naar de berichten",
fr: "Ouvrir les messages"
})
},
favourite: {
title: "Custom",
description: new T({
en: "<h3>Your custom theme</h3>In your custom theme, you can add some favourite layers from other themes to create a custom theme."
}),
panelIntro: new T({
en:"<h3>Your custom theme</h3>Create your own theme here by picking your favourite layers"
})
}
}

View file

@ -1,3 +1,8 @@
import {UIElement} from "./UI/UIElement";
import {DropDown} from "./UI/Input/DropDown";
import {State} from "./State";
import Locale from "./UI/i18n/Locale";
export class Utils {
/**
@ -18,4 +23,22 @@ export class Utils {
public static Upper(str : string){
return str.substr(0,1).toUpperCase() + str.substr(1);
}
static DoEvery(millis: number, f: (() => void)) {
window.setTimeout(
function () {
f();
Utils.DoEvery(millis, f);
}
, millis)
}
public static CreateLanguagePicker(label: string | UIElement = "") {
return new DropDown(label, State.state.layoutToUse.data.supportedLanguages.map(lang => {
return {value: lang, shown: lang}
}
), Locale.language);
}
}

View file

@ -1241,4 +1241,65 @@ form {
.add-ui {
font-size: large;
}
.custom-layer-panel {
}
.custom-layer-panel-header {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
font-size: large;
margin: 0.5em;
background-color: white;
justify-content: flex-start;
align-items: center;
text-decoration: none;
color: black;
}
.custom-layer-panel-header-img img {
max-width: 3em;
width: 100%;
max-height: 3em;
padding: 0.5em;
}
.custom-layer-panel-header-img {
width: 4em;
height: 4em;
}
.custom-layer-checkbox {
font-size: larger;
height: 2em;
background-color: #e5f5ff;
margin:0.3em;
margin-left: 2em;
display: flex;
justify-content: flex-start;
align-items: stretch;
text-decoration: none;
padding: 0.5em;
border-radius: 1em;
}
.custom-layer-checkbox img {
max-width: 1.5em;
max-height: 1.5em;
width: 100%;
height: 100%;
padding: 0.2em;
padding-right: 0.5em;
}
.custom-layer-checkbox svg {
max-width: 1.5em;
max-height: 1.5em;
padding: 0.2em;
padding-right: 0.5em;
}

156
index.ts
View file

@ -1,37 +1,21 @@
import {ElementStorage} from "./Logic/ElementStorage";
import {UIEventSource} from "./UI/UIEventSource";
import {UserBadge} from "./UI/UserBadge";
import {PendingChanges} from "./UI/PendingChanges";
import {CenterMessageBox} from "./UI/CenterMessageBox";
import {Helpers} from "./Helpers";
import {TagUtils} from "./Logic/TagsFilter";
import {LayerUpdater} from "./Logic/LayerUpdater";
import {UIElement} from "./UI/UIElement";
import {FullScreenMessageBoxHandler} from "./UI/FullScreenMessageBoxHandler";
import {FeatureInfoBox} from "./UI/FeatureInfoBox";
import {SimpleAddUI} from "./UI/SimpleAddUI";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import {SearchAndGo} from "./UI/SearchAndGo";
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
import {CheckBox} from "./UI/Input/CheckBox";
import Translations from "./UI/i18n/Translations";
import Locale from "./UI/i18n/Locale";
import {Layout} from "./Customizations/Layout";
import {DropDown} from "./UI/Input/DropDown";
import {FixedUiElement} from "./UI/Base/FixedUiElement";
import {LayerSelection} from "./UI/LayerSelection";
import Combine from "./UI/Base/Combine";
import {Img} from "./UI/Img";
import {QueryParameters} from "./Logic/QueryParameters";
import {Utils} from "./Utils";
import {LocalStorageSource} from "./Logic/LocalStorageSource";
import {InitUiElements} from "./InitUiElements";
import {StrayClickHandler} from "./Logic/Leaflet/StrayClickHandler";
import {BaseLayers, Basemap} from "./Logic/Leaflet/Basemap";
import {GeoLocationHandler} from "./Logic/Leaflet/GeoLocationHandler";
import {OsmConnection} from "./Logic/Osm/OsmConnection";
import {Changes} from "./Logic/Osm/Changes";
import {State} from "./State";
import {All} from "./Customizations/Layouts/All";
import {CustomLayers} from "./Logic/CustomLayers";
// --------------------- Special actions based on the parameters -----------------
@ -79,114 +63,59 @@ for (const k in AllKnownLayouts.allSets) {
defaultLayout = QueryParameters.GetQueryParameter("layout", defaultLayout).data;
const layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout] ?? AllKnownLayouts["all"];
console.log("Using layout: ", layoutToUse.name);
if (layoutToUse === undefined) {
console.log("Incorrect layout")
new FixedUiElement("Error: incorrect layout " + defaultLayout + "<a href='https://pietervdvn.github.io/MapComplete/index.html'>Go to MapComplete</a>").AttachTo("centermessage").onClick(() => {
});
throw "Incorrect layout"
}
// Setup the global state
console.log("Using layout: ", layoutToUse.name);
State.state = new State(layoutToUse);
const state = State.state;
// ----------------- Prepare the important objects -----------------
state.osmConnection = new OsmConnection(
QueryParameters.GetQueryParameter("test", "false").data === "true",
QueryParameters.GetQueryParameter("oauth_token", undefined)
);
Locale.language.syncWith(state.osmConnection.GetPreference("language"));
// @ts-ignore
window.setLanguage = function (language: string) {
Locale.language.setData(language)
}
Locale.language.addCallback((currentLanguage) => {
if (layoutToUse.supportedLanguages.indexOf(currentLanguage) < 0) {
console.log("Resetting languate 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()
state.allElements = new ElementStorage();
state.changes = new Changes(
"Beantwoorden van vragen met #MapComplete voor vragenset #" + state.layoutToUse.data.name,
state.osmConnection, state.allElements);
state.bm = new Basemap("leafletDiv", state.locationControl, new VariableUiElement(
state.locationControl.map((location) => {
const mapComplete = "<a href='https://github.com/pietervdvn/MapComplete' target='_blank'>Mapcomple</a> " +
" " +
"<a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'><img src='./assets/bug.svg' alt='Report bug' class='small-userbadge-icon'></a>";
let editHere = "";
if (location !== undefined) {
editHere = " | " +
"<a href='https://www.openstreetmap.org/edit?editor=id#map=" + location.zoom + "/" + location.lat + "/" + location.lon + "' target='_blank'>" +
"<img src='./assets/pencil.svg' alt='edit here' class='small-userbadge-icon'>" +
"</a>"
}
return mapComplete + editHere;
})
));
function setupAllLayerElements() {
// ------------- Setup the layers -------------------------------
const layerSetup = InitUiElements.InitLayers();
const layerSetup = InitUiElements.InitLayers();
const layerUpdater = new LayerUpdater(layerSetup.minZoom, layoutToUse.widenFactor, layerSetup.flayers);
// --------------- Setting up layer selection ui --------
const closedFilterButton = `<button id="filter__button" class="filter__button shadow">${Img.closedFilterButton}</button>`;
const openFilterButton = `
<button id="filter__button" class="filter__button">${Img.openFilterButton}</button>`;
let baseLayerOptions = BaseLayers.baseLayers.map((layer) => {
return {value: layer, shown: layer.name}
});
const backgroundMapPicker = new Combine([new DropDown(`Background map`, baseLayerOptions, State.state.bm.CurrentLayer), openFilterButton]);
const layerSelection = new Combine([`<p class="filter__label">Maplayers</p>`, new LayerSelection(layerSetup.flayers)]);
let layerControl = backgroundMapPicker;
if (layerSetup.flayers.length > 1) {
layerControl = new Combine([layerSelection, backgroundMapPicker]);
}
InitUiElements.OnlyIf(State.state.featureSwitchLayers, () => {
const checkbox = new CheckBox(layerControl, closedFilterButton);
checkbox.AttachTo("filter__selection");
State.state.bm.Location.addCallback(() => {
checkbox.isEnabled.setData(false);
});
});
const layerUpdater = new LayerUpdater(layerSetup.minZoom, layoutToUse.widenFactor, layerSetup.flayers);
InitUiElements.InitLayerSelection(layerSetup)
// ------------------ Setup various other UI elements ------------
document.title = Translations.W(layoutToUse.title).InnerRender();
Locale.language.addCallback(e => {
document.title = Translations.W(layoutToUse.title).InnerRender();
})
InitUiElements.OnlyIf(State.state.featureSwitchAddNew, () => {
new StrayClickHandler(() => {
return new SimpleAddUI(
layerUpdater.runningQuery,
layerSetup.presets);
}
);
});
new CenterMessageBox(
layerSetup.minZoom,
layerUpdater.runningQuery)
.AttachTo("centermessage");
InitUiElements.OnlyIf(State.state.featureSwitchAddNew, () => {
new StrayClickHandler(() => {
return new SimpleAddUI(
layerUpdater.runningQuery,
layerSetup.presets);
}
setupAllLayerElements();
if (layoutToUse === AllKnownLayouts.allSets[CustomLayers.NAME]) {
State.state.favourteLayers.addCallback((favs) => {
for (const fav of favs) {
const layer = AllKnownLayouts.allLayers[fav];
if (!!layer) {
layoutToUse.layers.push(layer);
}
setupAllLayerElements();
}
);
});
})
}
/**
@ -219,7 +148,6 @@ State.state.selectedElement.addCallback((feature) => {
}
);
console.log("Enable new:",State.state.featureSwitchAddNew.data,"deafult", layoutToUse.enableAdd)
InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => {
new UserBadge().AttachTo('userbadge');
});
@ -241,16 +169,6 @@ if ((window != window.top && !State.state.featureSwitchWelcomeMessage) || State.
}
new CenterMessageBox(
layerSetup.minZoom,
layerUpdater.runningQuery)
.AttachTo("centermessage");
Helpers.SetupAutoSave();
Helpers.LastEffortSave();
new GeoLocationHandler().AttachTo("geolocate-button");

37
test.ts
View file

@ -1,32 +1,11 @@
import {ImageUploadFlow} from "./UI/ImageUploadFlow";
import {OsmConnection, UserDetails} from "./Logic/Osm/OsmConnection";
import {OsmImageUploadHandler} from "./Logic/Osm/OsmImageUploadHandler";
import {UIEventSource} from "./UI/UIEventSource";
import {Changes} from "./Logic/Osm/Changes";
import {SlideShow} from "./UI/SlideShow";
import {ElementStorage} from "./Logic/ElementStorage";
import {isNullOrUndefined} from "util";
import Locale from "./UI/i18n/Locale";
import {State} from "./State";
import Cyclofix from "./Customizations/Layouts/Cyclofix";
import {CustomLayersPanel} from "./Logic/CustomLayersPanel";
const osmConnection = new OsmConnection(true, new UIEventSource<string>(undefined));
const uploadHandler = new OsmImageUploadHandler(
new UIEventSource<any>({}),
osmConnection.userDetails,
new UIEventSource<string>("cc0"),
new Changes("blabla", osmConnection, new ElementStorage()),
undefined);
State.state= new State(new Cyclofix());
new ImageUploadFlow(
osmConnection.userDetails,
new UIEventSource<string>("cc0"),
(license: string) => {
return {
title: "test",
description: "test",
handleURL: console.log,
allDone: () => {
}
}
}).AttachTo("maindiv")
new CustomLayersPanel().AttachTo("maindiv");
Locale.language.setData("nl")
State.state.osmConnection.GetPreference("mapcomplete-custom-layer-count").addCallback((count) => console.log("Count: ", count))
State.state.favourteLayers.addCallback(console.log)