From 026cf953901952122223467b2e5518658906680a Mon Sep 17 00:00:00 2001 From: shelly_goldblit Date: Mon, 18 Dec 2023 11:20:48 +0200 Subject: [PATCH] Main commit, missing package.json --- cypress/e2e/accessibility.cy.ts | 60 +-- cypress/e2e/driver.ts | 350 ++++++------ cypress/e2e/history.cy.ts | 135 +++-- cypress/e2e/keyboard.cy.ts | 53 +- cypress/e2e/layers.cy.ts | 544 +++++++++++-------- cypress/e2e/map.cy.ts | 46 +- cypress/e2e/modals.cy.ts | 165 +++--- cypress/support/e2e.ts | 5 +- src/components/App.jsx | 894 +++++++++++++++++-------------- src/components/AppToolbar.jsx | 350 ++++++------ src/components/LayerEditor.jsx | 456 +++++++++------- src/components/LayerList.jsx | 329 ++++++------ src/components/MapMaplibreGl.jsx | 223 ++++---- src/libs/zoomcontrol.js | 22 +- 14 files changed, 1988 insertions(+), 1644 deletions(-) diff --git a/cypress/e2e/accessibility.cy.ts b/cypress/e2e/accessibility.cy.ts index 17c71b62..5a47f1ad 100644 --- a/cypress/e2e/accessibility.cy.ts +++ b/cypress/e2e/accessibility.cy.ts @@ -1,41 +1,39 @@ -import driver from "./driver"; +import MaputnikDriver from "./driver"; describe("accessibility", () => { - // skipped due to the following issue with cypress: https://github.com/cypress-io/cypress/issues/299 - describe.skip("skip links", () => { + let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + beforeAndAfter(); + + describe("skip links", () => { beforeEach(() => { - driver.beforeEach(); - driver.setStyle("layer"); + when.setStyle("layer"); }); - + it("skip link to layer list", () => { - const selector = driver.getDataAttribute("root:skip:layer-list"); - driver.isExists(selector); - driver.typeKeys('{tab}'); - driver.isFocused(selector); - driver.click(selector); - - driver.isFocused("#skip-target-layer-list"); + const selector = "root:skip:layer-list"; + should.isExists(selector); + when.tab(); + should.beFocused(selector); + when.click(selector); + should.beFocused("skip-target-layer-list"); }); - + it("skip link to layer editor", () => { - const selector = driver.getDataAttribute("root:skip:layer-editor"); - driver.isExists(selector); - driver.typeKeys('{tab}{tab}'); - driver.isFocused(selector); - driver.click(selector); - - driver.isFocused("#skip-target-layer-editor"); + const selector = "root:skip:layer-editor"; + should.isExists(selector); + when.tab().tab(); + should.beFocused(selector); + when.click(selector); + should.beFocused("skip-target-layer-editor"); }); - + it("skip link to map view", () => { - const selector = driver.getDataAttribute("root:skip:map-view"); - driver.isExists(selector); - driver.typeKeys('{tab}{tab}{tab}'); - driver.isFocused(selector); - driver.click(selector); - - driver.isFocused(".maplibregl-canvas"); + const selector = "root:skip:map-view"; + should.isExists(selector); + when.tab().tab().tab(); + should.beFocused(selector); + when.click(selector); + should.canvasBeFocused(); }); - }); -}) + }); +}); diff --git a/cypress/e2e/driver.ts b/cypress/e2e/driver.ts index fcadcd66..16cd60e4 100644 --- a/cypress/e2e/driver.ts +++ b/cypress/e2e/driver.ts @@ -1,170 +1,198 @@ -import {v1 as uuid} from "uuid"; +import { CypressHelper } from "@shellygo/cypress-test-utils"; +import { v1 as uuid } from "uuid"; +export default class MaputnikDriver { + private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" }); + public beforeAndAfter = () => { + beforeEach(() => { + this.given.setupInterception(); + this.when.setStyle("both"); + }); + }; -export default { - isMac() { - return Cypress.platform === "darwin"; + public given = { + setupInterception: () => { + cy.intercept("GET", "http://localhost:8888/example-style.json", { + fixture: "example-style.json", + }).as("example-style.json"); + cy.intercept("GET", "http://localhost:8888/example-layer-style.json", { + fixture: "example-layer-style.json", + }); + cy.intercept("GET", "http://localhost:8888/geojson-style.json", { + fixture: "geojson-style.json", + }); + cy.intercept("GET", "http://localhost:8888/raster-style.json", { + fixture: "raster-style.json", + }); + cy.intercept("GET", "http://localhost:8888/geojson-raster-style.json", { + fixture: "geojson-raster-style.json", + }); + cy.intercept({ method: "GET", url: "*example.local/*" }, []); + cy.intercept({ method: "GET", url: "*example.com/*" }, []); }, + }; - beforeEach() { - this.setupInterception(); - this.setStyle('both'); + public when = { + within: (selector: string, fn: () => void) => { + this.helper.when.within(fn, selector); }, - - setupInterception() { - cy.intercept('GET', 'http://localhost:8888/example-style.json', { fixture: 'example-style.json' }).as('example-style.json'); - cy.intercept('GET', 'http://localhost:8888/example-layer-style.json', { fixture: 'example-layer-style.json' }); - cy.intercept('GET', 'http://localhost:8888/geojson-style.json', { fixture: 'geojson-style.json' }); - cy.intercept('GET', 'http://localhost:8888/raster-style.json', { fixture: 'raster-style.json' }); - cy.intercept('GET', 'http://localhost:8888/geojson-raster-style.json', { fixture: 'geojson-raster-style.json' }); - cy.intercept({method: 'GET', url: '*example.local/*' }, []); - cy.intercept({method: 'GET', url: '*example.com/*' }, []); + tab: () => cy.get("body").tab(), + waitForExampleFileRequset: () => { + this.helper.when.waitForResponse("example-style.json"); }, - - setStyle(styleProperties: 'geojson' | 'raster' | 'both' | 'layer' | '', zoom? : number) { - let url = "?debug"; - switch (styleProperties) { - case "geojson": - url += "&style=http://localhost:8888/geojson-style.json"; - break; - case "raster": - url += "&style=http://localhost:8888/raster-style.json"; - break; - case "both": - url += "&style=http://localhost:8888/geojson-raster-style.json"; - break; - case "layer": - url += "&style=http://localhost:8888/example-layer-style.json"; - break; - } - if (zoom) { - url += "#" + zoom + "/41.3805/2.1635"; - } - cy.visit("http://localhost:8888/" + url); - if (styleProperties) { - cy.on('window:confirm', () => true) - } - cy.get(".maputnik-toolbar-link").should("be.visible"); - }, - - getDataAttribute(key: string, selector?: string) { - return `*[data-wd-key='${key}'] ${selector || ''}`; - }, - - closeModal(key: string) { - const selector = this.getDataAttribute(key); - - this.isDisplayedInViewport(selector); - - this.click(this.getDataAttribute(key + ".close-modal")); - - this.doesNotExists(selector); - }, - - openLayersModal() { - cy.get(this.getDataAttribute('layer-list:add-layer')).click(); - - cy.get(this.getDataAttribute('modal:add-layer')).should('exist'); - cy.get(this.getDataAttribute('modal:add-layer')).should('be.visible'); - }, - - getStyleFromWindow(win: Window) { - const styleId = win.localStorage.getItem("maputnik:latest_style"); - const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`) - const obj = JSON.parse(styleItem || ""); - return obj; - }, - - isStyleStoreEqual(getter: (obj:any) => any, styleObj: any) { - cy.window().then((win: any) => { - const obj = this.getStyleFromWindow(win); - assert.deepEqual(getter(obj), styleObj); - }); - }, - - isStyleStoreEqualToExampleFileData() { - cy.window().then((win: any) => { - const obj = this.getStyleFromWindow(win); - cy.fixture('example-style.json').should('deep.equal', obj); - }); - }, - - fillLayersModal(opts: any) { - var type = opts.type; - var layer = opts.layer; - var id; - if(opts.id) { - id = opts.id - } - else { - id = `${type}:${uuid()}`; - } - - cy.get(this.getDataAttribute('add-layer.layer-type', "select")).select(type); - cy.get(this.getDataAttribute("add-layer.layer-id", "input")).type(id); - if(layer) { - cy.get(this.getDataAttribute("add-layer.layer-source-block", "input")).type(layer); - } - cy.get(this.getDataAttribute("add-layer")).click(); - - return id; - }, - - typeKeys(keys: string) { - cy.get('body').type(keys); - }, - - click(selector: string) { - cy.get(selector).click(); - }, - - select(selector: string, value: string) { - cy.get(selector).select(value); - }, - - isSelected(selector: string, value: string) { - cy.get(selector).find(`option[value="${value}"]`).should("be.selected"); - }, - - - focus(selector: string) { - cy.get(selector).focus(); - }, - - isFocused(selector: string) { - cy.get(selector).should('have.focus'); - }, - - isDisplayedInViewport(selector: string) { - cy.get(selector).should('be.visible'); - }, - - isNotDisplayedInViewport(selector: string) { - cy.get(selector).should('not.be.visible'); - }, - - setValue(selector: string, text: string) { - cy.get(selector).clear().type(text, {parseSpecialCharSequences: false}); - }, - - isExists(selector: string) { - cy.get(selector).should('exist'); - }, - - doesNotExists(selector: string) { - cy.get(selector).should('not.exist'); - }, - - chooseExampleFile() { - cy.get("input[type='file']").selectFile('cypress/fixtures/example-style.json', {force: true}); - }, - - getExampleFileUrl() { - return "http://localhost:8888/example-style.json"; - }, - - waitForExampleFileRequset() { - cy.wait('@example-style.json'); + chooseExampleFile: () => { + cy.get("input[type='file']").selectFile( + "cypress/fixtures/example-style.json", + { force: true } + ); + }, + setStyle: ( + styleProperties: "geojson" | "raster" | "both" | "layer" | "", + zoom?: number + ) => { + let url = "?debug"; + switch (styleProperties) { + case "geojson": + url += "&style=http://localhost:8888/geojson-style.json"; + break; + case "raster": + url += "&style=http://localhost:8888/raster-style.json"; + break; + case "both": + url += "&style=http://localhost:8888/geojson-raster-style.json"; + break; + case "layer": + url += "&style=http://localhost:8888/example-layer-style.json"; + break; + } + if (zoom) { + url += "#" + zoom + "/41.3805/2.1635"; + } + cy.visit("http://localhost:8888/" + url); + if (styleProperties) { + cy.on("window:confirm", () => true); + } + cy.get(".maputnik-toolbar-link").should("be.visible"); + }, + fillLayersModal: (opts: any) => { + var type = opts.type; + var layer = opts.layer; + var id; + if (opts.id) { + id = opts.id; + } else { + id = `${type}:${uuid()}`; } + cy.get( + this.get.getDataAttribute("add-layer.layer-type", "select") + ).select(type); + cy.get(this.get.getDataAttribute("add-layer.layer-id", "input")).type(id); + if (layer) { + cy.get( + this.get.getDataAttribute("add-layer.layer-source-block", "input") + ).type(layer); + } + this.when.click("add-layer"); + return id; + }, + + typeKeys: (keys: string) => { + cy.get("body").type(keys); + }, + + click: (selector: string) => { + this.helper.when.click(selector); + // cy.get(selector).click({ force: true }); + }, + + select: (selector: string, value: string) => { + cy.get(selector).select(value); + }, + + focus: (selector: string) => { + this.helper.when.focus(selector); + }, + + setValue: (selector: string, text: string) => { + cy.get(selector).clear().type(text, { parseSpecialCharSequences: false }); + }, + + closeModal: (key: string) => { + this.helper.when.waitUntil(() => this.helper.get.element(key)); + this.when.click(key + ".close-modal"); + }, + + openLayersModal: () => { + this.helper.when.click("layer-list:add-layer"); + + cy.get(this.get.getDataAttribute("modal:add-layer")).should("exist"); + cy.get(this.get.getDataAttribute("modal:add-layer")).should("be.visible"); + }, + }; + + public get = { + isMac: () => { + return Cypress.platform === "darwin"; + }, + getStyleFromWindow: (win: Window) => { + const styleId = win.localStorage.getItem("maputnik:latest_style"); + const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`); + const obj = JSON.parse(styleItem || ""); + return obj; + }, + getExampleFileUrl: () => { + return "http://localhost:8888/example-style.json"; + }, + getDataAttribute: (key: string, selector?: string): string => { + return `*[data-wd-key='${key}'] ${selector || ""}`; + }, + }; + + public should = { + canvasBeFocused: () => { + this.when.within("maplibre:map", () => { + cy.get("canvas").should("be.focused"); + }); + }, + notExist: (selector: string) => { + cy.get(selector).should("not.exist"); + }, + beFocused: (selector: string) => { + this.helper.get.element(selector).should("have.focus"); + }, + + notBeFocused: (selector: string) => { + this.helper.get.element(selector).should("not.have.focus"); + }, + + beVisible: (selector: string) => { + this.helper.get.element(selector).should("be.visible"); + }, + + notBeVisible: (selector: string) => { + this.helper.get.element(selector).should("not.be.visible"); + }, + + equalStyleStore: (getter: (obj: any) => any, styleObj: any) => { + cy.window().then((win: any) => { + const obj = this.get.getStyleFromWindow(win); + assert.deepEqual(getter(obj), styleObj); + }); + }, + + isStyleStoreEqualToExampleFileData: () => { + cy.window().then((win: any) => { + const obj = this.get.getStyleFromWindow(win); + cy.fixture("example-style.json").should("deep.equal", obj); + }); + }, + + isExists: (selector: string) => { + this.helper.get.element(selector).should("exist"); + }, + isSelected: (selector: string, value: string) => { + cy.get(selector).find(`option[value="${value}"]`).should("be.selected"); + }, + }; } diff --git a/cypress/e2e/history.cy.ts b/cypress/e2e/history.cy.ts index a404985a..59b6001e 100644 --- a/cypress/e2e/history.cy.ts +++ b/cypress/e2e/history.cy.ts @@ -1,80 +1,97 @@ -import driver from "./driver"; +import MaputnikDriver from "./driver"; describe("history", () => { + let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + beforeAndAfter(); + let undoKeyCombo: string; let redoKeyCombo: string; before(() => { - const isMac = driver.isMac(); - undoKeyCombo = isMac ? '{meta}z' : '{ctrl}z'; - redoKeyCombo = isMac ? '{meta}{shift}z' : '{ctrl}y'; - driver.beforeEach(); + const isMac = get.isMac(); + undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z"; + redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y"; }); it("undo/redo", () => { - driver.setStyle('geojson'); - driver.openLayersModal(); + when.setStyle("geojson"); + when.openLayersModal(); - driver.isStyleStoreEqual((a: any) => a.layers, []); + should.equalStyleStore((a: any) => a.layers, []); - driver.fillLayersModal({ + when.fillLayersModal({ id: "step 1", - type: "background" - }) + type: "background", + }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": "step 1", - "type": 'background' - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "step 1", + type: "background", + }, + ] + ); - driver.openLayersModal(); - driver.fillLayersModal({ + when.openLayersModal(); + when.fillLayersModal({ id: "step 2", - type: "background" - }) + type: "background", + }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": "step 1", - "type": 'background' - }, - { - "id": "step 2", - "type": 'background' - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "step 1", + type: "background", + }, + { + id: "step 2", + type: "background", + }, + ] + ); - driver.typeKeys(undoKeyCombo); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": "step 1", - "type": 'background' - } - ]); + when.typeKeys(undoKeyCombo); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "step 1", + type: "background", + }, + ] + ); - driver.typeKeys(undoKeyCombo) - driver.isStyleStoreEqual((a: any) => a.layers, []); + when.typeKeys(undoKeyCombo); + should.equalStyleStore((a: any) => a.layers, []); - driver.typeKeys(redoKeyCombo) - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": "step 1", - "type": 'background' - } - ]); + when.typeKeys(redoKeyCombo); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "step 1", + type: "background", + }, + ] + ); - driver.typeKeys(redoKeyCombo) - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": "step 1", - "type": 'background' - }, - { - "id": "step 2", - "type": 'background' - } - ]); + when.typeKeys(redoKeyCombo); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "step 1", + type: "background", + }, + { + id: "step 2", + type: "background", + }, + ] + ); }); -}) +}); diff --git a/cypress/e2e/keyboard.cy.ts b/cypress/e2e/keyboard.cy.ts index 48e660ab..3d7a24bb 100644 --- a/cypress/e2e/keyboard.cy.ts +++ b/cypress/e2e/keyboard.cy.ts @@ -1,60 +1,61 @@ -import driver from "./driver"; +import { default as MaputnikDriver } from "./driver"; describe("keyboard", () => { + let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + beforeAndAfter(); describe("shortcuts", () => { beforeEach(() => { - driver.setupInterception(); - driver.setStyle(''); - }) + given.setupInterception(); + when.setStyle(""); + }); it("ESC should unfocus", () => { - const targetSelector = driver.getDataAttribute("nav:inspect") + " select"; - driver.focus(targetSelector); - driver.isFocused(targetSelector); + const targetSelector = "maputnik-select"; + when.focus(targetSelector); + should.beFocused(targetSelector); - //driver.typeKeys("{esc}"); - //driver.isFocused('body'); + when.typeKeys("{esc}"); + expect(should.notBeFocused(targetSelector)); }); it("'?' should show shortcuts modal", () => { - driver.typeKeys("?"); - driver.isDisplayedInViewport(driver.getDataAttribute("modal:shortcuts")); + when.typeKeys("?"); + should.beVisible("modal:shortcuts"); }); it("'o' should show open modal", () => { - driver.typeKeys("o"); - driver.isDisplayedInViewport(driver.getDataAttribute("modal:open")); + when.typeKeys("o"); + should.beVisible("modal:open"); }); it("'e' should show export modal", () => { - driver.typeKeys("e"); - driver.isDisplayedInViewport(driver.getDataAttribute("modal:export")); + when.typeKeys("e"); + should.beVisible("modal:export"); }); it("'d' should show sources modal", () => { - driver.typeKeys("d"); - driver.isDisplayedInViewport(driver.getDataAttribute("modal:sources")); + when.typeKeys("d"); + should.beVisible("modal:sources"); }); it("'s' should show settings modal", () => { - driver.typeKeys("s"); - driver.isDisplayedInViewport(driver.getDataAttribute("modal:settings")); + when.typeKeys("s"); + should.beVisible("modal:settings"); }); it("'i' should change map to inspect mode", () => { - driver.typeKeys("i"); - driver.isSelected(driver.getDataAttribute("nav:inspect"), "inspect"); + when.typeKeys("i"); + should.isSelected(get.getDataAttribute("nav:inspect"), "inspect"); }); it("'m' should focus map", () => { - driver.typeKeys("m"); - driver.isFocused(".maplibregl-canvas"); + when.typeKeys("m"); + should.beFocused(".maplibregl-canvas"); }); it("'!' should show debug modal", () => { - driver.typeKeys("!"); - driver.isDisplayedInViewport(driver.getDataAttribute("modal:debug")); + when.typeKeys("!"); + should.beVisible("modal:debug"); }); }); - }); diff --git a/cypress/e2e/layers.cy.ts b/cypress/e2e/layers.cy.ts index 1ea995a6..736a578e 100644 --- a/cypress/e2e/layers.cy.ts +++ b/cypress/e2e/layers.cy.ts @@ -1,112 +1,132 @@ var assert = require("assert"); -import driver from "./driver"; -import { v1 as uuid } from 'uuid'; +import { v1 as uuid } from "uuid"; +import MaputnikDriver from "./driver"; describe("layers", () => { + let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + beforeAndAfter(); beforeEach(() => { - driver.beforeEach(); - driver.setStyle('both'); - driver.openLayersModal(); + when.setStyle("both"); + when.openLayersModal(); }); describe("ops", () => { it("delete", () => { - var id = driver.fillLayersModal({ - type: "background" - }) + var id = when.fillLayersModal({ + type: "background", + }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": 'background' - }, - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "background", + }, + ] + ); - driver.click(driver.getDataAttribute("layer-list-item:"+id+":delete", "")) + when.click("layer-list-item:" + id + ":delete"); - driver.isStyleStoreEqual((a: any) => a.layers, []); + should.equalStyleStore((a: any) => a.layers, []); }); it("duplicate", () => { var styleObj; - var id = driver.fillLayersModal({ - type: "background" - }) + var id = when.fillLayersModal({ + type: "background", + }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": 'background' - }, - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "background", + }, + ] + ); - driver.click(driver.getDataAttribute("layer-list-item:"+id+":copy", "")); + when.click("layer-list-item:" + id + ":copy"); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id+"-copy", - "type": "background" - }, - { - "id": id, - "type": "background" - }, - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id + "-copy", + type: "background", + }, + { + id: id, + type: "background", + }, + ] + ); }); it("hide", () => { var styleObj; - var id = driver.fillLayersModal({ - type: "background" - }) + var id = when.fillLayersModal({ + type: "background", + }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": 'background' - }, - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "background", + }, + ] + ); - driver.click(driver.getDataAttribute("layer-list-item:"+id+":toggle-visibility", "")); + when.click("layer-list-item:" + id + ":toggle-visibility"); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": "background", - "layout": { - "visibility": "none" - } - }, - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "background", + layout: { + visibility: "none", + }, + }, + ] + ); - driver.click(driver.getDataAttribute("layer-list-item:"+id+":toggle-visibility", "")); + when.click("layer-list-item:" + id + ":toggle-visibility"); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": "background", - "layout": { - "visibility": "visible" - } - }, - ]); - }) - }) - - - describe('background', () => { + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "background", + layout: { + visibility: "visible", + }, + }, + ] + ); + }); + }); + describe("background", () => { it("add", () => { - var id = driver.fillLayersModal({ - type: "background" - }) + var id = when.fillLayersModal({ + type: "background", + }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": 'background' - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "background", + }, + ] + ); }); describe("modify", () => { @@ -114,17 +134,26 @@ describe("layers", () => { // Setup var id = uuid(); - driver.select(driver.getDataAttribute("add-layer.layer-type", "select"), "background"); - driver.setValue(driver.getDataAttribute("add-layer.layer-id", "input"), "background:"+id); + when.select( + get.getDataAttribute("add-layer.layer-type", "select"), + "background" + ); + when.setValue( + get.getDataAttribute("add-layer.layer-id", "input"), + "background:" + id + ); - driver.click(driver.getDataAttribute("add-layer")); + when.click("add-layer"); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": 'background:'+id, - "type": 'background' - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "background:" + id, + type: "background", + }, + ] + ); return id; } @@ -134,35 +163,47 @@ describe("layers", () => { it("id", () => { var bgId = createBackground(); - driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); + when.click("layer-list-item:background:" + bgId); var id = uuid(); - driver.setValue(driver.getDataAttribute("layer-editor.layer-id", "input"), "foobar:"+id) - driver.click(driver.getDataAttribute("min-zoom")); + when.setValue( + get.getDataAttribute("layer-editor.layer-id", "input"), + "foobar:" + id + ); + when.click("min-zoom"); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": 'foobar:'+id, - "type": 'background' - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "foobar:" + id, + type: "background", + }, + ] + ); }); it("min-zoom", () => { var bgId = createBackground(); - driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); - driver.setValue(driver.getDataAttribute("min-zoom", 'input[type="text"]'), "1"); + when.click("layer-list-item:background:" + bgId); + when.setValue( + get.getDataAttribute("min-zoom", 'input[type="text"]'), + "1" + ); - driver.click(driver.getDataAttribute("layer-editor.layer-id", "input")); + when.click("layer-editor.layer-id"); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": 'background:'+bgId, - "type": 'background', - "minzoom": 1 - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "background:" + bgId, + type: "background", + minzoom: 1, + }, + ] + ); // AND RESET! // driver.setValue(driver.getDataAttribute("min-zoom", "input"), "") @@ -179,38 +220,47 @@ describe("layers", () => { it("max-zoom", () => { var bgId = createBackground(); - driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); - driver.setValue(driver.getDataAttribute("max-zoom", 'input[type="text"]'), "1") + when.click("layer-list-item:background:" + bgId); + when.setValue( + get.getDataAttribute("max-zoom", 'input[type="text"]'), + "1" + ); - driver.click(driver.getDataAttribute("layer-editor.layer-id", "input")); + when.click("layer-editor.layer-id"); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": 'background:'+bgId, - "type": 'background', - "maxzoom": 1 - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "background:" + bgId, + type: "background", + maxzoom: 1, + }, + ] + ); }); it("comments", () => { var bgId = createBackground(); var id = uuid(); - driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); - driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), id); + when.click("layer-list-item:background:" + bgId); + when.setValue(get.getDataAttribute("layer-comment", "textarea"), id); - driver.click(driver.getDataAttribute("layer-editor.layer-id", "input")); + when.click("layer-editor.layer-id"); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": 'background:'+bgId, - "type": 'background', - metadata: { - 'maputnik:comment': id - } - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "background:" + bgId, + type: "background", + metadata: { + "maputnik:comment": id, + }, + }, + ] + ); // Unset it again. // TODO: This fails @@ -228,31 +278,33 @@ describe("layers", () => { it("color", () => { var bgId = createBackground(); - driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); + when.click("layer-list-item:background:" + bgId); - driver.click(driver.getDataAttribute("spec-field:background-color", "input")); + when.click("spec-field:background-color"); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": 'background:'+bgId, - "type": 'background' - } - ]); - - }) - }) + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: "background:" + bgId, + type: "background", + }, + ] + ); + }); + }); describe("filter", () => { it("expand/collapse"); it("compound filter"); - }) + }); describe("paint", () => { it("expand/collapse"); it("color"); it("pattern"); it("opacity"); - }) + }); // <===== describe("json-editor", () => { @@ -263,165 +315,183 @@ describe("layers", () => { it.skip("parse error", () => { var bgId = createBackground(); - driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId)); + when.click("layer-list-item:background:" + bgId); var errorSelector = ".CodeMirror-lint-marker-error"; - driver.doesNotExists(errorSelector); + should.notExist(errorSelector); - driver.click(".CodeMirror"); - driver.typeKeys("\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"); - driver.isExists(errorSelector); + when.click(".CodeMirror"); + when.typeKeys( + "\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {" + ); + should.isExists(errorSelector); - driver.click(driver.getDataAttribute("layer-editor.layer-id")); + when.click("layer-editor.layer-id"); }); }); - }) + }); }); - describe('fill', () => { + describe("fill", () => { it("add", () => { - - var id = driver.fillLayersModal({ + var id = when.fillLayersModal({ type: "fill", - layer: "example" + layer: "example", }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": 'fill', - "source": "example" - } - ]); - }) + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "fill", + source: "example", + }, + ] + ); + }); // TODO: Change source - it("change source") + it("change source"); }); - describe('line', () => { + describe("line", () => { it("add", () => { - var id = driver.fillLayersModal({ + var id = when.fillLayersModal({ type: "line", - layer: "example" + layer: "example", }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": "line", - "source": "example", - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "line", + source: "example", + }, + ] + ); }); it("groups", () => { // TODO // Click each of the layer groups. - }) + }); }); - describe('symbol', () => { + describe("symbol", () => { it("add", () => { - var id = driver.fillLayersModal({ + var id = when.fillLayersModal({ type: "symbol", - layer: "example" + layer: "example", }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": "symbol", - "source": "example", - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "symbol", + source: "example", + }, + ] + ); }); }); - describe('raster', () => { + describe("raster", () => { it("add", () => { - var id = driver.fillLayersModal({ + var id = when.fillLayersModal({ type: "raster", - layer: "raster" + layer: "raster", }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": "raster", - "source": "raster", - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "raster", + source: "raster", + }, + ] + ); }); }); - describe('circle', () => { + describe("circle", () => { it("add", () => { - var id = driver.fillLayersModal({ + var id = when.fillLayersModal({ type: "circle", - layer: "example" + layer: "example", }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": "circle", - "source": "example", - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "circle", + source: "example", + }, + ] + ); }); - }); - describe('fill extrusion', () => { + describe("fill extrusion", () => { it("add", () => { - var id = driver.fillLayersModal({ + var id = when.fillLayersModal({ type: "fill-extrusion", - layer: "example" + layer: "example", }); - driver.isStyleStoreEqual((a: any) => a.layers, [ - { - "id": id, - "type": 'fill-extrusion', - "source": "example" - } - ]); + should.equalStyleStore( + (a: any) => a.layers, + [ + { + id: id, + type: "fill-extrusion", + source: "example", + }, + ] + ); }); }); - describe("groups", () => { it("simple", () => { - driver.setStyle("geojson"); + when.setStyle("geojson"); - driver.openLayersModal(); - driver.fillLayersModal({ + when.openLayersModal(); + when.fillLayersModal({ id: "foo", - type: "background" - }) + type: "background", + }); - driver.openLayersModal(); - driver.fillLayersModal({ + when.openLayersModal(); + when.fillLayersModal({ id: "foo_bar", - type: "background" - }) + type: "background", + }); - driver.openLayersModal(); - driver.fillLayersModal({ + when.openLayersModal(); + when.fillLayersModal({ id: "foo_bar_baz", - type: "background" - }) + type: "background", + }); - driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo")); - driver.isNotDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar")); - driver.isNotDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar_baz")); + should.beVisible("layer-list-item:foo"); - driver.click(driver.getDataAttribute("layer-list-group:foo-0")); + should.notBeVisible("layer-list-item:foo_bar"); + should.notBeVisible("layer-list-item:foo_bar_baz"); - driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo")); - driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar")); - driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar_baz")); - }) - }) + when.click("layer-list-group:foo-0"); + + should.beVisible("layer-list-item:foo"); + should.beVisible("layer-list-item:foo_bar"); + should.beVisible("layer-list-item:foo_bar_baz"); + }); + }); }); diff --git a/cypress/e2e/map.cy.ts b/cypress/e2e/map.cy.ts index 102e3055..ea0b695b 100644 --- a/cypress/e2e/map.cy.ts +++ b/cypress/e2e/map.cy.ts @@ -1,25 +1,25 @@ -import driver from "./driver"; +import MaputnikDriver from "./driver"; describe("map", () => { - describe("zoom level", () => { - beforeEach(() => { - driver.beforeEach(); - }); - it("via url", () => { - var zoomLevel = 12.37; - driver.setStyle("geojson", zoomLevel); - driver.isDisplayedInViewport(".maplibregl-ctrl-zoom"); - // HM TODO - //driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel); - }) - it("via map controls", () => { - var zoomLevel = 12.37; - driver.setStyle("geojson", zoomLevel); - - driver.click(".maplibregl-ctrl-zoom-in"); - driver.isDisplayedInViewport(".maplibregl-ctrl-zoom"); - // HM TODO - //driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel + 1); - }) - }) - }) + let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + beforeAndAfter(); + describe("zoom level", () => { + it("via url", () => { + var zoomLevel = 12.37; + when.setStyle("geojson", zoomLevel); + should.beVisible("maplibre:ctrl-zoom"); + // HM TODO + //driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel); + }); + + it("via map controls", () => { + var zoomLevel = 12.37; + when.setStyle("geojson", zoomLevel); + + when.click("maplibre:ctrl-zoom"); + should.beVisible("maplibre:ctrl-zoom"); + // HM TODO + //driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel + 1); + }); + }); +}); diff --git a/cypress/e2e/modals.cy.ts b/cypress/e2e/modals.cy.ts index f8a044a1..a910d2c4 100644 --- a/cypress/e2e/modals.cy.ts +++ b/cypress/e2e/modals.cy.ts @@ -1,137 +1,160 @@ -import driver from "./driver"; +import MaputnikDriver from "./driver"; describe("modals", () => { + let { beforeAndAfter, given, when, get, should } = new MaputnikDriver(); + beforeAndAfter(); beforeEach(() => { - driver.beforeEach(); - driver.setStyle(''); + when.setStyle(""); }); describe("open", () => { beforeEach(() => { - driver.click(driver.getDataAttribute("nav:open")); + when.click("nav:open"); }); it("close", () => { - driver.closeModal("modal:open"); + when.closeModal("modal:open"); + should.notExist("modal:open"); }); it.skip("upload", () => { // HM: I was not able to make the following choose file actually to select a file and close the modal... - driver.chooseExampleFile(); + when.chooseExampleFile(); - driver.isStyleStoreEqualToExampleFileData(); + should.isStyleStoreEqualToExampleFileData(); }); it("load from url", () => { - var styleFileUrl = driver.getExampleFileUrl(); + var styleFileUrl = get.getExampleFileUrl(); - driver.setValue(driver.getDataAttribute("modal:open.url.input"), styleFileUrl); - driver.click(driver.getDataAttribute("modal:open.url.button")) - driver.waitForExampleFileRequset(); + when.setValue(get.getDataAttribute("modal:open.url.input"), styleFileUrl); + when.click("modal:open.url.button"); + when.waitForExampleFileRequset(); - driver.isStyleStoreEqualToExampleFileData(); + should.isStyleStoreEqualToExampleFileData(); }); - }) + }); describe("shortcuts", () => { it("open/close", () => { - driver.setStyle(''); - - driver.typeKeys("?"); - - driver.isDisplayedInViewport(driver.getDataAttribute("modal:shortcuts")); - - driver.closeModal("modal:shortcuts"); + when.setStyle(""); + when.typeKeys("?"); + when.closeModal("modal:shortcuts"); + should.notExist("modal:shortcuts"); }); - }); describe("export", () => { beforeEach(() => { - driver.click(driver.getDataAttribute("nav:export")); + when.click("nav:export"); }); it("close", () => { - driver.closeModal("modal:export"); + when.closeModal("modal:export"); + should.notExist("modal:export"); }); // TODO: Work out how to download a file and check the contents - it("download") - - }) + it("download"); + }); describe("sources", () => { - it("active sources") - it("public source") - it("add new source") - }) + it("active sources"); + it("public source"); + it("add new source"); + }); describe("inspect", () => { it("toggle", () => { - driver.setStyle('geojson'); + when.setStyle("geojson"); - driver.select(driver.getDataAttribute("nav:inspect", "select"), "inspect"); - }) - }) + when.select(get.getDataAttribute("nav:inspect", "select"), "inspect"); + }); + }); describe("style settings", () => { beforeEach(() => { - driver.click(driver.getDataAttribute("nav:settings")); + when.click("nav:settings"); }); it("name", () => { - driver.setValue(driver.getDataAttribute("modal:settings.name"), "foobar"); - driver.click(driver.getDataAttribute("modal:settings.owner")); + when.setValue(get.getDataAttribute("modal:settings.name"), "foobar"); + when.click("modal:settings.owner"); - driver.isStyleStoreEqual((obj) => obj.name, "foobar"); - }) + should.equalStyleStore((obj) => obj.name, "foobar"); + }); it("owner", () => { - driver.setValue(driver.getDataAttribute("modal:settings.owner"), "foobar") - driver.click(driver.getDataAttribute("modal:settings.name")); + when.setValue(get.getDataAttribute("modal:settings.owner"), "foobar"); + when.click("modal:settings.name"); - driver.isStyleStoreEqual((obj) => obj.owner, "foobar"); - }) + should.equalStyleStore((obj) => obj.owner, "foobar"); + }); it("sprite url", () => { - driver.setValue(driver.getDataAttribute("modal:settings.sprite"), "http://example.com") - driver.click(driver.getDataAttribute("modal:settings.name")); + when.setValue( + get.getDataAttribute("modal:settings.sprite"), + "http://example.com" + ); + when.click("modal:settings.name"); - driver.isStyleStoreEqual((obj) => obj.sprite, "http://example.com"); - }) + should.equalStyleStore((obj) => obj.sprite, "http://example.com"); + }); it("glyphs url", () => { - var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf" - driver.setValue(driver.getDataAttribute("modal:settings.glyphs"), glyphsUrl); - driver.click(driver.getDataAttribute("modal:settings.name")); + var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf"; + when.setValue(get.getDataAttribute("modal:settings.glyphs"), glyphsUrl); + when.click("modal:settings.name"); - driver.isStyleStoreEqual((obj) => obj.glyphs, glyphsUrl); - }) + should.equalStyleStore((obj) => obj.glyphs, glyphsUrl); + }); it("maptiler access token", () => { var apiKey = "testing123"; - driver.setValue(driver.getDataAttribute("modal:settings.maputnik:openmaptiles_access_token"), apiKey); - driver.click(driver.getDataAttribute("modal:settings.name")); + when.setValue( + get.getDataAttribute( + "modal:settings.maputnik:openmaptiles_access_token" + ), + apiKey + ); + when.click("modal:settings.name"); - driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:openmaptiles_access_token"], apiKey); - }) + should.equalStyleStore( + (obj) => obj.metadata["maputnik:openmaptiles_access_token"], + apiKey + ); + }); it("thunderforest access token", () => { var apiKey = "testing123"; - driver.setValue(driver.getDataAttribute("modal:settings.maputnik:thunderforest_access_token"), apiKey); - driver.click(driver.getDataAttribute("modal:settings.name")); + when.setValue( + get.getDataAttribute( + "modal:settings.maputnik:thunderforest_access_token" + ), + apiKey + ); + when.click("modal:settings.name"); - driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:thunderforest_access_token"], apiKey); - }) + should.equalStyleStore( + (obj) => obj.metadata["maputnik:thunderforest_access_token"], + apiKey + ); + }); it("style renderer", () => { - cy.on('uncaught:exception', () => false); // this is due to the fact that this is an invalid style for openlayers - driver.select(driver.getDataAttribute("modal:settings.maputnik:renderer"), "ol"); - driver.isSelected(driver.getDataAttribute("modal:settings.maputnik:renderer"), "ol"); - - driver.click(driver.getDataAttribute("modal:settings.name")); + cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers + when.select( + get.getDataAttribute("modal:settings.maputnik:renderer"), + "ol" + ); + should.isSelected( + get.getDataAttribute("modal:settings.maputnik:renderer"), + "ol" + ); - driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:renderer"], "ol"); - }) - }) + when.click("modal:settings.name"); + + should.equalStyleStore((obj) => obj.metadata["maputnik:renderer"], "ol"); + }); + }); describe("sources", () => { - it("toggle") - }) -}) + it("toggle"); + }); +}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index f80f74f8..c3760faa 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -14,7 +14,8 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands' +import "cypress-plugin-tab"; +import "./commands"; // Alternatively you can use CommonJS syntax: -// require('./commands') \ No newline at end of file +// require('./commands') diff --git a/src/components/App.jsx b/src/components/App.jsx index 6714f42e..5ef42a0d 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -1,43 +1,50 @@ -import autoBind from 'react-autobind'; -import React from 'react' -import cloneDeep from 'lodash.clonedeep' -import clamp from 'lodash.clamp' -import buffer from 'buffer' -import get from 'lodash.get' -import {unset} from 'lodash' -import {arrayMoveMutable} from 'array-move' -import url from 'url' +import { arrayMoveMutable } from "array-move"; +import buffer from "buffer"; +import { unset } from "lodash"; +import clamp from "lodash.clamp"; +import cloneDeep from "lodash.clonedeep"; +import get from "lodash.get"; +import React from "react"; +import autoBind from "react-autobind"; import hash from "string-hash"; +import url from "url"; -import MapMaplibreGl from './MapMaplibreGl' -import MapOpenLayers from './MapOpenLayers' -import LayerList from './LayerList' -import LayerEditor from './LayerEditor' -import AppToolbar from './AppToolbar' -import AppLayout from './AppLayout' -import MessagePanel from './AppMessagePanel' +import AppLayout from "./AppLayout"; +import MessagePanel from "./AppMessagePanel"; +import AppToolbar from "./AppToolbar"; +import LayerEditor from "./LayerEditor"; +import LayerList from "./LayerList"; +import MapMaplibreGl from "./MapMaplibreGl"; +import MapOpenLayers from "./MapOpenLayers"; -import ModalSettings from './ModalSettings' -import ModalExport from './ModalExport' -import ModalSources from './ModalSources' -import ModalOpen from './ModalOpen' -import ModalShortcuts from './ModalShortcuts' -import ModalSurvey from './ModalSurvey' -import ModalDebug from './ModalDebug' +import ModalDebug from "./ModalDebug"; +import ModalExport from "./ModalExport"; +import ModalOpen from "./ModalOpen"; +import ModalSettings from "./ModalSettings"; +import ModalShortcuts from "./ModalShortcuts"; +import ModalSources from "./ModalSources"; +import ModalSurvey from "./ModalSurvey"; -import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata' -import {latest, validate} from '@maplibre/maplibre-gl-style-spec' -import style from '../libs/style' -import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen' -import { undoMessages, redoMessages } from '../libs/diffmessage' -import { StyleStore } from '../libs/stylestore' -import { ApiStyleStore } from '../libs/apistore' -import { RevisionStore } from '../libs/revisions' -import LayerWatcher from '../libs/layerwatcher' -import tokens from '../config/tokens.json' -import isEqual from 'lodash.isequal' -import Debug from '../libs/debug' -import {formatLayerId} from '../util/format'; +import { latest, validate } from "@maplibre/maplibre-gl-style-spec"; +import isEqual from "lodash.isequal"; +import tokens from "../config/tokens.json"; +import { ApiStyleStore } from "../libs/apistore"; +import Debug from "../libs/debug"; +import { redoMessages, undoMessages } from "../libs/diffmessage"; +import LayerWatcher from "../libs/layerwatcher"; +import { + downloadGlyphsMetadata, + downloadSpriteMetadata, +} from "../libs/metadata"; +import { RevisionStore } from "../libs/revisions"; +import style from "../libs/style"; +import { StyleStore } from "../libs/stylestore"; +import { + initialStyleUrl, + loadStyleUrl, + removeStyleQuerystring, +} from "../libs/urlopen"; +import { formatLayerId } from "../util/format"; // Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed. window.Buffer = buffer.Buffer; @@ -47,18 +54,20 @@ function setFetchAccessToken(url, mapStyle) { const matchesMaptiler = url.match(/\.maptiler\.com/); const matchesThunderforest = url.match(/\.thunderforest\.com/); if (matchesTilehosting || matchesMaptiler) { - const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true}) + const accessToken = style.getAccessToken("openmaptiles", mapStyle, { + allowFallback: true, + }); if (accessToken) { - return url.replace('{key}', accessToken) + return url.replace("{key}", accessToken); } - } - else if (matchesThunderforest) { - const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true}) + } else if (matchesThunderforest) { + const accessToken = style.getAccessToken("thunderforest", mapStyle, { + allowFallback: true, + }); if (accessToken) { - return url.replace('{key}', accessToken) + return url.replace("{key}", accessToken); } - } - else { + } else { return url; } } @@ -70,124 +79,135 @@ function updateRootSpec(spec, fieldName, newValues) { ...spec.$root, [fieldName]: { ...spec.$root[fieldName], - values: newValues - } - } - } + values: newValues, + }, + }, + }; } export default class App extends React.Component { constructor(props) { - super(props) + super(props); autoBind(this); - this.revisionStore = new RevisionStore() - const params = new URLSearchParams(window.location.search.substring(1)) - let port = params.get("localport") - if (port == null && (window.location.port != 80 && window.location.port != 443)) { - port = window.location.port + this.revisionStore = new RevisionStore(); + const params = new URLSearchParams(window.location.search.substring(1)); + let port = params.get("localport"); + if ( + port == null && + window.location.port != 80 && + window.location.port != 443 + ) { + port = window.location.port; } this.styleStore = new ApiStyleStore({ - onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}), + onLocalStyleChange: (mapStyle) => + this.onStyleChanged(mapStyle, { save: false }), port: port, - host: params.get("localhost") - }) - + host: params.get("localhost"), + }); const shortcuts = [ { key: "?", handler: () => { this.toggleModal("shortcuts"); - } + }, }, { key: "o", handler: () => { this.toggleModal("open"); - } + }, }, { key: "e", handler: () => { this.toggleModal("export"); - } + }, }, { key: "d", handler: () => { this.toggleModal("sources"); - } + }, }, { key: "s", handler: () => { this.toggleModal("settings"); - } + }, }, { key: "i", handler: () => { - this.setMapState( - this.state.mapState === "map" ? "inspect" : "map" - ); - } + this.setMapState(this.state.mapState === "map" ? "inspect" : "map"); + }, }, { key: "m", handler: () => { document.querySelector(".maplibregl-canvas").focus(); - } + }, }, { key: "!", handler: () => { this.toggleModal("debug"); - } + }, }, - ] + ]; document.body.addEventListener("keyup", (e) => { - if(e.key === "Escape") { + if (e.key === "Escape") { e.target.blur(); document.body.focus(); - } - else if(this.state.isOpen.shortcuts || document.activeElement === document.body) { + } else if ( + this.state.isOpen.shortcuts || + document.activeElement === document.body + ) { const shortcut = shortcuts.find((shortcut) => { - return (shortcut.key === e.key) - }) + return shortcut.key === e.key; + }); - if(shortcut) { + if (shortcut) { this.setModal("shortcuts", false); shortcut.handler(e); } } - }) + }); - const styleUrl = initialStyleUrl() - if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) { - this.styleStore = new StyleStore() - loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle)) - removeStyleQuerystring() + const styleUrl = initialStyleUrl(); + if ( + styleUrl && + window.confirm( + "Load style from URL: " + styleUrl + " and discard current changes?" + ) + ) { + this.styleStore = new StyleStore(); + loadStyleUrl(styleUrl, (mapStyle) => this.onStyleChanged(mapStyle)); + removeStyleQuerystring(); } else { - if(styleUrl) { - removeStyleQuerystring() + if (styleUrl) { + removeStyleQuerystring(); } - this.styleStore.init(err => { - if(err) { - console.log('Falling back to local storage for storing styles') - this.styleStore = new StyleStore() + this.styleStore.init((err) => { + if (err) { + console.log("Falling back to local storage for storing styles"); + this.styleStore = new StyleStore(); } - this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle, {initialLoad: true})) + this.styleStore.latestStyle((mapStyle) => + this.onStyleChanged(mapStyle, { initialLoad: true }) + ); - if(Debug.enabled()) { + if (Debug.enabled()) { Debug.set("maputnik", "styleStore", this.styleStore); Debug.set("maputnik", "revisionStore", this.revisionStore); } - }) + }); } - if(Debug.enabled()) { + if (Debug.enabled()) { Debug.set("maputnik", "revisionStore", this.revisionStore); Debug.set("maputnik", "styleStore", this.styleStore); } @@ -228,35 +248,32 @@ export default class App extends React.Component { openlayersDebugOptions: { debugToolbox: false, }, - } + }; this.layerWatcher = new LayerWatcher({ - onVectorLayersChange: v => this.setState({ vectorLayers: v }) - }) + onVectorLayersChange: (v) => this.setState({ vectorLayers: v }), + }); } handleKeyPress = (e) => { - if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) { - if(e.metaKey && e.shiftKey && e.keyCode === 90) { + if (navigator.platform.toUpperCase().indexOf("MAC") >= 0) { + if (e.metaKey && e.shiftKey && e.keyCode === 90) { e.preventDefault(); this.onRedo(e); - } - else if(e.metaKey && e.keyCode === 90) { + } else if (e.metaKey && e.keyCode === 90) { e.preventDefault(); this.onUndo(e); } - } - else { - if(e.ctrlKey && e.keyCode === 90) { + } else { + if (e.ctrlKey && e.keyCode === 90) { e.preventDefault(); this.onUndo(e); - } - else if(e.ctrlKey && e.keyCode === 89) { + } else if (e.ctrlKey && e.keyCode === 89) { e.preventDefault(); this.onRedo(e); } } - } + }; componentDidMount() { window.addEventListener("keydown", this.handleKeyPress); @@ -267,33 +284,38 @@ export default class App extends React.Component { } saveStyle(snapshotStyle) { - this.styleStore.save(snapshotStyle) + this.styleStore.save(snapshotStyle); } updateFonts(urlTemplate) { - const metadata = this.state.mapStyle.metadata || {} - const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles + const metadata = this.state.mapStyle.metadata || {}; + const accessToken = + metadata["maputnik:openmaptiles_access_token"] || tokens.openmaptiles; - let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate; - downloadGlyphsMetadata(glyphUrl, fonts => { - this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)}) - }) + let glyphUrl = + typeof urlTemplate === "string" + ? urlTemplate.replace("{key}", accessToken) + : urlTemplate; + downloadGlyphsMetadata(glyphUrl, (fonts) => { + this.setState({ spec: updateRootSpec(this.state.spec, "glyphs", fonts) }); + }); } updateIcons(baseUrl) { - downloadSpriteMetadata(baseUrl, icons => { - this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)}) - }) + downloadSpriteMetadata(baseUrl, (icons) => { + this.setState({ spec: updateRootSpec(this.state.spec, "sprite", icons) }); + }); } onChangeMetadataProperty = (property, value) => { // If we're changing renderer reset the map state. if ( - property === 'maputnik:renderer' && - value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mlgljs') + property === "maputnik:renderer" && + value !== + get(this.state.mapStyle, ["metadata", "maputnik:renderer"], "mlgljs") ) { this.setState({ - mapState: 'map' + mapState: "map", }); } @@ -301,13 +323,13 @@ export default class App extends React.Component { ...this.state.mapStyle, metadata: { ...this.state.mapStyle.metadata, - [property]: value - } - } - this.onStyleChanged(changedStyle) - } + [property]: value, + }, + }; + this.onStyleChanged(changedStyle); + }; - onStyleChanged = (newStyle, opts={}) => { + onStyleChanged = (newStyle, opts = {}) => { opts = { save: true, addRevision: true, @@ -338,9 +360,11 @@ export default class App extends React.Component { }); } - const mappedErrors = layerErrors.concat(errors).map(error => { + const mappedErrors = layerErrors.concat(errors).map((error) => { // Special case: Duplicate layer id - const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/); + const dupMatch = error.message.match( + /layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/ + ); if (dupMatch) { const [matchStr, index, message] = dupMatch; return { @@ -351,13 +375,15 @@ export default class App extends React.Component { index: parseInt(index, 10), key: "id", message, - } - } - } + }, + }, + }; } // Special case: Invalid source - const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/); + const invalidSourceMatch = error.message.match( + /layers\[(\d+)\]: (source "(?:.*)" not found)/ + ); if (invalidSourceMatch) { const [matchStr, index, message] = invalidSourceMatch; return { @@ -368,15 +394,17 @@ export default class App extends React.Component { index: parseInt(index, 10), key: "source", message, - } - } - } + }, + }, + }; } - const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/); + const layerMatch = error.message.match( + /layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/ + ); if (layerMatch) { const [matchStr, index, group, property, message] = layerMatch; - const key = (group && property) ? [group, property].join(".") : property; + const key = group && property ? [group, property].join(".") : property; return { message: error.message, parsed: { @@ -384,12 +412,11 @@ export default class App extends React.Component { data: { index: parseInt(index, 10), key, - message - } - } - } - } - else { + message, + }, + }, + }; + } else { return { message: error.message, }; @@ -400,27 +427,26 @@ export default class App extends React.Component { if (errors.length > 0) { dirtyMapStyle = cloneDeep(newStyle); - errors.forEach(error => { - const {message} = error; + errors.forEach((error) => { + const { message } = error; if (message) { try { const objPath = message.split(":")[0]; // Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter' const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0]; unset(dirtyMapStyle, unsetPath); - } - catch (err) { + } catch (err) { console.warn(err); } } }); } - if(newStyle.glyphs !== this.state.mapStyle.glyphs) { - this.updateFonts(newStyle.glyphs) + if (newStyle.glyphs !== this.state.mapStyle.glyphs) { + this.updateFonts(newStyle.glyphs); } - if(newStyle.sprite !== this.state.mapStyle.sprite) { - this.updateIcons(newStyle.sprite) + if (newStyle.sprite !== this.state.mapStyle.sprite) { + this.updateIcons(newStyle.sprite); } if (opts.addRevision) { @@ -430,251 +456,266 @@ export default class App extends React.Component { this.saveStyle(newStyle); } - this.setState({ - mapStyle: newStyle, - dirtyMapStyle: dirtyMapStyle, - errors: mappedErrors, - }, () => { - this.fetchSources(); - this.setStateInUrl(); - }) - - } + this.setState( + { + mapStyle: newStyle, + dirtyMapStyle: dirtyMapStyle, + errors: mappedErrors, + }, + () => { + this.fetchSources(); + this.setStateInUrl(); + } + ); + }; onUndo = () => { - const activeStyle = this.revisionStore.undo() + const activeStyle = this.revisionStore.undo(); - const messages = undoMessages(this.state.mapStyle, activeStyle) - this.onStyleChanged(activeStyle, {addRevision: false}); + const messages = undoMessages(this.state.mapStyle, activeStyle); + this.onStyleChanged(activeStyle, { addRevision: false }); this.setState({ infos: messages, - }) - } + }); + }; onRedo = () => { - const activeStyle = this.revisionStore.redo() - const messages = redoMessages(this.state.mapStyle, activeStyle) - this.onStyleChanged(activeStyle, {addRevision: false}); + const activeStyle = this.revisionStore.redo(); + const messages = redoMessages(this.state.mapStyle, activeStyle); + this.onStyleChanged(activeStyle, { addRevision: false }); this.setState({ infos: messages, - }) - } + }); + }; onMoveLayer = (move) => { let { oldIndex, newIndex } = move; let layers = this.state.mapStyle.layers; - oldIndex = clamp(oldIndex, 0, layers.length-1); - newIndex = clamp(newIndex, 0, layers.length-1); - if(oldIndex === newIndex) return; + oldIndex = clamp(oldIndex, 0, layers.length - 1); + newIndex = clamp(newIndex, 0, layers.length - 1); + if (oldIndex === newIndex) return; if (oldIndex === this.state.selectedLayerIndex) { this.setState({ - selectedLayerIndex: newIndex + selectedLayerIndex: newIndex, }); } layers = layers.slice(0); arrayMoveMutable(layers, oldIndex, newIndex); this.onLayersChange(layers); - } + }; onLayersChange = (changedLayers) => { const changedStyle = { ...this.state.mapStyle, - layers: changedLayers - } - this.onStyleChanged(changedStyle) - } + layers: changedLayers, + }; + this.onStyleChanged(changedStyle); + }; onLayerDestroy = (index) => { let layers = this.state.mapStyle.layers; const remainingLayers = layers.slice(0); remainingLayers.splice(index, 1); this.onLayersChange(remainingLayers); - } + }; onLayerCopy = (index) => { let layers = this.state.mapStyle.layers; - const changedLayers = layers.slice(0) + const changedLayers = layers.slice(0); - const clonedLayer = cloneDeep(changedLayers[index]) - clonedLayer.id = clonedLayer.id + "-copy" - changedLayers.splice(index, 0, clonedLayer) - this.onLayersChange(changedLayers) - } + const clonedLayer = cloneDeep(changedLayers[index]); + clonedLayer.id = clonedLayer.id + "-copy"; + changedLayers.splice(index, 0, clonedLayer); + this.onLayersChange(changedLayers); + }; onLayerVisibilityToggle = (index) => { let layers = this.state.mapStyle.layers; - const changedLayers = layers.slice(0) + const changedLayers = layers.slice(0); - const layer = { ...changedLayers[index] } - const changedLayout = 'layout' in layer ? {...layer.layout} : {} - changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none' - - layer.layout = changedLayout - changedLayers[index] = layer - this.onLayersChange(changedLayers) - } + const layer = { ...changedLayers[index] }; + const changedLayout = "layout" in layer ? { ...layer.layout } : {}; + changedLayout.visibility = + changedLayout.visibility === "none" ? "visible" : "none"; + layer.layout = changedLayout; + changedLayers[index] = layer; + this.onLayersChange(changedLayers); + }; onLayerIdChange = (index, oldId, newId) => { - const changedLayers = this.state.mapStyle.layers.slice(0) + const changedLayers = this.state.mapStyle.layers.slice(0); changedLayers[index] = { ...changedLayers[index], - id: newId - } + id: newId, + }; - this.onLayersChange(changedLayers) - } + this.onLayersChange(changedLayers); + }; onLayerChanged = (index, layer) => { - const changedLayers = this.state.mapStyle.layers.slice(0) - changedLayers[index] = layer + const changedLayers = this.state.mapStyle.layers.slice(0); + changedLayers[index] = layer; - this.onLayersChange(changedLayers) - } + this.onLayersChange(changedLayers); + }; setMapState = (newState) => { - this.setState({ - mapState: newState - }, this.setStateInUrl); - } + this.setState( + { + mapState: newState, + }, + this.setStateInUrl + ); + }; setDefaultValues = (styleObj) => { - const metadata = styleObj.metadata || {} - if(metadata['maputnik:renderer'] === undefined) { + const metadata = styleObj.metadata || {}; + if (metadata["maputnik:renderer"] === undefined) { const changedStyle = { ...styleObj, metadata: { ...styleObj.metadata, - 'maputnik:renderer': 'mlgljs' - } - } - return changedStyle + "maputnik:renderer": "mlgljs", + }, + }; + return changedStyle; } else { - return styleObj + return styleObj; } - } + }; openStyle = (styleObj) => { - styleObj = this.setDefaultValues(styleObj) - this.onStyleChanged(styleObj) - } + styleObj = this.setDefaultValues(styleObj); + this.onStyleChanged(styleObj); + }; fetchSources() { const sourceList = {}; - for(let [key, val] of Object.entries(this.state.mapStyle.sources)) { - if( + for (let [key, val] of Object.entries(this.state.mapStyle.sources)) { + if ( !this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url") ) { sourceList[key] = { type: val.type, - layers: [] + layers: [], }; let url = val.url; try { - url = setFetchAccessToken(url, this.state.mapStyle) - } catch(err) { + url = setFetchAccessToken(url, this.state.mapStyle); + } catch (err) { console.warn("Failed to setFetchAccessToken: ", err); } fetch(url, { - mode: 'cors', + mode: "cors", }) - .then(response => response.json()) - .then(json => { + .then((response) => response.json()) + .then((json) => { + if (!json.hasOwnProperty("vector_layers")) { + return; + } - if(!json.hasOwnProperty("vector_layers")) { - return; - } + // Create new objects before setState + const sources = Object.assign( + {}, + { + [key]: this.state.sources[key], + } + ); - // Create new objects before setState - const sources = Object.assign({}, { - [key]: this.state.sources[key], + for (let layer of json.vector_layers) { + sources[key].layers.push(layer.id); + } + + console.debug("Updating source: " + key); + this.setState({ + sources: sources, + }); + }) + .catch((err) => { + console.error("Failed to process sources for '%s'", url, err); }); - - for(let layer of json.vector_layers) { - sources[key].layers.push(layer.id) - } - - console.debug("Updating source: "+key); - this.setState({ - sources: sources - }); - }) - .catch(err => { - console.error("Failed to process sources for '%s'", url, err); - }); - } - else { - sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key]; + } else { + sourceList[key] = + this.state.sources[key] || this.state.mapStyle.sources[key]; } } - if(!isEqual(this.state.sources, sourceList)) { + if (!isEqual(this.state.sources, sourceList)) { console.debug("Setting sources"); this.setState({ - sources: sourceList - }) + sources: sourceList, + }); } } - _getRenderer () { + _getRenderer() { const metadata = this.state.mapStyle.metadata || {}; - return metadata['maputnik:renderer'] || 'mlgljs'; + return metadata["maputnik:renderer"] || "mlgljs"; } onMapChange = (mapView) => { this.setState({ mapView, }); - } + }; mapRenderer() { - const {mapStyle, dirtyMapStyle} = this.state; + const { mapStyle, dirtyMapStyle } = this.state; const metadata = this.state.mapStyle.metadata || {}; const mapProps = { - mapStyle: (dirtyMapStyle || mapStyle), + mapStyle: dirtyMapStyle || mapStyle, replaceAccessTokens: (mapStyle) => { return style.replaceAccessTokens(mapStyle, { - allowFallback: true + allowFallback: true, }); }, onDataChange: (e) => { - this.layerWatcher.analyzeMap(e.map) + this.layerWatcher.analyzeMap(e.map); this.fetchSources(); }, - } + }; const renderer = this._getRenderer(); let mapElement; // Check if OL code has been loaded? - if(renderer === 'ol') { - mapElement = + if (renderer === "ol") { + mapElement = ( + + ); } else { - mapElement = + mapElement = ( + + ); } let filterName; - if(this.state.mapState.match(/^filter-/)) { + if (this.state.mapState.match(/^filter-/)) { filterName = this.state.mapState.replace(/^filter-/, ""); } const elementStyle = {}; @@ -682,38 +723,42 @@ export default class App extends React.Component { elementStyle.filter = `url('#${filterName}')`; } - return
- {mapElement} -
+ return ( +
+ {mapElement} +
+ ); } setStateInUrl = () => { - const {mapState, mapStyle, isOpen} = this.state; - const {selectedLayerIndex} = this.state; + const { mapState, mapStyle, isOpen } = this.state; + const { selectedLayerIndex } = this.state; const url = new URL(location.href); const hashVal = hash(JSON.stringify(mapStyle)); url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`); const openModals = Object.entries(isOpen) - .map(([key, val]) => (val === true ? key : null)) - .filter(val => val !== null); + .map(([key, val]) => (val === true ? key : null)) + .filter((val) => val !== null); if (openModals.length > 0) { url.searchParams.set("modal", openModals.join(",")); - } - else { + } else { url.searchParams.delete("modal"); } if (mapState === "map") { url.searchParams.delete("view"); - } - else if (mapState === "inspect") { + } else if (mapState === "inspect") { url.searchParams.set("view", "inspect"); } - history.replaceState({selectedLayerIndex}, "Maputnik", url.href); - } + history.replaceState({ selectedLayerIndex }, "Maputnik", url.href); + }; getInitialStateFromUrl = (mapStyle) => { const url = new URL(location.href); @@ -721,7 +766,7 @@ export default class App extends React.Component { if (modalParam && modalParam !== "") { const modals = modalParam.split(","); const modalObj = {}; - modals.forEach(modalName => { + modals.forEach((modalName) => { modalObj[modalName] = true; }); @@ -729,7 +774,7 @@ export default class App extends React.Component { isOpen: { ...this.state.isOpen, ...modalObj, - } + }, }); } @@ -760,31 +805,36 @@ export default class App extends React.Component { selectedLayerOriginalId: mapStyle.layers[selectedLayerIndex].id, }); } - } - catch (err) { + } catch (err) { console.warn(err); } } - } + }; onLayerSelect = (index) => { - this.setState({ - selectedLayerIndex: index, - selectedLayerOriginalId: this.state.mapStyle.layers[index].id, - }, this.setStateInUrl); - } + this.setState( + { + selectedLayerIndex: index, + selectedLayerOriginalId: this.state.mapStyle.layers[index].id, + }, + this.setStateInUrl + ); + }; setModal(modalName, value) { - if(modalName === 'survey' && value === false) { - localStorage.setItem('survey', ''); + if (modalName === "survey" && value === false) { + localStorage.setItem("survey", ""); } - this.setState({ - isOpen: { - ...this.state.isOpen, - [modalName]: value - } - }, this.setStateInUrl) + this.setState( + { + isOpen: { + ...this.state.isOpen, + [modalName]: value, + }, + }, + this.setStateInUrl + ); } toggleModal(modalName) { @@ -796,131 +846,147 @@ export default class App extends React.Component { openlayersDebugOptions: { ...this.state.openlayersDebugOptions, [key]: value, - } + }, }); - } + }; onChangeMaplibreGlDebug = (key, value) => { this.setState({ maplibreGlDebugOptions: { ...this.state.maplibreGlDebugOptions, [key]: value, - } + }, }); - } + }; render() { - const layers = this.state.mapStyle.layers || [] - const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null - const metadata = this.state.mapStyle.metadata || {} + const layers = this.state.mapStyle.layers || []; + const selectedLayer = + layers.length > 0 ? layers[this.state.selectedLayerIndex] : null; + const metadata = this.state.mapStyle.metadata || {}; - const toolbar = - - const layerList = - - const layerEditor = selectedLayer ? : null - - const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? : null - - - const modals =
- - this.shortcutEl = el} - isOpen={this.state.isOpen.shortcuts} - onOpenToggle={this.toggleModal.bind(this, 'shortcuts')} - /> - - - - - -
+ ); - return + const layerList = ( + + ); + + const layerEditor = selectedLayer ? ( + + ) : null; + + const bottomPanel = + this.state.errors.length + this.state.infos.length > 0 ? ( + + ) : null; + + const modals = ( +
+ + (this.shortcutEl = el)} + isOpen={this.state.isOpen.shortcuts} + onOpenToggle={this.toggleModal.bind(this, "shortcuts")} + /> + + + + + +
+ ); + + return ( + + ); } } diff --git a/src/components/AppToolbar.jsx b/src/components/AppToolbar.jsx index 9c354570..6911889c 100644 --- a/src/components/AppToolbar.jsx +++ b/src/components/AppToolbar.jsx @@ -1,24 +1,33 @@ -import React from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import {detect} from 'detect-browser'; +import classnames from "classnames"; +import { detect } from "detect-browser"; +import PropTypes from "prop-types"; +import React from "react"; -import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md' -import pkgJson from '../../package.json' +import { + MdAssignmentTurnedIn, + MdFileDownload, + MdFindInPage, + MdHelpOutline, + MdLayers, + MdOpenInBrowser, + MdSettings, +} from "react-icons/md"; +import logoImage from "maputnik-design/logos/logo-color.svg"; +import pkgJson from "../../package.json"; // This is required because of , there isn't another way to detect support that I'm aware of. const browser = detect(); -const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser.name) > -1; - +const colorAccessibilityFiltersEnabled = + ["chrome", "firefox"].indexOf(browser.name) > -1; class IconText extends React.Component { static propTypes = { children: PropTypes.node, - } + }; render() { - return {this.props.children} + return {this.props.children}; } } @@ -28,17 +37,19 @@ class ToolbarLink extends React.Component { children: PropTypes.node, href: PropTypes.string, onToggleModal: PropTypes.func, - } + }; render() { - return - {this.props.children} - + return ( + + {this.props.children} + + ); } } @@ -47,36 +58,41 @@ class ToolbarLinkHighlighted extends React.Component { className: PropTypes.string, children: PropTypes.node, href: PropTypes.string, - onToggleModal: PropTypes.func - } + onToggleModal: PropTypes.func, + }; render() { - return - - {this.props.children} - - + return ( + + + {this.props.children} + + + ); } } class ToolbarSelect extends React.Component { static propTypes = { children: PropTypes.node, - wdKey: PropTypes.string - } + wdKey: PropTypes.string, + }; render() { - return
- {this.props.children} -
+ return ( +
+ {this.props.children} +
+ ); } } @@ -84,17 +100,19 @@ class ToolbarAction extends React.Component { static propTypes = { children: PropTypes.node, onClick: PropTypes.func, - wdKey: PropTypes.string - } + wdKey: PropTypes.string, + }; render() { - return + return ( + + ); } } @@ -112,7 +130,7 @@ export default class AppToolbar extends React.Component { onSetMapState: PropTypes.func, mapState: PropTypes.string, renderer: PropTypes.string, - } + }; state = { isOpen: { @@ -121,8 +139,8 @@ export default class AppToolbar extends React.Component { open: false, add: false, export: false, - } - } + }, + }; handleSelection(val) { this.props.onSetMapState(val); @@ -131,12 +149,11 @@ export default class AppToolbar extends React.Component { onSkip = (target) => { if (target === "map") { document.querySelector(".maplibregl-canvas").focus(); - } - else { - const el = document.querySelector("#skip-target-"+target); + } else { + const el = document.querySelector("#skip-target-" + target); el.focus(); } - } + }; render() { const views = [ @@ -149,7 +166,7 @@ export default class AppToolbar extends React.Component { id: "inspect", group: "general", title: "Inspect", - disabled: this.props.renderer === 'ol', + disabled: this.props.renderer === "ol", }, { id: "filter-deuteranopia", @@ -181,102 +198,137 @@ export default class AppToolbar extends React.Component { return view.id === this.props.mapState; }); - return + + ); } } diff --git a/src/components/LayerEditor.jsx b/src/components/LayerEditor.jsx index f4dbb8ec..5b0ce5b5 100644 --- a/src/components/LayerEditor.jsx +++ b/src/components/LayerEditor.jsx @@ -1,47 +1,46 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton' +import PropTypes from "prop-types"; +import React from "react"; +import { Button, Menu, MenuItem, Wrapper } from "react-aria-menubutton"; -import FieldJson from './FieldJson' -import FilterEditor from './FilterEditor' -import PropertyGroup from './PropertyGroup' -import LayerEditorGroup from './LayerEditorGroup' -import FieldType from './FieldType' -import FieldId from './FieldId' -import FieldMinZoom from './FieldMinZoom' -import FieldMaxZoom from './FieldMaxZoom' -import FieldComment from './FieldComment' -import FieldSource from './FieldSource' -import FieldSourceLayer from './FieldSourceLayer' -import {Accordion} from 'react-accessible-accordion'; +import { Accordion } from "react-accessible-accordion"; +import FieldComment from "./FieldComment"; +import FieldId from "./FieldId"; +import FieldJson from "./FieldJson"; +import FieldMaxZoom from "./FieldMaxZoom"; +import FieldMinZoom from "./FieldMinZoom"; +import FieldSource from "./FieldSource"; +import FieldSourceLayer from "./FieldSourceLayer"; +import FieldType from "./FieldType"; +import FilterEditor from "./FilterEditor"; +import LayerEditorGroup from "./LayerEditorGroup"; +import PropertyGroup from "./PropertyGroup"; -import {MdMoreVert} from 'react-icons/md' +import { MdMoreVert } from "react-icons/md"; -import { changeType, changeProperty } from '../libs/layer' -import layout from '../config/layout.json' -import {formatLayerId} from '../util/format'; +import layout from "../config/layout.json"; +import { changeProperty, changeType } from "../libs/layer"; +import { formatLayerId } from "../util/format"; - -function getLayoutForType (type) { +function getLayoutForType(type) { return layout[type] ? layout[type] : layout.invalid; } function layoutGroups(layerType) { const layerGroup = { - title: 'Layer', - type: 'layer' - } + title: "Layer", + type: "layer", + }; const filterGroup = { - title: 'Filter', - type: 'filter' - } + title: "Filter", + type: "filter", + }; const editorGroup = { - title: 'JSON Editor', - type: 'jsoneditor' - } + title: "JSON Editor", + type: "jsoneditor", + }; return [layerGroup, filterGroup] .concat(getLayoutForType(layerType).groups) - .concat([editorGroup]) + .concat([editorGroup]); } /** Layer editor supporting multiple types of layers. */ @@ -61,277 +60,320 @@ export default class LayerEditor extends React.Component { isLastLayer: PropTypes.bool, layerIndex: PropTypes.number, errors: PropTypes.array, - } + }; static defaultProps = { onLayerChanged: () => {}, onLayerIdChange: () => {}, onLayerDestroyed: () => {}, - } + }; static childContextTypes = { - reactIconBase: PropTypes.object - } + reactIconBase: PropTypes.object, + }; constructor(props) { - super(props) + super(props); //TODO: Clean this up and refactor into function - const editorGroups = {} - layoutGroups(this.props.layer.type).forEach(group => { - editorGroups[group.title] = true - }) + const editorGroups = {}; + layoutGroups(this.props.layer.type).forEach((group) => { + editorGroups[group.title] = true; + }); - this.state = { editorGroups } + this.state = { editorGroups }; } static getDerivedStateFromProps(props, state) { - const additionalGroups = { ...state.editorGroups } + const additionalGroups = { ...state.editorGroups }; - getLayoutForType(props.layer.type).groups.forEach(group => { - if(!(group.title in additionalGroups)) { - additionalGroups[group.title] = true + getLayoutForType(props.layer.type).groups.forEach((group) => { + if (!(group.title in additionalGroups)) { + additionalGroups[group.title] = true; } - }) + }); return { - editorGroups: additionalGroups + editorGroups: additionalGroups, }; } - getChildContext () { + getChildContext() { return { reactIconBase: { size: 14, - color: '#8e8e8e', - } - } + color: "#8e8e8e", + }, + }; } changeProperty(group, property, newValue) { this.props.onLayerChanged( this.props.layerIndex, changeProperty(this.props.layer, group, property, newValue) - ) + ); } onGroupToggle(groupTitle, active) { const changedActiveGroups = { ...this.state.editorGroups, [groupTitle]: active, - } + }; this.setState({ - editorGroups: changedActiveGroups - }) + editorGroups: changedActiveGroups, + }); } renderGroupType(type, fields) { - let comment = "" - if(this.props.layer.metadata) { - comment = this.props.layer.metadata['maputnik:comment'] + let comment = ""; + if (this.props.layer.metadata) { + comment = this.props.layer.metadata["maputnik:comment"]; } - const {errors, layerIndex} = this.props; + const { errors, layerIndex } = this.props; const errorData = {}; - errors.forEach(error => { + errors.forEach((error) => { if ( error.parsed && error.parsed.type === "layer" && error.parsed.data.index == layerIndex ) { errorData[error.parsed.data.key] = { - message: error.parsed.data.message + message: error.parsed.data.message, }; } - }) + }); let sourceLayerIds; - if(this.props.sources.hasOwnProperty(this.props.layer.source)) { + if (this.props.sources.hasOwnProperty(this.props.layer.source)) { sourceLayerIds = this.props.sources[this.props.layer.source].layers; } - switch(type) { - case 'layer': return
- this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)} - /> - this.props.onLayerChanged( - this.props.layerIndex, - changeType(this.props.layer, newType) - )} - /> - {this.props.layer.type !== 'background' && this.changeProperty(null, 'source', v)} - /> - } - {['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 && - this.changeProperty(null, 'source-layer', v)} - /> - } - this.changeProperty(null, 'minzoom', v)} - /> - this.changeProperty(null, 'maxzoom', v)} - /> - this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)} - /> -
- case 'filter': return
-
- + + this.props.onLayerIdChange( + this.props.layerIndex, + this.props.layer.id, + newId + ) + } + /> + + this.props.onLayerChanged( + this.props.layerIndex, + changeType(this.props.layer, newType) + ) + } + /> + {this.props.layer.type !== "background" && ( + this.changeProperty(null, "source", v)} + /> + )} + {["background", "raster", "hillshade", "heatmap"].indexOf( + this.props.layer.type + ) < 0 && ( + this.changeProperty(null, "source-layer", v)} + /> + )} + this.changeProperty(null, "minzoom", v)} + /> + this.changeProperty(null, "maxzoom", v)} + /> + + this.changeProperty( + "metadata", + "maputnik:comment", + v == "" ? undefined : v + ) + } + /> +
+ ); + case "filter": + return ( +
+
+ this.changeProperty(null, "filter", f)} + /> +
+
+ ); + case "properties": + return ( + this.changeProperty(null, 'filter', f)} + layer={this.props.layer} + groupFields={fields} + spec={this.props.spec} + onChange={this.changeProperty.bind(this)} /> -
- - case 'properties': - return - case 'jsoneditor': - return { - this.props.onLayerChanged( - this.props.layerIndex, - layer - ); - }} - /> + ); + case "jsoneditor": + return ( + { + this.props.onLayerChanged(this.props.layerIndex, layer); + }} + /> + ); } } moveLayer(offset) { this.props.onMoveLayer({ oldIndex: this.props.layerIndex, - newIndex: this.props.layerIndex+offset - }) + newIndex: this.props.layerIndex + offset, + }); } render() { const groupIds = []; - const layerType = this.props.layer.type - const groups = layoutGroups(layerType).filter(group => { - return !(layerType === 'background' && group.type === 'source') - }).map(group => { - const groupId = group.title.replace(/ /g, "_"); - groupIds.push(groupId); - return - {this.renderGroupType(group.type, group.fields)} - - }) + const layerType = this.props.layer.type; + const groups = layoutGroups(layerType) + .filter((group) => { + return !(layerType === "background" && group.type === "source"); + }) + .map((group) => { + const groupId = group.title.replace(/ /g, "_"); + groupIds.push(groupId); + return ( + + {this.renderGroupType(group.type, group.fields)} + + ); + }); - const layout = this.props.layer.layout || {} + const layout = this.props.layer.layout || {}; const items = { delete: { text: "Delete", - handler: () => this.props.onLayerDestroy(this.props.layerIndex) + handler: () => this.props.onLayerDestroy(this.props.layerIndex), }, duplicate: { text: "Duplicate", - handler: () => this.props.onLayerCopy(this.props.layerIndex) + handler: () => this.props.onLayerCopy(this.props.layerIndex), }, hide: { - text: (layout.visibility === "none") ? "Show" : "Hide", - handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex) + text: layout.visibility === "none" ? "Show" : "Hide", + handler: () => + this.props.onLayerVisibilityToggle(this.props.layerIndex), }, moveLayerUp: { text: "Move layer up", // Not actually used... disabled: this.props.isFirstLayer, - handler: () => this.moveLayer(-1) + handler: () => this.moveLayer(-1), }, moveLayerDown: { text: "Move layer down", // Not actually used... disabled: this.props.isLastLayer, - handler: () => this.moveLayer(+1) - } - } + handler: () => this.moveLayer(+1), + }, + }; function handleSelection(id, event) { event.stopPropagation; items[id].handler(); } - return
-
-
-

- Layer: {formatLayerId(this.props.layer.id)} -

-
- - - -
    - {Object.keys(items).map((id, idx) => { - const item = items[id]; - return
  • - - {item.text} - -
  • - })} -
-
-
-
-
- -
- - {groups} - -
+
+
+

+ Layer: {formatLayerId(this.props.layer.id)} +

+
+ + + +
    + {Object.keys(items).map((id, idx) => { + const item = items[id]; + return ( +
  • + + {item.text} + +
  • + ); + })} +
+
+
+
+
+
+ + {groups} + + + ); } } diff --git a/src/components/LayerList.jsx b/src/components/LayerList.jsx index 82a96c7d..14d9b971 100644 --- a/src/components/LayerList.jsx +++ b/src/components/LayerList.jsx @@ -1,13 +1,13 @@ -import React from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import lodash from 'lodash'; +import classnames from "classnames"; +import lodash from "lodash"; +import PropTypes from "prop-types"; +import React from "react"; -import LayerListGroup from './LayerListGroup' -import LayerListItem from './LayerListItem' -import ModalAdd from './ModalAdd' +import LayerListGroup from "./LayerListGroup"; +import LayerListItem from "./LayerListItem"; +import ModalAdd from "./ModalAdd"; -import {SortableContainer} from 'react-sortable-hoc'; +import { SortableContainer } from "react-sortable-hoc"; const layerListPropTypes = { layers: PropTypes.array.isRequired, @@ -15,34 +15,34 @@ const layerListPropTypes = { onLayersChange: PropTypes.func.isRequired, onLayerSelect: PropTypes.func, sources: PropTypes.object.isRequired, -} +}; function layerPrefix(name) { - return name.replace(' ', '-').replace('_', '-').split('-')[0] + return name.replace(" ", "-").replace("_", "-").split("-")[0]; } function findClosestCommonPrefix(layers, idx) { - const currentLayerPrefix = layerPrefix(layers[idx].id) - let closestIdx = idx + const currentLayerPrefix = layerPrefix(layers[idx].id); + let closestIdx = idx; for (let i = idx; i > 0; i--) { - const previousLayerPrefix = layerPrefix(layers[i-1].id) - if(previousLayerPrefix === currentLayerPrefix) { - closestIdx = i - 1 + const previousLayerPrefix = layerPrefix(layers[i - 1].id); + if (previousLayerPrefix === currentLayerPrefix) { + closestIdx = i - 1; } else { - return closestIdx + return closestIdx; } } - return closestIdx + return closestIdx; } let UID = 0; // List of collapsible layer editors class LayerListContainer extends React.Component { - static propTypes = {...layerListPropTypes} + static propTypes = { ...layerListPropTypes }; static defaultProps = { onLayerSelect: () => {}, - } + }; constructor(props) { super(props); @@ -56,8 +56,8 @@ class LayerListContainer extends React.Component { }, isOpen: { add: false, - } - } + }, + }; } toggleModal(modalName) { @@ -68,79 +68,82 @@ class LayerListContainer extends React.Component { }, isOpen: { ...this.state.isOpen, - [modalName]: !this.state.isOpen[modalName] - } - }) + [modalName]: !this.state.isOpen[modalName], + }, + }); } toggleLayers = () => { - let idx=0 + let idx = 0; - let newGroups=[] - - this.groupedLayers().forEach(layers => { - const groupPrefix = layerPrefix(layers[0].id) - const lookupKey = [groupPrefix, idx].join('-') + let newGroups = []; + this.groupedLayers().forEach((layers) => { + const groupPrefix = layerPrefix(layers[0].id); + const lookupKey = [groupPrefix, idx].join("-"); if (layers.length > 1) { - newGroups[lookupKey] = this.state.areAllGroupsExpanded + newGroups[lookupKey] = this.state.areAllGroupsExpanded; } layers.forEach((layer) => { - idx += 1 - }) + idx += 1; + }); }); this.setState({ collapsedGroups: newGroups, - areAllGroupsExpanded: !this.state.areAllGroupsExpanded - }) - } + areAllGroupsExpanded: !this.state.areAllGroupsExpanded, + }); + }; groupedLayers() { - const groups = [] + const groups = []; const layerIdCount = new Map(); for (let i = 0; i < this.props.layers.length; i++) { const origLayer = this.props.layers[i]; - const previousLayer = this.props.layers[i-1] - layerIdCount.set(origLayer.id, + const previousLayer = this.props.layers[i - 1]; + layerIdCount.set( + origLayer.id, layerIdCount.has(origLayer.id) ? layerIdCount.get(origLayer.id) + 1 : 0 ); const layer = { ...origLayer, key: `layers-list-${origLayer.id}-${layerIdCount.get(origLayer.id)}`, - } - if(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) { - const lastGroup = groups[groups.length - 1] - lastGroup.push(layer) + }; + if ( + previousLayer && + layerPrefix(previousLayer.id) == layerPrefix(layer.id) + ) { + const lastGroup = groups[groups.length - 1]; + lastGroup.push(layer); } else { - groups.push([layer]) + groups.push([layer]); } } - return groups + return groups; } toggleLayerGroup(groupPrefix, idx) { - const lookupKey = [groupPrefix, idx].join('-') - const newGroups = { ...this.state.collapsedGroups } - if(lookupKey in this.state.collapsedGroups) { - newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey] + const lookupKey = [groupPrefix, idx].join("-"); + const newGroups = { ...this.state.collapsedGroups }; + if (lookupKey in this.state.collapsedGroups) { + newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey]; } else { - newGroups[lookupKey] = false + newGroups[lookupKey] = false; } this.setState({ - collapsedGroups: newGroups - }) + collapsedGroups: newGroups, + }); } isCollapsed(groupPrefix, idx) { - const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')] - return collapsed === undefined ? true : collapsed + const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join("-")]; + return collapsed === undefined ? true : collapsed; } - shouldComponentUpdate (nextProps, nextState) { + shouldComponentUpdate(nextProps, nextState) { // Always update on state change if (this.state !== nextState) { return true; @@ -148,28 +151,28 @@ class LayerListContainer extends React.Component { // This component tree only requires id and visibility from the layers // objects - function getRequiredProps (layer) { + function getRequiredProps(layer) { const out = { id: layer.id, }; if (layer.layout) { out.layout = { - visibility: layer.layout.visibility + visibility: layer.layout.visibility, }; } return out; } const layersEqual = lodash.isEqual( nextProps.layers.map(getRequiredProps), - this.props.layers.map(getRequiredProps), + this.props.layers.map(getRequiredProps) ); - function withoutLayers (props) { + function withoutLayers(props) { const out = { - ...props + ...props, }; - delete out['layers']; + delete out["layers"]; return out; } @@ -184,16 +187,16 @@ class LayerListContainer extends React.Component { return propsChanged; } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) { const selectedItemNode = this.selectedItemRef.current; if (selectedItemNode && selectedItemNode.node) { const target = selectedItemNode.node; const options = { root: this.scrollContainerRef.current, - threshold: 1.0 - } - const observer = new IntersectionObserver(entries => { + threshold: 1.0, + }; + const observer = new IntersectionObserver((entries) => { observer.unobserve(target); if (entries.length > 0 && entries[0].intersectionRatio < 1) { target.scrollIntoView(); @@ -206,28 +209,32 @@ class LayerListContainer extends React.Component { } render() { - - const listItems = [] - let idx = 0 + const listItems = []; + let idx = 0; const layersByGroup = this.groupedLayers(); - layersByGroup.forEach(layers => { - const groupPrefix = layerPrefix(layers[0].id) - if(layers.length > 1) { - const grp = l.key).join(" ")} - key={`group-${groupPrefix}-${idx}`} - title={groupPrefix} - isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex} - onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)} - /> - listItems.push(grp) + layersByGroup.forEach((layers) => { + const groupPrefix = layerPrefix(layers[0].id); + if (layers.length > 1) { + const grp = ( + l.key).join(" ")} + key={`group-${groupPrefix}-${idx}`} + title={groupPrefix} + isActive={ + !this.isCollapsed(groupPrefix, idx) || + idx === this.props.selectedLayerIndex + } + onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)} + /> + ); + listItems.push(grp); } layers.forEach((layer, idxInGroup) => { - const groupIdx = findClosestCommonPrefix(this.props.layers, idx) + const groupIdx = findClosestCommonPrefix(this.props.layers, idx); - const layerError = this.props.errors.find(error => { + const layerError = this.props.errors.find((error) => { return ( error.parsed && error.parsed.type === "layer" && @@ -240,93 +247,107 @@ class LayerListContainer extends React.Component { additionalProps.ref = this.selectedItemRef; } - const listItem = 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex, - 'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1, - 'maputnik-layer-list-item--error': !!layerError - })} - index={idx} - key={layer.key} - id={layer.key} - layerId={layer.id} - layerIndex={idx} - layerType={layer.type} - visibility={(layer.layout || {}).visibility} - isSelected={idx === this.props.selectedLayerIndex} - onLayerSelect={this.props.onLayerSelect} - onLayerDestroy={this.props.onLayerDestroy.bind(this)} - onLayerCopy={this.props.onLayerCopy.bind(this)} - onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)} - {...additionalProps} - /> - listItems.push(listItem) - idx += 1 - }) - }) + const listItem = ( + 1 && + this.isCollapsed(groupPrefix, groupIdx) && + idx !== this.props.selectedLayerIndex, + "maputnik-layer-list-item-group-last": + idxInGroup == layers.length - 1 && layers.length > 1, + "maputnik-layer-list-item--error": !!layerError, + })} + index={idx} + key={layer.key} + id={layer.key} + layerId={layer.id} + layerIndex={idx} + layerType={layer.type} + visibility={(layer.layout || {}).visibility} + isSelected={idx === this.props.selectedLayerIndex} + onLayerSelect={this.props.onLayerSelect} + onLayerDestroy={this.props.onLayerDestroy.bind(this)} + onLayerCopy={this.props.onLayerCopy.bind(this)} + onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind( + this + )} + {...additionalProps} + /> + ); + listItems.push(listItem); + idx += 1; + }); + }); - return
- + -
- Layers - -
-
- + /> +
+ Layers + +
+
+ +
-
-
-
- +
+
+ +
+
+
+
    {listItems}
- -
-
    - {listItems} -
-
-
+ + ); } } -const LayerListContainerSortable = SortableContainer((props) => ) +const LayerListContainerSortable = SortableContainer((props) => ( + +)); export default class LayerList extends React.Component { - static propTypes = {...layerListPropTypes} + static propTypes = { ...layerListPropTypes }; render() { - return false} - /> + return ( + false} + /> + ); } } diff --git a/src/components/MapMaplibreGl.jsx b/src/components/MapMaplibreGl.jsx index ceba1abd..e4b29dac 100644 --- a/src/components/MapMaplibreGl.jsx +++ b/src/components/MapMaplibreGl.jsx @@ -1,19 +1,17 @@ -import React from 'react' -import PropTypes from 'prop-types' -import ReactDOM from 'react-dom' -import MapLibreGl from 'maplibre-gl' -import MapboxInspect from 'mapbox-gl-inspect' -import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup' -import MapMaplibreGlFeaturePropertyPopup from './MapMaplibreGlFeaturePropertyPopup' -import tokens from '../config/tokens.json' -import colors from 'mapbox-gl-inspect/lib/colors' -import Color from 'color' -import ZoomControl from '../libs/zoomcontrol' -import { colorHighlightedLayer } from '../libs/highlight' -import 'maplibre-gl/dist/maplibre-gl.css' -import '../maplibregl.css' -import '../libs/maplibre-rtl' - +import Color from "color"; +import MapboxInspect from "mapbox-gl-inspect"; +import colors from "mapbox-gl-inspect/lib/colors"; +import MapLibreGl from "maplibre-gl"; +import "maplibre-gl/dist/maplibre-gl.css"; +import PropTypes from "prop-types"; +import React from "react"; +import ReactDOM from "react-dom"; +import { colorHighlightedLayer } from "../libs/highlight"; +import "../libs/maplibre-rtl"; +import ZoomControl from "../libs/zoomcontrol"; +import "../maplibregl.css"; +import MapMaplibreGlFeaturePropertyPopup from "./MapMaplibreGlFeaturePropertyPopup"; +import MapMaplibreGlLayerPopup from "./MapMaplibreGlLayerPopup"; const IS_SUPPORTED = MapLibreGl.supported(); @@ -24,32 +22,32 @@ function renderPopup(popup, mountNode) { function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) { const backgroundLayer = { - "id": "background", - "type": "background", - "paint": { - "background-color": '#1c1f24', - } + id: "background", + type: "background", + paint: { + "background-color": "#1c1f24", + }, + }; + + const layer = colorHighlightedLayer(highlightedLayer); + if (layer) { + coloredLayers.push(layer); } - const layer = colorHighlightedLayer(highlightedLayer) - if(layer) { - coloredLayers.push(layer) - } - - const sources = {} - Object.keys(originalMapStyle.sources).forEach(sourceId => { - const source = originalMapStyle.sources[sourceId] - if(source.type !== 'raster' && source.type !== 'raster-dem') { - sources[sourceId] = source + const sources = {}; + Object.keys(originalMapStyle.sources).forEach((sourceId) => { + const source = originalMapStyle.sources[sourceId]; + if (source.type !== "raster" && source.type !== "raster-dem") { + sources[sourceId] = source; } - }) + }); const inspectStyle = { ...originalMapStyle, sources: sources, - layers: [backgroundLayer].concat(coloredLayers) - } - return inspectStyle + layers: [backgroundLayer].concat(coloredLayers), + }; + return inspectStyle; } export default class MapMaplibreGl extends React.Component { @@ -62,7 +60,7 @@ export default class MapMaplibreGl extends React.Component { options: PropTypes.object, replaceAccessTokens: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, - } + }; static defaultProps = { onMapLoaded: () => {}, @@ -70,51 +68,55 @@ export default class MapMaplibreGl extends React.Component { onLayerSelect: () => {}, onChange: () => {}, options: {}, - } + }; constructor(props) { - super(props) + super(props); this.state = { map: null, inspect: null, - } + }; } updateMapFromProps(props) { - if(!IS_SUPPORTED) return; + if (!IS_SUPPORTED) return; - if(!this.state.map) return + if (!this.state.map) return; //Maplibre GL now does diffing natively so we don't need to calculate //the necessary operations ourselves! - this.state.map.setStyle( - this.props.replaceAccessTokens(props.mapStyle), - {diff: true} - ) + this.state.map.setStyle(this.props.replaceAccessTokens(props.mapStyle), { + diff: true, + }); } shouldComponentUpdate(nextProps, nextState) { let should = false; try { - should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState); - } catch(e) { + should = + JSON.stringify(this.props) !== JSON.stringify(nextProps) || + JSON.stringify(this.state) !== JSON.stringify(nextState); + } catch (e) { // no biggie, carry on } return should; } componentDidUpdate(prevProps, prevState, snapshot) { - if(!IS_SUPPORTED) return; + if (!IS_SUPPORTED) return; const map = this.state.map; this.updateMapFromProps(this.props); - if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) { + if ( + this.state.inspect && + this.props.inspectModeEnabled !== this.state.inspect._showInspectMap + ) { // HACK: Fix for , while we wait for a proper fix. // eslint-disable-next-line this.state.inspect._popupBlocked = false; - this.state.inspect.toggleInspector() + this.state.inspect.toggleInspector(); } if (map) { if (this.props.inspectModeEnabled) { @@ -123,7 +125,7 @@ export default class MapMaplibreGl extends React.Component { // mapbox-gl-inspect. try { this.state.inspect.render(); - } catch(err) { + } catch (err) { console.error("FIXME: Caught error", err); } } @@ -135,40 +137,40 @@ export default class MapMaplibreGl extends React.Component { } componentDidMount() { - if(!IS_SUPPORTED) return; + if (!IS_SUPPORTED) return; const mapOpts = { ...this.props.options, container: this.container, style: this.props.mapStyle, hash: true, - maxZoom: 24 - } + maxZoom: 24, + }; const map = new MapLibreGl.Map(mapOpts); const mapViewChange = () => { const center = map.getCenter(); const zoom = map.getZoom(); - this.props.onChange({center, zoom}); - } + this.props.onChange({ center, zoom }); + }; mapViewChange(); map.showTileBoundaries = mapOpts.showTileBoundaries; map.showCollisionBoxes = mapOpts.showCollisionBoxes; map.showOverdrawInspector = mapOpts.showOverdrawInspector; - const zoomControl = new ZoomControl; - map.addControl(zoomControl, 'top-right'); + const zoomControl = new ZoomControl(); + map.addControl(zoomControl, "top-right"); - const nav = new MapLibreGl.NavigationControl({visualizePitch:true}); - map.addControl(nav, 'top-right'); + const nav = new MapLibreGl.NavigationControl({ visualizePitch: true }); + map.addControl(nav, "top-right"); - const tmpNode = document.createElement('div'); + const tmpNode = document.createElement("div"); const inspect = new MapboxInspect({ popup: new MapLibreGl.Popup({ - closeOnClick: false + closeOnClick: false, }), showMapPopup: true, showMapPopupOnHover: false, @@ -176,41 +178,58 @@ export default class MapMaplibreGl extends React.Component { showInspectButton: false, blockHoverPopupOnClick: true, assignLayerColor: (layerId, alpha) => { - return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string() + return Color(colors.brightColor(layerId, alpha)) + .desaturate(0.5) + .string(); }, - buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer), - renderPopup: features => { - if(this.props.inspectModeEnabled) { - return renderPopup(, tmpNode); + buildInspectStyle: (originalMapStyle, coloredLayers) => + buildInspectStyle( + originalMapStyle, + coloredLayers, + this.props.highlightedLayer + ), + renderPopup: (features) => { + if (this.props.inspectModeEnabled) { + return renderPopup( + , + tmpNode + ); } else { - return renderPopup(, tmpNode); + return renderPopup( + , + tmpNode + ); } - } - }) - map.addControl(inspect) + }, + }); + map.addControl(inspect); map.on("style.load", () => { this.setState({ map, inspect, - zoom: map.getZoom() + zoom: map.getZoom(), }); - }) + }); - map.on("data", e => { - if(e.dataType !== 'tile') return + map.on("data", (e) => { + if (e.dataType !== "tile") return; this.props.onDataChange({ - map: this.state.map - }) - }) + map: this.state.map, + }); + }); - map.on("error", e => { + map.on("error", (e) => { console.log("ERROR", e); - }) + }); - map.on("zoom", e => { + map.on("zoom", (e) => { this.setState({ - zoom: map.getZoom() + zoom: map.getZoom(), }); }); @@ -219,28 +238,32 @@ export default class MapMaplibreGl extends React.Component { } onLayerSelectById = (id) => { - const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id); + const index = this.props.mapStyle.layers.findIndex( + (layer) => layer.id === id + ); this.props.onLayerSelect(index); - } + }; render() { - if(IS_SUPPORTED) { - return
this.container = x} - >
- } - else { - return
-
- Error: Cannot load MaplibreGL, WebGL is either unsupported or disabled + if (IS_SUPPORTED) { + return ( +
(this.container = x)} + data-wd-key="maplibre:map" + >
+ ); + } else { + return ( +
+
+ Error: Cannot load MaplibreGL, WebGL is either unsupported or + disabled +
-
+ ); } } } - diff --git a/src/libs/zoomcontrol.js b/src/libs/zoomcontrol.js index db69dbf1..ed675338 100644 --- a/src/libs/zoomcontrol.js +++ b/src/libs/zoomcontrol.js @@ -1,26 +1,28 @@ export default class ZoomControl { onAdd(map) { this._map = map; - this._container = document.createElement('div'); - this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-zoom'; + this._container = document.createElement("div"); + this._container.className = + "maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-zoom"; + this._container.setAttribute("data-wd-key", "maplibre:ctrl-zoom"); this._container.innerHTML = ` Zoom: `; this._textEl = this._container.querySelector("span"); - + this.addEventListeners(); - + return this._container; } - + updateZoomLevel() { this._textEl.innerHTML = this._map.getZoom().toFixed(2); } - - addEventListeners (){ - this._map.on('render', this.updateZoomLevel.bind(this) ); - this._map.on('zoomIn', this.updateZoomLevel.bind(this) ); - this._map.on('zoomOut', this.updateZoomLevel.bind(this) ); + + addEventListeners() { + this._map.on("render", this.updateZoomLevel.bind(this)); + this._map.on("zoomIn", this.updateZoomLevel.bind(this)); + this._map.on("zoomOut", this.updateZoomLevel.bind(this)); } onRemove() {