mirror of
https://github.com/maputnik/editor.git
synced 2026-08-24 22:17:25 +00:00
Compare commits
66 Commits
1bf365df02
...
v3.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fc334ad85 | |||
| 42a040e91a | |||
| f5757dd8a2 | |||
| 3b79303f83 | |||
| 55a487d0c8 | |||
| 03f3c5c032 | |||
| 02efeec96d | |||
| 4896c40554 | |||
| 40f1837835 | |||
| 3f94ab2f3b | |||
| 0cd4ab6e0a | |||
| fce182050d | |||
| abe6230932 | |||
| 6f4c34b29a | |||
| 3f011a21dc | |||
| 54c1b761fd | |||
| 7289e26563 | |||
| 4d5c74f4ee | |||
| 285fa6e509 | |||
| 5d4b9e9654 | |||
| bd576e7bae | |||
| 8d5d8e1ee0 | |||
| b503a44aa4 | |||
| 1607c985d9 | |||
| bdcc5ddc31 | |||
| 2545c55f1a | |||
| b238e07b0b | |||
| fc500449d7 | |||
| 0af01c7c39 | |||
| 65f4ced302 | |||
| c78025c9e4 | |||
| 1fbeb66f55 | |||
| 3ad55d7f97 | |||
| ba35f260a5 | |||
| d178e2f46b | |||
| 60bcfcf208 | |||
| 7ceb2ccabd | |||
| d5140d77ba | |||
| 70571dc1cc | |||
| 62f1a0622a | |||
| 9b9f0b4c78 | |||
| e2e29d7f5e | |||
| 728de3aed6 | |||
| f1d98099df | |||
| 9e7cec932b | |||
| 701e1f4c79 | |||
| 9f682a4213 | |||
| 530907610e | |||
| 719c8e5a22 | |||
| 104ce8221c | |||
| 81c157e3e3 | |||
| 0b466fd3b8 | |||
| 0eeba50de8 | |||
| 23b24376ee | |||
| d852443a03 | |||
| 4f3a8c8ecf | |||
| 6742c0b3a9 | |||
| cf9d9cb2ab | |||
| 727bc7dfae | |||
| 0bbb04e4ff | |||
| 4ea1958275 | |||
| db93ce1be1 | |||
| 2d242c2a47 | |||
| 1805763ba5 | |||
| 499d183dcc | |||
| 60c57135c4 |
+29
-23
@@ -7,17 +7,7 @@ on:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
name: build docker
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker build -t test-docker-image-build .
|
||||
|
||||
# build the editor
|
||||
build-node:
|
||||
name: "build on ${{ matrix.os }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -30,8 +20,8 @@ jobs:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
@@ -47,8 +37,8 @@ jobs:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
@@ -61,7 +51,7 @@ jobs:
|
||||
|
||||
# Build and upload desktop CLI artifacts
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
@@ -89,23 +79,39 @@ jobs:
|
||||
path: ./desktop/bin/windows/
|
||||
|
||||
e2e-tests:
|
||||
name: "E2E tests using ${{ matrix.browser }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: [ chrome ]
|
||||
name: "E2E tests using chrome"
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- run: npm ci
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
build: npm run build
|
||||
start: npm run start
|
||||
browser: ${{ matrix.browser }}
|
||||
browser: chrome
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
||||
verbose: true
|
||||
|
||||
e2e-tests-docker:
|
||||
name: "E2E tests using chrome and docker"
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- run: npm ci
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
build: docker build -t maputnik .
|
||||
start: docker run --rm --network host maputnik --port=8888
|
||||
browser: chrome
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -46,6 +46,6 @@ jobs:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
@@ -39,19 +39,19 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Set up Go for desktop build
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
|
||||
@@ -6,7 +6,7 @@ ci:
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-executables-have-shebangs
|
||||
|
||||
+15
-2
@@ -1,5 +1,14 @@
|
||||
## main
|
||||
|
||||
### ✨ Features and improvements
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
|
||||
- _...Add new stuff here..._
|
||||
|
||||
## 3.0.0
|
||||
|
||||
### ✨ Features and improvements
|
||||
- Fix radio/delete filter buttons styling regression
|
||||
- Add german translation
|
||||
@@ -16,13 +25,17 @@
|
||||
- Remove usage of legacy `childContextTypes` API
|
||||
- Refactor Field components to use arrow function syntax
|
||||
- Replace react-autocomplete with Downshift in the autocomplete component
|
||||
- _...Add new stuff here..._
|
||||
- Add LocationIQ as supported map provider with access token field and gallery style
|
||||
- Use maputnik go binary for the docker image to allow file watching
|
||||
- Revmove support for `debug` and `localport` url parameters
|
||||
- Replace react-sortable-hoc with dnd-kit to avoid react console warnings and also use a maintained library
|
||||
|
||||
### 🐞 Bug fixes
|
||||
|
||||
- Fix incorrect handing of network error response (#944)
|
||||
- Show an error when adding a layer with a duplicate ID
|
||||
- _...Add new stuff here..._
|
||||
- Replace deprecated `ReactDOM.render` usage with `createRoot` and drop the
|
||||
`DOMNodeRemoved` cleanup hack
|
||||
|
||||
## 2.1.1
|
||||
|
||||
|
||||
+8
-10
@@ -1,16 +1,14 @@
|
||||
FROM node:18 as builder
|
||||
FROM golang:1.23-alpine AS builder
|
||||
WORKDIR /maputnik
|
||||
|
||||
# Only copy package.json to prevent npm install from running on every build
|
||||
COPY package.json package-lock.json .npmrc ./
|
||||
RUN npm ci
|
||||
RUN apk add --no-cache nodejs npm make git gcc g++ libc-dev
|
||||
|
||||
# Build maputnik
|
||||
COPY . .
|
||||
RUN npx vite build
|
||||
RUN npm ci
|
||||
RUN CGO_ENABLED=1 GOOS=linux npm run build-linux
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Create a clean nginx-alpine slim image with just the build results
|
||||
FROM nginx:alpine-slim
|
||||
|
||||
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
COPY --from=builder /maputnik/desktop/bin/linux ./
|
||||
ENTRYPOINT ["/app/maputnik"]
|
||||
|
||||
@@ -18,9 +18,15 @@ targeted at developers and map designers.
|
||||
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8888:80 ghcr.io/maplibre/maputnik:main
|
||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main
|
||||
```
|
||||
|
||||
To see the CLI options (for example file watching or style serving) run:
|
||||
```bash
|
||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main --help
|
||||
```
|
||||
You might need to mount a volume (`-v`) to be able to use these options.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
||||
|
||||
@@ -16,7 +16,6 @@ describe("history", () => {
|
||||
it("undo/redo", () => {
|
||||
when.setStyle("geojson");
|
||||
when.modal.open();
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||
|
||||
when.modal.fillLayers({
|
||||
id: "step 1",
|
||||
|
||||
@@ -378,8 +378,57 @@ describe("layers", () => {
|
||||
});
|
||||
|
||||
it("groups", () => {
|
||||
// TODO
|
||||
// Click each of the layer groups.
|
||||
when.modal.open();
|
||||
const id1 = when.modal.fillLayers({
|
||||
id: "aa",
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
const id2 = when.modal.fillLayers({
|
||||
id: "aa-2",
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
const id3 = when.modal.fillLayers({
|
||||
id: "b",
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
||||
when.click("layer-list-group:aa-0");
|
||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
||||
when.click("layer-list-item:" + id2);
|
||||
when.click("skip-target-layer-editor");
|
||||
when.click("menu-move-layer-down");
|
||||
then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "aa",
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
{
|
||||
id: "aa-2",
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -495,9 +544,7 @@ describe("layers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("layereditor jsonlint should error", ()=>{
|
||||
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "circle",
|
||||
@@ -523,4 +570,42 @@ describe("layers", () => {
|
||||
error.should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe("drag and drop", () => {
|
||||
it("move layer should update local storage", () => {
|
||||
when.modal.open();
|
||||
const firstId = when.modal.fillLayers({
|
||||
id: "a",
|
||||
type: "background",
|
||||
});
|
||||
when.modal.open();
|
||||
const secondId = when.modal.fillLayers({
|
||||
id: "b",
|
||||
type: "background",
|
||||
});
|
||||
when.modal.open();
|
||||
const thirdId = when.modal.fillLayers({
|
||||
id: "c",
|
||||
type: "background",
|
||||
});
|
||||
when.dragAndDrop(get.elementByTestId("layer-list-item:" + firstId), get.elementByTestId("layer-list-item:" + thirdId));
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: secondId,
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: thirdId,
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: firstId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,8 +94,8 @@ export class MaputnikDriver {
|
||||
public when = {
|
||||
...this.helper.when,
|
||||
modal: this.modalDriver.when,
|
||||
within: (selector: string, fn: () => void) => {
|
||||
this.helper.when.within(fn, selector);
|
||||
doWithin: (selector: string, fn: () => void) => {
|
||||
this.helper.when.doWithin(fn, selector);
|
||||
},
|
||||
tab: () => this.helper.get.element("body").tab(),
|
||||
waitForExampleFileResponse: () => {
|
||||
@@ -110,25 +110,25 @@ export class MaputnikDriver {
|
||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
||||
zoom?: number
|
||||
) => {
|
||||
let url = "?debug";
|
||||
const url = new URL(baseUrl);
|
||||
switch (styleProperties) {
|
||||
case "geojson":
|
||||
url += `&style=${baseUrl}geojson-style.json`;
|
||||
url.searchParams.set("style", baseUrl + "geojson-style.json");
|
||||
break;
|
||||
case "raster":
|
||||
url += `&style=${baseUrl}raster-style.json`;
|
||||
url.searchParams.set("style", baseUrl + "raster-style.json");
|
||||
break;
|
||||
case "both":
|
||||
url += `&style=${baseUrl}geojson-raster-style.json`;
|
||||
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
|
||||
break;
|
||||
case "layer":
|
||||
url += `&style=${baseUrl}/example-layer-style.json`;
|
||||
url.searchParams.set("style", baseUrl + "example-layer-style.json");
|
||||
break;
|
||||
}
|
||||
if (zoom) {
|
||||
url += `#${zoom}/41.3805/2.1635`;
|
||||
url.hash = `${zoom}/41.3805/2.1635`;
|
||||
}
|
||||
this.helper.when.visit(baseUrl + url);
|
||||
this.helper.when.visit(url.toString());
|
||||
if (styleProperties) {
|
||||
this.helper.when.acceptConfirm();
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export class MaputnikDriver {
|
||||
},
|
||||
|
||||
selectWithin: (selector: string, value: string) => {
|
||||
this.when.within(selector, () => {
|
||||
this.when.doWithin(selector, () => {
|
||||
this.helper.get.element("select").select(value);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -20,8 +20,8 @@ export default class ModalDriver {
|
||||
this.helper.when.type("add-layer.layer-id.input", id);
|
||||
|
||||
if (layer) {
|
||||
this.helper.when.within(() => {
|
||||
this.helper.get.element("input").type(layer!);
|
||||
this.helper.when.doWithin(() => {
|
||||
this.helper.get.element("input").clear().type(layer!);
|
||||
}, "add-layer.layer-source-block");
|
||||
}
|
||||
this.helper.when.click("add-layer");
|
||||
|
||||
@@ -224,6 +224,18 @@ describe("modals", () => {
|
||||
).shouldInclude({ "maputnik:stadia_access_token": apiKey });
|
||||
});
|
||||
|
||||
it("locationiq access token", () => {
|
||||
const apiKey = "testing123";
|
||||
when.setValue(
|
||||
"modal:settings.maputnik:locationiq_access_token",
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
).shouldInclude({ "maputnik:locationiq_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
|
||||
when.select("modal:settings.maputnik:renderer", "ol");
|
||||
|
||||
+5
-2
@@ -2,8 +2,7 @@ SOURCEDIR=.
|
||||
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
|
||||
BINARY=maputnik
|
||||
VERSION := $(shell node -p "require('../package.json').version")
|
||||
GOPATH := $(if $(GOPATH),$(GOPATH),$(HOME)/go)
|
||||
GOBIN := $(if $(GOBIN),$(GOBIN),$(HOME)/go/bin)
|
||||
GOBIN := $(or $(shell if [ -d /go/bin ]; then echo "/go/bin"; fi),$(HOME)/go/bin)
|
||||
|
||||
all: $(BINARY)
|
||||
|
||||
@@ -11,6 +10,10 @@ $(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go win
|
||||
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
||||
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
|
||||
|
||||
bin/linux/$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
|
||||
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
||||
$(GOBIN)/gox -osarch "linux/amd64" -output "bin/{{.OS}}/${BINARY}"
|
||||
|
||||
winres/winres.json: winres/winres_template.json
|
||||
sed 's/{{.Version}}/$(VERSION)/g' winres/winres_template.json > $@
|
||||
|
||||
|
||||
+1
-2
@@ -5,8 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/GeertJohan/go.rice"
|
||||
"github.com/GeertJohan/go.rice"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/maputnik/desktop/filewatch"
|
||||
|
||||
Generated
+558
-597
File diff suppressed because it is too large
Load Diff
+27
-24
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "2.1.1",
|
||||
"version": "3.0.0",
|
||||
"description": "A MapLibre GL visual style editor",
|
||||
"type": "module",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build --base=/maputnik/",
|
||||
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
|
||||
"build": "tsc && vite build --mode=production",
|
||||
"build-desktop": "tsc && vite build --mode=desktop && cd desktop && make",
|
||||
"build-linux": "tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
|
||||
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
|
||||
"lint": "eslint",
|
||||
"test": "cypress run",
|
||||
@@ -23,22 +24,26 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
||||
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
||||
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
||||
"@maplibre/maplibre-gl-style-spec": "^23.2.3",
|
||||
"@maplibre/maplibre-gl-style-spec": "^23.3.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@prantlf/jsonlint": "^16.0.0",
|
||||
"array-move": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^5.65.19",
|
||||
"codemirror": "^5.65.20",
|
||||
"color": "^5.0.0",
|
||||
"detect-browser": "^5.3.0",
|
||||
"downshift": "^9.0.10",
|
||||
"events": "^3.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.3.2",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
@@ -50,25 +55,23 @@
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"maplibre-gl": "^5.5.0",
|
||||
"maplibre-gl": "^5.7.1",
|
||||
"maputnik-design": "github:maputnik/design#172b06c",
|
||||
"ol": "^10.6.1",
|
||||
"ol-mapbox-style": "^13.0.1",
|
||||
"ol-mapbox-style": "^13.1.0",
|
||||
"pmtiles": "^4.3.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-accessible-accordion": "^5.0.1",
|
||||
"react-aria-menubutton": "^7.0.3",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"downshift": "^9.0.10",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-file-reader-input": "^2.0.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-icon-base": "^2.1.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"slugify": "^1.6.6",
|
||||
"string-hash": "^1.1.3",
|
||||
@@ -92,12 +95,12 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/code-coverage": "^3.14.5",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@cypress/code-coverage": "^3.14.6",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@rollup/plugin-replace": "^6.0.2",
|
||||
"@shellygo/cypress-test-utils": "^5.0.2",
|
||||
"@types/codemirror": "^5.60.15",
|
||||
"@shellygo/cypress-test-utils": "^6.0.1",
|
||||
"@types/codemirror": "^5.60.16",
|
||||
"@types/color": "^4.2.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
@@ -121,11 +124,11 @@
|
||||
"@types/string-hash": "^1.1.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/wicg-file-system-access": "^2023.10.6",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"cors": "^2.8.5",
|
||||
"cypress": "^14.5.2",
|
||||
"cypress": "^15.1.0",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
@@ -134,14 +137,14 @@
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"postcss": "^8.5.6",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"sass": "^1.89.2",
|
||||
"stylelint": "^16.22.0",
|
||||
"stylelint-config-recommended-scss": "^15.0.1",
|
||||
"sass": "^1.92.1",
|
||||
"stylelint": "^16.24.0",
|
||||
"stylelint-config-recommended-scss": "^16.0.0",
|
||||
"stylelint-scss": "^6.12.1",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "^6.3.5",
|
||||
"typescript-eslint": "^8.43.0",
|
||||
"uuid": "^13.0.0",
|
||||
"vite": "^7.1.5",
|
||||
"vite-plugin-istanbul": "^7.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
+90
-144
@@ -27,17 +27,14 @@ import ModalDebug from './ModalDebug'
|
||||
|
||||
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
||||
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 { createStyleStore, type IStyleStore } from '../libs/store/style-store-factory'
|
||||
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 { SortEnd } from 'react-sortable-hoc';
|
||||
import { MapOptions } from 'maplibre-gl';
|
||||
import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions'
|
||||
|
||||
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
|
||||
window.Buffer = buffer.Buffer;
|
||||
@@ -46,6 +43,7 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
|
||||
const matchesTilehosting = url.match(/\.tilehosting\.com/);
|
||||
const matchesMaptiler = url.match(/\.maptiler\.com/);
|
||||
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
||||
const matchesLocationIQ = url.match(/\.locationiq\.com/);
|
||||
if (matchesTilehosting || matchesMaptiler) {
|
||||
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
|
||||
if (accessToken) {
|
||||
@@ -58,6 +56,12 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
|
||||
return url.replace('{key}', accessToken)
|
||||
}
|
||||
}
|
||||
else if (matchesLocationIQ) {
|
||||
const accessToken = style.getAccessToken("locationiq", mapStyle, {allowFallback: true})
|
||||
if (accessToken) {
|
||||
return url.replace('{key}', accessToken)
|
||||
}
|
||||
}
|
||||
else {
|
||||
return url;
|
||||
}
|
||||
@@ -76,12 +80,6 @@ function updateRootSpec(spec: any, fieldName: string, newValues: any) {
|
||||
}
|
||||
}
|
||||
|
||||
type OnStyleChangedOpts = {
|
||||
save?: boolean
|
||||
addRevision?: boolean
|
||||
initialLoad?: boolean
|
||||
}
|
||||
|
||||
type MappedErrors = {
|
||||
message: string
|
||||
parsed?: {
|
||||
@@ -97,11 +95,11 @@ type MappedErrors = {
|
||||
type AppState = {
|
||||
errors: MappedErrors[],
|
||||
infos: string[],
|
||||
mapStyle: StyleSpecification & {id: string},
|
||||
mapStyle: StyleSpecificationWithId,
|
||||
dirtyMapStyle?: StyleSpecification,
|
||||
selectedLayerIndex: number,
|
||||
selectedLayerOriginalId?: string,
|
||||
sources: {[key: string]: SourceSpecification},
|
||||
sources: {[key: string]: SourceSpecification & {layers: string[]} },
|
||||
vectorLayers: {},
|
||||
spec: any,
|
||||
mapView: {
|
||||
@@ -133,25 +131,56 @@ type AppState = {
|
||||
|
||||
export default class App extends React.Component<any, AppState> {
|
||||
revisionStore: RevisionStore;
|
||||
styleStore: StyleStore | ApiStyleStore;
|
||||
styleStore: IStyleStore | null = null;
|
||||
layerWatcher: LayerWatcher;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
|
||||
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();
|
||||
this.configureKeyboardShortcuts();
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
infos: [],
|
||||
mapStyle: style.emptyStyle,
|
||||
selectedLayerIndex: 0,
|
||||
sources: {},
|
||||
vectorLayers: {},
|
||||
mapState: "map",
|
||||
spec: latest,
|
||||
mapView: {
|
||||
zoom: 0,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0,
|
||||
},
|
||||
},
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
debug: false,
|
||||
},
|
||||
maplibreGlDebugOptions: {
|
||||
showTileBoundaries: false,
|
||||
showCollisionBoxes: false,
|
||||
showOverdrawInspector: false,
|
||||
},
|
||||
openlayersDebugOptions: {
|
||||
debugToolbox: false,
|
||||
},
|
||||
fileHandle: null,
|
||||
}
|
||||
this.styleStore = new ApiStyleStore({
|
||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
|
||||
port: port,
|
||||
host: params.get("localhost")
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
configureKeyboardShortcuts = () => {
|
||||
const shortcuts = [
|
||||
{
|
||||
key: "?",
|
||||
@@ -221,74 +250,6 @@ export default class App extends React.Component<any, AppState> {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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()
|
||||
}
|
||||
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}))
|
||||
|
||||
if(Debug.enabled()) {
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if(Debug.enabled()) {
|
||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
infos: [],
|
||||
mapStyle: style.emptyStyle,
|
||||
selectedLayerIndex: 0,
|
||||
sources: {},
|
||||
vectorLayers: {},
|
||||
mapState: "map",
|
||||
spec: latest,
|
||||
mapView: {
|
||||
zoom: 0,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0,
|
||||
},
|
||||
},
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
|
||||
debug: false,
|
||||
},
|
||||
maplibreGlDebugOptions: {
|
||||
showTileBoundaries: false,
|
||||
showCollisionBoxes: false,
|
||||
showOverdrawInspector: false,
|
||||
},
|
||||
openlayersDebugOptions: {
|
||||
debugToolbox: false,
|
||||
},
|
||||
fileHandle: null,
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyPress = (e: KeyboardEvent) => {
|
||||
@@ -314,7 +275,8 @@ export default class App extends React.Component<any, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
async componentDidMount() {
|
||||
this.styleStore = await createStyleStore((mapStyle, opts) => this.onStyleChanged(mapStyle, opts));
|
||||
window.addEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
@@ -322,8 +284,8 @@ export default class App extends React.Component<any, AppState> {
|
||||
window.removeEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle: StyleSpecification & {id: string}) {
|
||||
this.styleStore.save(snapshotStyle)
|
||||
saveStyle(snapshotStyle: StyleSpecificationWithId) {
|
||||
this.styleStore?.save(snapshotStyle)
|
||||
}
|
||||
|
||||
updateFonts(urlTemplate: string) {
|
||||
@@ -364,7 +326,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => {
|
||||
onStyleChanged = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}): void => {
|
||||
opts = {
|
||||
save: true,
|
||||
addRevision: true,
|
||||
@@ -395,7 +357,6 @@ export default class App extends React.Component<any, AppState> {
|
||||
}
|
||||
|
||||
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
|
||||
|
||||
// The validate function doesn't give us errors for duplicate error with
|
||||
// empty string for layer.id, manually deal with that here.
|
||||
const layerErrors: (Error | ValidationError)[] = [];
|
||||
@@ -474,7 +435,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
if (errors.length > 0) {
|
||||
dirtyMapStyle = cloneDeep(newStyle);
|
||||
|
||||
errors.forEach(error => {
|
||||
for (const error of errors) {
|
||||
const {message} = error;
|
||||
if (message) {
|
||||
try {
|
||||
@@ -484,10 +445,10 @@ export default class App extends React.Component<any, AppState> {
|
||||
unset(dirtyMapStyle, unsetPath);
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(err);
|
||||
console.warn(message + " " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
@@ -501,7 +462,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
this.revisionStore.addRevision(newStyle);
|
||||
}
|
||||
if (opts.save) {
|
||||
this.saveStyle(newStyle as StyleSpecification & {id: string});
|
||||
this.saveStyle(newStyle);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@@ -534,7 +495,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
})
|
||||
}
|
||||
|
||||
onMoveLayer = (move: SortEnd) => {
|
||||
onMoveLayer = (move: {oldIndex: number; newIndex: number}) => {
|
||||
let { oldIndex, newIndex } = move;
|
||||
let layers = this.state.mapStyle.layers;
|
||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||
@@ -614,7 +575,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
|
||||
setDefaultValues = (styleObj: StyleSpecification & {id: string}) => {
|
||||
setDefaultValues = (styleObj: StyleSpecificationWithId) => {
|
||||
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
|
||||
if(metadata['maputnik:renderer'] === undefined) {
|
||||
const changedStyle = {
|
||||
@@ -630,27 +591,28 @@ export default class App extends React.Component<any, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
openStyle = (styleObj: StyleSpecification & {id: string}, fileHandle: FileSystemFileHandle | null) => {
|
||||
openStyle = (styleObj: StyleSpecificationWithId, fileHandle: FileSystemFileHandle | null) => {
|
||||
this.setState({fileHandle: fileHandle});
|
||||
styleObj = this.setDefaultValues(styleObj)
|
||||
this.onStyleChanged(styleObj)
|
||||
}
|
||||
|
||||
fetchSources() {
|
||||
const sourceList: {[key: string]: any} = {};
|
||||
|
||||
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||
if(
|
||||
!Object.prototype.hasOwnProperty.call(this.state.sources, key) &&
|
||||
val.type === "vector" &&
|
||||
Object.prototype.hasOwnProperty.call(val, "url")
|
||||
) {
|
||||
async fetchSources() {
|
||||
const sourceList: {[key: string]: SourceSpecification & {layers: string[]}} = {};
|
||||
for(const key of Object.keys(this.state.mapStyle.sources)) {
|
||||
const source = this.state.mapStyle.sources[key];
|
||||
if(source.type !== "vector" || !('url' in source)) {
|
||||
sourceList[key] = this.state.sources[key] || {...this.state.mapStyle.sources[key]};
|
||||
if (sourceList[key].layers === undefined) {
|
||||
sourceList[key].layers = [];
|
||||
}
|
||||
} else {
|
||||
sourceList[key] = {
|
||||
type: val.type,
|
||||
type: source.type,
|
||||
layers: []
|
||||
};
|
||||
|
||||
let url = val.url;
|
||||
let url = source.url;
|
||||
|
||||
try {
|
||||
url = setFetchAccessToken(url!, this.state.mapStyle)
|
||||
@@ -663,44 +625,28 @@ export default class App extends React.Component<any, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new objects before setState
|
||||
const sources = Object.assign({}, {
|
||||
[key]: this.state.sources[key],
|
||||
});
|
||||
|
||||
for(const layer of json.vector_layers) {
|
||||
(sources[key] as any).layers.push(layer.id)
|
||||
sourceList[key].layers.push(layer.id)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
sources: sources
|
||||
});
|
||||
};
|
||||
|
||||
if (url!.startsWith("pmtiles://")) {
|
||||
(new PMTiles(url!.substr(10))).getTileJson("")
|
||||
.then(json => setVectorLayers(json))
|
||||
.catch(err => {
|
||||
console.error("Failed to process sources for '%s'", url, err);
|
||||
});
|
||||
} else {
|
||||
fetch(url!, {
|
||||
mode: 'cors',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => setVectorLayers(json))
|
||||
.catch(err => {
|
||||
console.error("Failed to process sources for '%s'", url, err);
|
||||
});
|
||||
try {
|
||||
if (url!.startsWith("pmtiles://")) {
|
||||
const json = await (new PMTiles(url!.substring(10))).getTileJson("");
|
||||
setVectorLayers(json);
|
||||
} else {
|
||||
const response = await fetch(url!, { mode: 'cors' });
|
||||
const json = await response.json();
|
||||
setVectorLayers(json);
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(`Failed to process source for url: '${url}', ${err}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
|
||||
}
|
||||
}
|
||||
|
||||
if(!isEqual(this.state.sources, sourceList)) {
|
||||
console.debug("Setting sources");
|
||||
console.debug("Setting sources", sourceList);
|
||||
this.setState({
|
||||
sources: sourceList
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ import pkgJson from '../../package.json'
|
||||
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
||||
import { withTranslation, WithTranslation } from 'react-i18next';
|
||||
import { supportedLanguages } from '../i18n';
|
||||
import type { OnStyleChangedCallback } from '../libs/definitions';
|
||||
|
||||
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
||||
const browser = detect();
|
||||
@@ -93,9 +94,9 @@ export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deut
|
||||
type AppToolbarInternalProps = {
|
||||
mapStyle: object
|
||||
inspectModeEnabled: boolean
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
onStyleChanged: OnStyleChangedCallback
|
||||
// A new style has been uploaded
|
||||
onStyleOpen(...args: unknown[]): unknown
|
||||
onStyleOpen: OnStyleChangedCallback
|
||||
// A dict of source id's and the available source layers
|
||||
sources: object
|
||||
children?: React.ReactNode
|
||||
|
||||
@@ -13,28 +13,30 @@ type FieldSourceInternalProps = {
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
|
||||
onChange = () => {},
|
||||
sourceIds = [],
|
||||
wdKey,
|
||||
value,
|
||||
error,
|
||||
t
|
||||
}) => {
|
||||
return (
|
||||
<Block
|
||||
label={t('Source')}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={props.error}
|
||||
data-wd-key={props.wdKey}
|
||||
error={error}
|
||||
data-wd-key={wdKey}
|
||||
>
|
||||
<InputAutocomplete
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
options={props.sourceIds?.map((src) => [src, src])}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={sourceIds?.map((src) => [src, src])}
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
FieldSourceInternal.defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
};
|
||||
|
||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||
export default FieldSource;
|
||||
|
||||
@@ -9,33 +9,31 @@ type FieldSourceLayerInternalProps = {
|
||||
value?: string
|
||||
onChange?(...args: unknown[]): unknown
|
||||
sourceLayerIds?: unknown[]
|
||||
isFixed?: boolean
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
|
||||
onChange = () => {},
|
||||
sourceLayerIds = [],
|
||||
value,
|
||||
error,
|
||||
t
|
||||
}) => {
|
||||
return (
|
||||
<Block
|
||||
label={t('Source Layer')}
|
||||
fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={props.error}
|
||||
error={error}
|
||||
>
|
||||
<InputAutocomplete
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
options={props.sourceLayerIds?.map((l) => [l, l])}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={sourceLayerIds?.map((l) => [l, l])}
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
FieldSourceLayerInternal.defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
isFixed: false,
|
||||
};
|
||||
|
||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||
export default FieldSourceLayer;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import {v8} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputString from './InputString'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { startCase } from 'lodash'
|
||||
|
||||
type FieldTypeInternalProps = {
|
||||
value: string
|
||||
@@ -14,41 +14,34 @@ type FieldTypeInternalProps = {
|
||||
disabled?: boolean
|
||||
} & WithTranslation;
|
||||
|
||||
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
|
||||
t,
|
||||
value,
|
||||
wdKey,
|
||||
onChange,
|
||||
error,
|
||||
disabled = false
|
||||
}) => {
|
||||
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, ' '))]);
|
||||
return (
|
||||
<Block label={t('Type')} fieldSpec={latest.layer.type}
|
||||
data-wd-key={props.wdKey}
|
||||
error={props.error}
|
||||
<Block label={t('Type')} fieldSpec={v8.layer.type}
|
||||
data-wd-key={wdKey}
|
||||
error={error}
|
||||
>
|
||||
{props.disabled && (
|
||||
<InputString value={props.value} disabled={true} />
|
||||
{disabled && (
|
||||
<InputString value={value} disabled={true} />
|
||||
)}
|
||||
{!props.disabled && (
|
||||
{!disabled && (
|
||||
<InputSelect
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
['fill', 'Fill'],
|
||||
['line', 'Line'],
|
||||
['symbol', 'Symbol'],
|
||||
['raster', 'Raster'],
|
||||
['circle', 'Circle'],
|
||||
['fill-extrusion', 'Fill Extrusion'],
|
||||
['hillshade', 'Hillshade'],
|
||||
['heatmap', 'Heatmap'],
|
||||
]}
|
||||
onChange={props.onChange}
|
||||
value={props.value}
|
||||
data-wd-key={props.wdKey + '.select'}
|
||||
options={layerstypes}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-wd-key={wdKey + '.select'}
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
FieldTypeInternal.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
const FieldType = withTranslation()(FieldTypeInternal);
|
||||
export default FieldType;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||
import {isEqual} from 'lodash';
|
||||
import {ExpressionSpecification, LegacyFilterSpecification, StyleSpecification} from 'maplibre-gl'
|
||||
import {ExpressionSpecification, LegacyFilterSpecification} from 'maplibre-gl'
|
||||
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
|
||||
import {mdiFunctionVariant} from '@mdi/js';
|
||||
|
||||
@@ -14,6 +14,7 @@ import InputButton from './InputButton'
|
||||
import Doc from './Doc'
|
||||
import ExpressionProperty from './_ExpressionProperty';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import type { StyleSpecificationWithId } from '../libs/definitions';
|
||||
|
||||
|
||||
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
@@ -39,7 +40,7 @@ function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecificati
|
||||
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
|
||||
}
|
||||
|
||||
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecification & {id: string} {
|
||||
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecificationWithId {
|
||||
return {
|
||||
"id": "tmp",
|
||||
"version": 8,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Accordion} from 'react-accessible-accordion';
|
||||
import {MdMoreVert} from 'react-icons/md'
|
||||
import { IconContext } from 'react-icons'
|
||||
import {BackgroundLayerSpecification, LayerSpecification, SourceSpecification} from 'maplibre-gl';
|
||||
import {v8} from '@maplibre/maplibre-gl-style-spec';
|
||||
|
||||
import FieldJson from './FieldJson'
|
||||
import FilterEditor from './FilterEditor'
|
||||
@@ -17,23 +18,79 @@ import FieldComment from './FieldComment'
|
||||
import FieldSource from './FieldSource'
|
||||
import FieldSourceLayer from './FieldSourceLayer'
|
||||
import { changeType, changeProperty } from '../libs/layer'
|
||||
import layout from '../config/layout.json'
|
||||
import {formatLayerId} from '../libs/format';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next';
|
||||
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers';
|
||||
import { OnMoveLayerCallback } from '../libs/definitions';
|
||||
|
||||
type MaputnikLayoutGroup = {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
fields: string[];
|
||||
}
|
||||
|
||||
function getLayoutForType(type: LayerSpecification["type"], t: TFunction) {
|
||||
return layout[type] ? {
|
||||
...layout[type],
|
||||
groups: layout[type].groups.map(group => {
|
||||
return {
|
||||
...group,
|
||||
id: group.title.replace(/ /g, "_"),
|
||||
title: t(group.title)
|
||||
};
|
||||
}),
|
||||
} : layout.invalid;
|
||||
function getLayoutForSymbolType(t: TFunction): MaputnikLayoutGroup[] {
|
||||
const groups: MaputnikLayoutGroup[] = [];
|
||||
groups.push({
|
||||
title: t("General layout properties"),
|
||||
id: "General_layout_properties",
|
||||
type: "properties",
|
||||
fields: Object.keys(v8["layout_symbol"]).filter(f => f.startsWith("symbol-"))
|
||||
});
|
||||
groups.push({
|
||||
title: t("Text layout properties"),
|
||||
id: "Text_layout_properties",
|
||||
type: "properties",
|
||||
fields: Object.keys(v8["layout_symbol"]).filter(f => f.startsWith("text-"))
|
||||
});
|
||||
groups.push({
|
||||
title: t("Icon layout properties"),
|
||||
id: "Icon_layout_properties",
|
||||
type: "properties",
|
||||
fields: Object.keys(v8["layout_symbol"]).filter(f => f.startsWith("icon-"))
|
||||
});
|
||||
groups.push({
|
||||
title: t("Text paint properties"),
|
||||
id: "Text_paint_properties",
|
||||
type: "properties",
|
||||
fields: Object.keys(v8["paint_symbol"]).filter(f => f.startsWith("text-"))
|
||||
});
|
||||
groups.push({
|
||||
title: t("Icon paint properties"),
|
||||
id: "Icon_paint_properties",
|
||||
type: "properties",
|
||||
fields: Object.keys(v8["paint_symbol"]).filter(f => f.startsWith("icon-"))
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
function getLayoutForType(type: LayerSpecification["type"], t: TFunction): MaputnikLayoutGroup[] {
|
||||
if (Object.keys(v8.layer.type.values).indexOf(type) < 0) {
|
||||
return []
|
||||
}
|
||||
if (type === "symbol") {
|
||||
return getLayoutForSymbolType(t);
|
||||
}
|
||||
const groups: MaputnikLayoutGroup[] = [];
|
||||
if (Object.keys(v8["paint_" + type]).length > 0) {
|
||||
groups.push({
|
||||
title: t("Paint properties"),
|
||||
id: "Paint_properties",
|
||||
type: "properties",
|
||||
fields: Object.keys(v8["paint_" + type]),
|
||||
});
|
||||
}
|
||||
if (Object.keys(v8["layout_" + type]).length > 0) {
|
||||
groups.push({
|
||||
title: t("Layout properties"),
|
||||
id: "Layout_properties",
|
||||
type: "properties",
|
||||
fields: Object.keys(v8["layout_" + type])
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
|
||||
@@ -53,18 +110,18 @@ function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id:
|
||||
type: 'jsoneditor'
|
||||
}
|
||||
return [layerGroup, filterGroup]
|
||||
.concat(getLayoutForType(layerType, t).groups)
|
||||
.concat(getLayoutForType(layerType, t))
|
||||
.concat([editorGroup])
|
||||
}
|
||||
|
||||
type LayerEditorInternalProps = {
|
||||
layer: LayerSpecification
|
||||
sources: {[key: string]: SourceSpecification}
|
||||
sources: {[key: string]: SourceSpecification & {layers: string[]}}
|
||||
vectorLayers: {[key: string]: any}
|
||||
spec: object
|
||||
onLayerChanged(...args: unknown[]): unknown
|
||||
onLayerIdChange(...args: unknown[]): unknown
|
||||
onMoveLayer(...args: unknown[]): unknown
|
||||
onMoveLayer: OnMoveLayerCallback
|
||||
onLayerDestroy(...args: unknown[]): unknown
|
||||
onLayerCopy(...args: unknown[]): unknown
|
||||
onLayerVisibilityToggle(...args: unknown[]): unknown
|
||||
@@ -89,11 +146,10 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
constructor(props: LayerEditorInternalProps) {
|
||||
super(props)
|
||||
|
||||
//TODO: Clean this up and refactor into function
|
||||
const editorGroups: {[keys:string]: boolean} = {}
|
||||
layoutGroups(this.props.layer.type, props.t).forEach(group => {
|
||||
for (const group of layoutGroups(this.props.layer.type, props.t)) {
|
||||
editorGroups[group.title] = true
|
||||
})
|
||||
}
|
||||
|
||||
this.state = { editorGroups }
|
||||
}
|
||||
@@ -101,11 +157,11 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
|
||||
const additionalGroups = { ...state.editorGroups }
|
||||
|
||||
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
|
||||
for (const group of getLayoutForType(props.layer.type, props.t)) {
|
||||
if(!(group.title in additionalGroups)) {
|
||||
additionalGroups[group.title] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
editorGroups: additionalGroups
|
||||
@@ -153,7 +209,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
let sourceLayerIds;
|
||||
const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
|
||||
if(Object.prototype.hasOwnProperty.call(this.props.sources, layer.source)) {
|
||||
sourceLayerIds = (this.props.sources[layer.source] as any).layers;
|
||||
sourceLayerIds = this.props.sources[layer.source].layers;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
@@ -180,7 +236,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
onChange={v => this.changeProperty(null, 'source', v)}
|
||||
/>
|
||||
}
|
||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
|
||||
{!NON_SOURCE_LAYERS.includes(this.props.layer.type) &&
|
||||
<FieldSourceLayer
|
||||
error={errorData['source-layer']}
|
||||
sourceLayerIds={sourceLayerIds}
|
||||
@@ -267,30 +323,38 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
|
||||
const layout = this.props.layer.layout || {}
|
||||
|
||||
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
|
||||
const items: {[key: string]: {
|
||||
text: string,
|
||||
handler: () => void,
|
||||
disabled?: boolean,
|
||||
wdKey?: string
|
||||
}} = {
|
||||
delete: {
|
||||
text: t("Delete"),
|
||||
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
|
||||
handler: () => this.props.onLayerDestroy(this.props.layerIndex),
|
||||
wdKey: "menu-delete-layer"
|
||||
},
|
||||
duplicate: {
|
||||
text: t("Duplicate"),
|
||||
handler: () => this.props.onLayerCopy(this.props.layerIndex)
|
||||
handler: () => this.props.onLayerCopy(this.props.layerIndex),
|
||||
wdKey: "menu-duplicate-layer"
|
||||
},
|
||||
hide: {
|
||||
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex),
|
||||
wdKey: "menu-hide-layer"
|
||||
},
|
||||
moveLayerUp: {
|
||||
text: t("Move layer up"),
|
||||
// Not actually used...
|
||||
disabled: this.props.isFirstLayer,
|
||||
handler: () => this.moveLayer(-1)
|
||||
handler: () => this.moveLayer(-1),
|
||||
wdKey: "menu-move-layer-up"
|
||||
},
|
||||
moveLayerDown: {
|
||||
text: t("Move layer down"),
|
||||
// Not actually used...
|
||||
disabled: this.props.isLastLayer,
|
||||
handler: () => this.moveLayer(+1)
|
||||
handler: () => this.moveLayer(+1),
|
||||
wdKey: "menu-move-layer-down"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +391,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
{Object.keys(items).map((id) => {
|
||||
const item = items[id];
|
||||
return <li key={id}>
|
||||
<MenuItem value={id} className='more-menu__menu__item'>
|
||||
<MenuItem value={id} className='more-menu__menu__item' data-wd-key={item.wdKey}>
|
||||
{item.text}
|
||||
</MenuItem>
|
||||
</li>
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import React, {type JSX} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import lodash from 'lodash';
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
DragEndEvent,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
import LayerListItem from './LayerListItem'
|
||||
import ModalAdd from './ModalAdd'
|
||||
|
||||
import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
|
||||
import type {LayerSpecification} from 'maplibre-gl';
|
||||
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl';
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { OnMoveLayerCallback } from '../libs/definitions';
|
||||
|
||||
type LayerListContainerProps = {
|
||||
layers: LayerSpecification[]
|
||||
@@ -20,7 +32,7 @@ type LayerListContainerProps = {
|
||||
onLayerDestroy?(...args: unknown[]): unknown
|
||||
onLayerCopy(...args: unknown[]): unknown
|
||||
onLayerVisibilityToggle(...args: unknown[]): unknown
|
||||
sources: object
|
||||
sources: Record<string, SourceSpecification & {layers: string[]}>;
|
||||
errors: any[]
|
||||
};
|
||||
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
|
||||
@@ -242,7 +254,6 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
'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}
|
||||
@@ -319,20 +330,35 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
}
|
||||
|
||||
const LayerListContainer = withTranslation()(LayerListContainerInternal);
|
||||
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
|
||||
|
||||
type LayerListProps = LayerListContainerProps & {
|
||||
onMoveLayer: SortEndHandler
|
||||
onMoveLayer: OnMoveLayerCallback
|
||||
};
|
||||
|
||||
export default class LayerList extends React.Component<LayerListProps> {
|
||||
render() {
|
||||
return <LayerListContainerSortable
|
||||
{...this.props}
|
||||
helperClass='sortableHelper'
|
||||
onSortEnd={this.props.onMoveLayer.bind(this)}
|
||||
useDragHandle={true}
|
||||
shouldCancelStart={() => false}
|
||||
/>
|
||||
}
|
||||
const LayerList: React.FC<LayerListProps> = (props) => {
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const {active, over} = event;
|
||||
if (!over) return;
|
||||
|
||||
const oldIndex = props.layers.findIndex(layer => layer.id === active.id);
|
||||
const newIndex = props.layers.findIndex(layer => layer.id === over.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
|
||||
props.onMoveLayer({oldIndex, newIndex});
|
||||
}
|
||||
};
|
||||
|
||||
const layerIds = props.layers.map(layer => layer.id);
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={layerIds} strategy={verticalListSortingStrategy}>
|
||||
<LayerListContainer {...props} />
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayerList;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
||||
import { IconContext } from 'react-icons'
|
||||
import {useSortable} from '@dnd-kit/sortable'
|
||||
import {CSS} from '@dnd-kit/utilities'
|
||||
|
||||
import IconLayer from './IconLayer'
|
||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||
|
||||
|
||||
type DraggableLabelProps = {
|
||||
layerId: string
|
||||
layerType: string
|
||||
dragAttributes?: React.HTMLAttributes<HTMLElement>
|
||||
dragListeners?: React.HTMLAttributes<HTMLElement>
|
||||
};
|
||||
|
||||
const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
|
||||
return <div className="maputnik-layer-list-item-handle">
|
||||
const DraggableLabel: React.FC<DraggableLabelProps> = (props) => {
|
||||
const {dragAttributes, dragListeners} = props;
|
||||
return <div className="maputnik-layer-list-item-handle" {...dragAttributes} {...dragListeners}>
|
||||
<IconLayer
|
||||
className="layer-handle__icon"
|
||||
type={props.layerType}
|
||||
@@ -23,7 +26,7 @@ const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
|
||||
{props.layerId}
|
||||
</button>
|
||||
</div>
|
||||
});
|
||||
};
|
||||
|
||||
type IconActionProps = {
|
||||
action: string
|
||||
@@ -82,55 +85,80 @@ type LayerListItemProps = {
|
||||
onLayerVisibilityToggle?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
class LayerListItem extends React.Component<LayerListItemProps> {
|
||||
static defaultProps = {
|
||||
isSelected: false,
|
||||
visibility: 'visible',
|
||||
onLayerCopy: () => {},
|
||||
onLayerDestroy: () => {},
|
||||
onLayerVisibilityToggle: () => {},
|
||||
}
|
||||
const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props, ref) => {
|
||||
const {
|
||||
isSelected = false,
|
||||
visibility = 'visible',
|
||||
onLayerCopy = () => {},
|
||||
onLayerDestroy = () => {},
|
||||
onLayerVisibilityToggle = () => {},
|
||||
} = props;
|
||||
|
||||
render() {
|
||||
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({id: props.layerId});
|
||||
|
||||
return <IconContext.Provider value={{size: '14px'}}>
|
||||
<li
|
||||
id={this.props.id}
|
||||
key={this.props.layerId}
|
||||
onClick={_e => this.props.onLayerSelect(this.props.layerIndex)}
|
||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||
[this.props.className!]: true,
|
||||
})}>
|
||||
<DraggableLabel {...this.props} />
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
classBlockName="delete"
|
||||
onClick={_e => this.props.onLayerDestroy!(this.props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||
action={'duplicate'}
|
||||
classBlockName="duplicate"
|
||||
onClick={_e => this.props.onLayerCopy!(this.props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
||||
action={visibilityAction}
|
||||
classBlockName="visibility"
|
||||
classBlockModifier={visibilityAction}
|
||||
onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
|
||||
/>
|
||||
</li>
|
||||
</IconContext.Provider>
|
||||
}
|
||||
}
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
const LayerListItemSortable = SortableElement<LayerListItemProps>((props: LayerListItemProps) => <LayerListItem {...props} />);
|
||||
const visibilityAction = visibility === 'visible' ? 'show' : 'hide';
|
||||
|
||||
export default LayerListItemSortable;
|
||||
// Cast ref to MutableRefObject since we know from the codebase that's what's always passed
|
||||
const refObject = ref as React.MutableRefObject<HTMLLIElement | null> | null;
|
||||
|
||||
return <IconContext.Provider value={{size: '14px'}}>
|
||||
<li
|
||||
ref={(node) => {
|
||||
setNodeRef(node);
|
||||
if (refObject) {
|
||||
refObject.current = node;
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
id={props.id}
|
||||
onClick={_e => props.onLayerSelect(props.layerIndex)}
|
||||
data-wd-key={"layer-list-item:" + props.layerId}
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": isSelected,
|
||||
[props.className!]: true,
|
||||
})}>
|
||||
<DraggableLabel
|
||||
layerId={props.layerId}
|
||||
layerType={props.layerType}
|
||||
dragAttributes={attributes}
|
||||
dragListeners={listeners}
|
||||
/>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:" + props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
classBlockName="delete"
|
||||
onClick={_e => onLayerDestroy!(props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:" + props.layerId+":copy"}
|
||||
action={'duplicate'}
|
||||
classBlockName="duplicate"
|
||||
onClick={_e => onLayerCopy!(props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+props.layerId+":toggle-visibility"}
|
||||
action={visibilityAction}
|
||||
classBlockName="visibility"
|
||||
classBlockModifier={visibilityAction}
|
||||
onClick={_e => onLayerVisibilityToggle!(props.layerIndex)}
|
||||
/>
|
||||
</li>
|
||||
</IconContext.Provider>
|
||||
});
|
||||
|
||||
export default LayerListItem;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {type JSX} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
|
||||
import MaplibreInspect from '@maplibre/maplibre-gl-inspect'
|
||||
import colors from '@maplibre/maplibre-gl-inspect/lib/colors'
|
||||
@@ -17,8 +17,14 @@ import { withTranslation, WithTranslation } from 'react-i18next'
|
||||
import i18next from 'i18next'
|
||||
import { Protocol } from "pmtiles";
|
||||
|
||||
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
|
||||
ReactDOM.render(popup, mountNode);
|
||||
function renderPopup(
|
||||
popupElement: JSX.Element,
|
||||
mountNode: HTMLElement,
|
||||
popup: MapLibreGl.Popup
|
||||
): HTMLElement {
|
||||
const root = createRoot(mountNode);
|
||||
popup.once('close', () => root.unmount());
|
||||
root.render(popupElement);
|
||||
return mountNode as HTMLElement;
|
||||
}
|
||||
|
||||
@@ -174,10 +180,12 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
|
||||
const tmpNode = document.createElement('div');
|
||||
|
||||
const inspectPopup = new MapLibreGl.Popup({
|
||||
closeOnClick: false
|
||||
});
|
||||
|
||||
const inspect = new MaplibreInspect({
|
||||
popup: new MapLibreGl.Popup({
|
||||
closeOnClick: false
|
||||
}),
|
||||
popup: inspectPopup,
|
||||
showMapPopup: true,
|
||||
showMapPopupOnHover: false,
|
||||
showInspectMapPopupOnHover: true,
|
||||
@@ -189,9 +197,21 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
||||
renderPopup: (features: InspectFeature[]) => {
|
||||
if(this.props.inspectModeEnabled) {
|
||||
return renderPopup(<MapMaplibreGlFeaturePropertyPopup features={features} />, tmpNode);
|
||||
return renderPopup(
|
||||
<MapMaplibreGlFeaturePropertyPopup features={features} />,
|
||||
tmpNode,
|
||||
inspectPopup
|
||||
);
|
||||
} else {
|
||||
return renderPopup(<MapMaplibreGlLayerPopup features={features} onLayerSelect={this.onLayerSelectById} zoom={this.state.zoom} />, tmpNode);
|
||||
return renderPopup(
|
||||
<MapMaplibreGlLayerPopup
|
||||
features={features}
|
||||
onLayerSelect={this.onLayerSelectById}
|
||||
zoom={this.state.zoom}
|
||||
/>,
|
||||
tmpNode,
|
||||
inspectPopup
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+28
-38
@@ -6,8 +6,9 @@ import FieldType from './FieldType'
|
||||
import FieldId from './FieldId'
|
||||
import FieldSource from './FieldSource'
|
||||
import FieldSourceLayer from './FieldSourceLayer'
|
||||
import type {LayerSpecification} from 'maplibre-gl'
|
||||
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers'
|
||||
|
||||
type ModalAddInternalProps = {
|
||||
layers: LayerSpecification[]
|
||||
@@ -15,7 +16,7 @@ type ModalAddInternalProps = {
|
||||
isOpen: boolean
|
||||
onOpenToggle(open: boolean): unknown
|
||||
// A dict of source id's and the available source layers
|
||||
sources: any
|
||||
sources: Record<string, SourceSpecification & {layers: string[]}>;
|
||||
} & WithTranslation;
|
||||
|
||||
type ModalAddState = {
|
||||
@@ -41,7 +42,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
||||
|
||||
if(this.state.type !== 'background') {
|
||||
layer.source = this.state.source
|
||||
if(this.state.type !== 'raster' && this.state['source-layer']) {
|
||||
if(!NON_SOURCE_LAYERS.includes(this.state.type) && this.state['source-layer']) {
|
||||
layer['source-layer'] = this.state['source-layer']
|
||||
}
|
||||
}
|
||||
@@ -61,9 +62,12 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
||||
error: null,
|
||||
}
|
||||
|
||||
if(props.sources.length > 0) {
|
||||
if(Object.keys(props.sources).length > 0) {
|
||||
state.source = Object.keys(this.props.sources)[0];
|
||||
state['source-layer'] = this.props.sources[state.source as keyof ModalAddInternalProps["sources"]][0]
|
||||
const sourceLayers = this.props.sources[state.source].layers || []
|
||||
if (sourceLayers.length > 0) {
|
||||
state['source-layer'] = sourceLayers[0];
|
||||
}
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
@@ -97,39 +101,26 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
||||
return sourceObj.layers || [];
|
||||
}
|
||||
|
||||
getSources(type: string) {
|
||||
const sources = [];
|
||||
getSources(type: LayerSpecification["type"]) {
|
||||
|
||||
const types = {
|
||||
vector: [
|
||||
"fill",
|
||||
"line",
|
||||
"symbol",
|
||||
"circle",
|
||||
"fill-extrusion",
|
||||
"heatmap"
|
||||
],
|
||||
raster: [
|
||||
"raster"
|
||||
],
|
||||
geojson: [
|
||||
"fill",
|
||||
"line",
|
||||
"symbol",
|
||||
"circle",
|
||||
"fill-extrusion",
|
||||
"heatmap"
|
||||
]
|
||||
switch(type) {
|
||||
case 'background':
|
||||
return [];
|
||||
case 'hillshade':
|
||||
case 'color-relief':
|
||||
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'raster-dem').map(([k, _]) => k);
|
||||
case 'raster':
|
||||
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'raster').map(([k, _]) => k);
|
||||
case 'heatmap':
|
||||
case 'circle':
|
||||
case 'fill':
|
||||
case 'fill-extrusion':
|
||||
case 'line':
|
||||
case 'symbol':
|
||||
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'vector' || v.type === 'geojson').map(([k, _]) => k);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
for(const [key, val] of Object.entries(this.props.sources) as any) {
|
||||
const valType = val.type as keyof typeof types;
|
||||
if(types[valType] && types[valType].indexOf(type) > -1) {
|
||||
sources.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
|
||||
@@ -182,9 +173,8 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
||||
onChange={(v: string) => this.setState({ source: v })}
|
||||
/>
|
||||
}
|
||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||
{!NON_SOURCE_LAYERS.includes(this.state.type) &&
|
||||
<FieldSourceLayer
|
||||
isFixed={true}
|
||||
sourceLayerIds={layers}
|
||||
value={this.state['source-layer']}
|
||||
onChange={(v: string) => this.setState({ 'source-layer': v })}
|
||||
|
||||
@@ -3,7 +3,6 @@ import Slugify from 'slugify'
|
||||
import {saveAs} from 'file-saver'
|
||||
import {version} from 'maplibre-gl/package.json'
|
||||
import {format} from '@maplibre/maplibre-gl-style-spec'
|
||||
import type {StyleSpecification} from 'maplibre-gl'
|
||||
import {MdMap, MdSave} from 'react-icons/md'
|
||||
import {WithTranslation, withTranslation} from 'react-i18next';
|
||||
|
||||
@@ -12,6 +11,7 @@ import InputButton from './InputButton'
|
||||
import Modal from './Modal'
|
||||
import style from '../libs/style'
|
||||
import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||
import type {OnStyleChangedCallback, StyleSpecificationWithId} from '../libs/definitions'
|
||||
|
||||
|
||||
const MAPLIBRE_GL_VERSION = version;
|
||||
@@ -19,8 +19,8 @@ const showSaveFilePickerAvailable = typeof window.showSaveFilePicker === "functi
|
||||
|
||||
|
||||
type ModalExportInternalProps = {
|
||||
mapStyle: StyleSpecification & { id: string }
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
mapStyle: StyleSpecificationWithId
|
||||
onStyleChanged: OnStyleChangedCallback
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown
|
||||
@@ -186,6 +186,12 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||
value={(this.props.mapStyle.metadata || {} as any)['maputnik:stadia_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
|
||||
/>
|
||||
<FieldString
|
||||
label={fsa.maputnik.locationiq_access_token.label}
|
||||
fieldSpec={fsa.maputnik.locationiq_access_token}
|
||||
value={(this.props.mapStyle.metadata || {} as any)['maputnik:locationiq_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:locationiq_access_token")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-export-buttons">
|
||||
|
||||
@@ -12,10 +12,11 @@ import FieldEnum from './FieldEnum'
|
||||
import FieldColor from './FieldColor'
|
||||
import Modal from './Modal'
|
||||
import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||
import type {OnStyleChangedCallback, StyleSpecificationWithId} from '../libs/definitions';
|
||||
|
||||
type ModalSettingsInternalProps = {
|
||||
mapStyle: StyleSpecification
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
mapStyle: StyleSpecificationWithId
|
||||
onStyleChanged: OnStyleChangedCallback
|
||||
onChangeMetadataProperty(...args: unknown[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
@@ -62,7 +63,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
changeTerrainProperty(property: keyof TerrainSpecification, value: any) {
|
||||
const terrain = {
|
||||
...this.props.mapStyle.terrain,
|
||||
}
|
||||
} as TerrainSpecification;
|
||||
|
||||
if (value === undefined) {
|
||||
delete terrain[property];
|
||||
@@ -164,6 +165,14 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
|
||||
/>
|
||||
|
||||
<FieldString
|
||||
label={fsa.maputnik.locationiq_access_token.label}
|
||||
fieldSpec={fsa.maputnik.locationiq_access_token}
|
||||
data-wd-key="modal:settings.maputnik:locationiq_access_token"
|
||||
value={metadata['maputnik:locationiq_access_token']}
|
||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:locationiq_access_token")}
|
||||
/>
|
||||
|
||||
<FieldArray
|
||||
label={t("Center")}
|
||||
fieldSpec={latest.$root.center}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl'
|
||||
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, VectorSourceSpecification} from 'maplibre-gl'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import Modal from './Modal'
|
||||
@@ -13,6 +13,7 @@ import ModalSourcesTypeEditor, { EditorMode } from './ModalSourcesTypeEditor'
|
||||
import style from '../libs/style'
|
||||
import { deleteSource, addSource, changeSource } from '../libs/source'
|
||||
import publicSources from '../config/tilesets.json'
|
||||
import { OnStyleChangedCallback, StyleSpecificationWithId } from '../libs/definitions';
|
||||
|
||||
|
||||
type PublicSourceProps = {
|
||||
@@ -270,10 +271,10 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
}
|
||||
|
||||
type ModalSourcesInternalProps = {
|
||||
mapStyle: StyleSpecification
|
||||
mapStyle: StyleSpecificationWithId
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
onStyleChanged: OnStyleChangedCallback
|
||||
} & WithTranslation;
|
||||
|
||||
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
||||
|
||||
@@ -17,7 +17,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
||||
const t = this.props.t;
|
||||
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
|
||||
|
||||
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
||||
if (this.props.fieldSpec.expression?.parameters.includes('zoom')) {
|
||||
expressionInputButton = (
|
||||
<InputButton
|
||||
className="maputnik-make-zoom-function"
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
{
|
||||
"line": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-opacity",
|
||||
"line-color",
|
||||
"line-width",
|
||||
"line-offset",
|
||||
"line-blur",
|
||||
"line-dasharray",
|
||||
"line-pattern",
|
||||
"line-translate",
|
||||
"line-translate-anchor",
|
||||
"line-gap-width"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-cap",
|
||||
"line-join",
|
||||
"line-miter-limit",
|
||||
"line-round-limit"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"background-color",
|
||||
"background-pattern",
|
||||
"background-opacity"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"fill": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"fill-opacity",
|
||||
"fill-color",
|
||||
"fill-antialias",
|
||||
"fill-outline-color",
|
||||
"fill-pattern",
|
||||
"fill-translate",
|
||||
"fill-translate-anchor"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"fill-extrusion": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"fill-extrusion-opacity",
|
||||
"fill-extrusion-color",
|
||||
"fill-extrusion-translate",
|
||||
"fill-extrusion-translate-anchor",
|
||||
"fill-extrusion-pattern",
|
||||
"fill-extrusion-height",
|
||||
"fill-extrusion-base",
|
||||
"fill-extrusion-vertical-gradient"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"circle": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-color",
|
||||
"circle-opacity",
|
||||
"circle-stroke-color",
|
||||
"circle-stroke-opacity",
|
||||
"circle-blur",
|
||||
"circle-radius",
|
||||
"circle-stroke-width",
|
||||
"circle-pitch-scale",
|
||||
"circle-translate",
|
||||
"circle-translate-anchor",
|
||||
"circle-pitch-alignment"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"symbol": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "General layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"symbol-placement",
|
||||
"symbol-spacing",
|
||||
"symbol-avoid-edges",
|
||||
"symbol-z-order"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Text layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-field",
|
||||
"text-font",
|
||||
"text-size",
|
||||
"text-line-height",
|
||||
"text-padding",
|
||||
"text-allow-overlap",
|
||||
"text-ignore-placement",
|
||||
"text-pitch-alignment",
|
||||
"text-rotation-alignment",
|
||||
"text-max-width",
|
||||
"text-letter-spacing",
|
||||
"text-justify",
|
||||
"text-anchor",
|
||||
"text-max-angle",
|
||||
"text-writing-mode",
|
||||
"text-rotate",
|
||||
"text-keep-upright",
|
||||
"text-transform",
|
||||
"text-offset",
|
||||
"text-optional",
|
||||
"text-variable-anchor",
|
||||
"text-radial-offset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Icon layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"icon-image",
|
||||
"icon-allow-overlap",
|
||||
"icon-ignore-placement",
|
||||
"icon-optional",
|
||||
"icon-rotation-alignment",
|
||||
"icon-size",
|
||||
"icon-text-fit",
|
||||
"icon-text-fit-padding",
|
||||
"icon-rotate",
|
||||
"icon-padding",
|
||||
"icon-keep-upright",
|
||||
"icon-offset",
|
||||
"icon-anchor",
|
||||
"icon-pitch-alignment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Text paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-color",
|
||||
"text-opacity",
|
||||
"text-halo-color",
|
||||
"text-halo-width",
|
||||
"text-halo-blur",
|
||||
"text-translate",
|
||||
"text-translate-anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Icon paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"icon-color",
|
||||
"icon-opacity",
|
||||
"icon-halo-color",
|
||||
"icon-halo-width",
|
||||
"icon-halo-blur",
|
||||
"icon-translate",
|
||||
"icon-translate-anchor"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"raster": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"raster-opacity",
|
||||
"raster-hue-rotate",
|
||||
"raster-brightness-min",
|
||||
"raster-brightness-max",
|
||||
"raster-saturation",
|
||||
"raster-contrast",
|
||||
"raster-fade-duration",
|
||||
"raster-resampling"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"hillshade": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"hillshade-illumination-direction",
|
||||
"hillshade-illumination-anchor",
|
||||
"hillshade-exaggeration",
|
||||
"hillshade-shadow-color",
|
||||
"hillshade-highlight-color",
|
||||
"hillshade-accent-color"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"heatmap": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"heatmap-radius",
|
||||
"heatmap-weight",
|
||||
"heatmap-intensity",
|
||||
"heatmap-opacity"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"invalid": {
|
||||
"groups": []
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,12 @@
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.9/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
|
||||
},
|
||||
{
|
||||
"id": "locationiq-streets",
|
||||
"title": "LocationIQ Streets",
|
||||
"url": "https://tiles.locationiq.com/v3/streets/vector.json?key={key}",
|
||||
"thumbnail": "https://static-assets.locationiq.com/maputnik/locationiq-streets.png"
|
||||
},
|
||||
{
|
||||
"id": "maptiler-basic-gl-style",
|
||||
"title": "MapTiler Basic",
|
||||
|
||||
@@ -18,5 +18,10 @@
|
||||
"type": "vector",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/data/vector/open-zoomstack/config.json",
|
||||
"title": "OS Open Zoomstack v2"
|
||||
},
|
||||
"locationiq": {
|
||||
"type": "vector",
|
||||
"url": "https://tiles.locationiq.com/v3/pbf/tiles.json?key={key}",
|
||||
"title": "LocationIQ"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"openmaptiles": "get_your_own_OpIi9ZULNHzrESv6T2vL",
|
||||
"thunderforest": "b71f7f0ba4064f5eb9e903859a9cf5c6"
|
||||
"thunderforest": "b71f7f0ba4064f5eb9e903859a9cf5c6",
|
||||
"locationiq": "pk.put_your_api_key_here7bb23dffeb4"
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
interface DebugStore {
|
||||
[namespace: string]: {
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
const debugStore: DebugStore = {};
|
||||
|
||||
function enabled() {
|
||||
const qs = new URL(window.location.href).searchParams;
|
||||
const debugQs = qs.get("debug");
|
||||
if(debugQs) {
|
||||
return !!debugQs.match(/^(|1|true)$/);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function genErr() {
|
||||
return new Error("Debug not enabled, enable by appending '?debug' to your query string");
|
||||
}
|
||||
|
||||
function set(namespace: keyof DebugStore, key: string, value: any) {
|
||||
if(!enabled()) {
|
||||
throw genErr();
|
||||
}
|
||||
debugStore[namespace] = debugStore[namespace] || {};
|
||||
debugStore[namespace][key] = value;
|
||||
}
|
||||
|
||||
function get(namespace: keyof DebugStore, key: string) {
|
||||
if(!enabled()) {
|
||||
throw genErr();
|
||||
}
|
||||
if(Object.prototype.hasOwnProperty.call(debugStore, namespace)) {
|
||||
return debugStore[namespace][key];
|
||||
}
|
||||
}
|
||||
|
||||
const mod = {
|
||||
enabled,
|
||||
get,
|
||||
set
|
||||
};
|
||||
|
||||
(window as any).debug = mod;
|
||||
export default mod;
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
import type { StyleSpecification } from "maplibre-gl";
|
||||
|
||||
export type StyleSpecificationWithId = StyleSpecification & {id: string};
|
||||
|
||||
export type OnStyleChangedOpts = {
|
||||
save?: boolean;
|
||||
addRevision?: boolean;
|
||||
initialLoad?: boolean;
|
||||
}
|
||||
|
||||
export type OnStyleChangedCallback = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}) => void;
|
||||
|
||||
export type OnMoveLayerCallback = (move: {oldIndex: number; newIndex: number}) => void;
|
||||
|
||||
export interface IStyleStore {
|
||||
getLatestStyle(): Promise<StyleSpecificationWithId>;
|
||||
save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId;
|
||||
}
|
||||
@@ -20,6 +20,12 @@ const spec = (t: TFunction) => ({
|
||||
docUrl: "https://docs.stadiamaps.com/authentication/",
|
||||
docUrlLinkText: t("Learn More")
|
||||
},
|
||||
locationiq_access_token: {
|
||||
label: t("LocationIQ Access Token"),
|
||||
doc: t("Public access token for LocationIQ services."),
|
||||
docUrl: "https://docs.locationiq.com/docs/maps",
|
||||
docUrlLinkText: t("Learn More")
|
||||
},
|
||||
style_renderer: {
|
||||
label: t("Style Renderer"),
|
||||
doc: t("Choose the default Maputnik renderer for this style.")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const NON_SOURCE_LAYERS = ['background', 'raster', 'hillshade', 'heatmap', 'color-relief']
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {StyleSpecification} from "maplibre-gl";
|
||||
import { StyleSpecificationWithId } from "./definitions";
|
||||
|
||||
export class RevisionStore {
|
||||
revisions: (StyleSpecification & {id: string})[];
|
||||
revisions: StyleSpecificationWithId[];
|
||||
currentIdx: number;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export class RevisionStore {
|
||||
return this.revisions[this.currentIdx]
|
||||
}
|
||||
|
||||
addRevision(revision: StyleSpecification & {id: string}) {
|
||||
addRevision(revision: StyleSpecificationWithId) {
|
||||
// clear any "redo" revisions once a change is made
|
||||
// and ensure current index is at end of list
|
||||
this.revisions = this.revisions.slice(0, this.currentIdx + 1);
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
import type {StyleSpecification, SourceSpecification} from "maplibre-gl";
|
||||
import type {SourceSpecification} from "maplibre-gl";
|
||||
import type {StyleSpecificationWithId} from "./definitions";
|
||||
|
||||
export function deleteSource(mapStyle: StyleSpecification, sourceId: string) {
|
||||
export function deleteSource(mapStyle: StyleSpecificationWithId, sourceId: string) {
|
||||
const remainingSources = { ...mapStyle.sources}
|
||||
delete remainingSources[sourceId]
|
||||
return {
|
||||
@@ -10,11 +11,11 @@ export function deleteSource(mapStyle: StyleSpecification, sourceId: string) {
|
||||
}
|
||||
|
||||
|
||||
export function addSource(mapStyle: StyleSpecification, sourceId: string, source: SourceSpecification) {
|
||||
export function addSource(mapStyle: StyleSpecificationWithId, sourceId: string, source: SourceSpecification) {
|
||||
return changeSource(mapStyle, sourceId, source)
|
||||
}
|
||||
|
||||
export function changeSource(mapStyle: StyleSpecification, sourceId: string, source: SourceSpecification) {
|
||||
export function changeSource(mapStyle: StyleSpecificationWithId, sourceId: string, source: SourceSpecification) {
|
||||
const changedSources = {
|
||||
...mapStyle.sources,
|
||||
[sourceId]: source
|
||||
|
||||
@@ -1,46 +1,38 @@
|
||||
import style from './style.js'
|
||||
import style from '../style'
|
||||
import {format} from '@maplibre/maplibre-gl-style-spec'
|
||||
import type {StyleSpecification} from 'maplibre-gl'
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket'
|
||||
import type {IStyleStore, OnStyleChangedCallback, StyleSpecificationWithId} from '../definitions'
|
||||
|
||||
export type ApiStyleStoreOptions = {
|
||||
port: string | null
|
||||
host: string | null
|
||||
onLocalStyleChange?: (style: any) => void
|
||||
onLocalStyleChange?: OnStyleChangedCallback
|
||||
}
|
||||
|
||||
export class ApiStyleStore {
|
||||
export class ApiStyleStore implements IStyleStore {
|
||||
|
||||
localUrl: string;
|
||||
websocketUrl: string;
|
||||
latestStyleId: string | undefined = undefined;
|
||||
onLocalStyleChange: (style: any) => void;
|
||||
onLocalStyleChange: OnStyleChangedCallback;
|
||||
|
||||
constructor(opts: ApiStyleStoreOptions) {
|
||||
this.onLocalStyleChange = opts.onLocalStyleChange || (() => {})
|
||||
const port = opts.port || '8000'
|
||||
const host = opts.host || 'localhost'
|
||||
const port = window.location.port
|
||||
const host = 'localhost'
|
||||
this.localUrl = `http://${host}:${port}`
|
||||
this.websocketUrl = `ws://${host}:${port}/ws`
|
||||
this.init = this.init.bind(this)
|
||||
}
|
||||
|
||||
init(cb: (...args: any[]) => void) {
|
||||
fetch(this.localUrl + '/styles', {
|
||||
mode: 'cors',
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((body) => {
|
||||
const styleIds = body;
|
||||
this.latestStyleId = styleIds[0]
|
||||
this.notifyLocalChanges()
|
||||
cb(null)
|
||||
})
|
||||
.catch(() => {
|
||||
cb(new Error('Can not connect to style API'))
|
||||
})
|
||||
async init(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(this.localUrl + '/styles', {mode: 'cors'});
|
||||
const body = await response.json();
|
||||
const styleIds = body;
|
||||
this.latestStyleId = styleIds[0]
|
||||
this.notifyLocalChanges();
|
||||
} catch {
|
||||
throw new Error('Can not connect to style API');
|
||||
}
|
||||
}
|
||||
|
||||
notifyLocalChanges() {
|
||||
@@ -59,24 +51,20 @@ export class ApiStyleStore {
|
||||
}
|
||||
}
|
||||
|
||||
latestStyle(cb: (...args: any[]) => void) {
|
||||
async getLatestStyle(): Promise<StyleSpecificationWithId> {
|
||||
if(this.latestStyleId) {
|
||||
fetch(this.localUrl + '/styles/' + this.latestStyleId, {
|
||||
const response = await fetch(this.localUrl + '/styles/' + this.latestStyleId, {
|
||||
mode: 'cors',
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(body) {
|
||||
cb(style.ensureStyleValidity(body))
|
||||
})
|
||||
});
|
||||
const body = await response.json();
|
||||
return style.ensureStyleValidity(body);
|
||||
} else {
|
||||
throw new Error('No latest style available. You need to init the api backend first.')
|
||||
}
|
||||
}
|
||||
|
||||
// Save current style replacing previous version
|
||||
save(mapStyle: StyleSpecification & { id: string }) {
|
||||
save(mapStyle: StyleSpecificationWithId) {
|
||||
const styleJSON = format(
|
||||
style.stripAccessTokens(
|
||||
style.replaceAccessTokens(mapStyle)
|
||||
@@ -0,0 +1,29 @@
|
||||
/// <reference types="vite/client" />
|
||||
import { IStyleStore, OnStyleChangedCallback } from "../definitions";
|
||||
import { getStyleUrlFromAddressbarAndRemoveItIfNeeded, loadStyleUrl } from "../urlopen";
|
||||
import { ApiStyleStore } from "./apistore";
|
||||
import { StyleStore } from "./stylestore";
|
||||
|
||||
export async function createStyleStore(onStyleChanged: OnStyleChangedCallback): Promise<IStyleStore> {
|
||||
const styleUrl = getStyleUrlFromAddressbarAndRemoveItIfNeeded();
|
||||
const useStyleUrl = styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?");
|
||||
let styleStore: IStyleStore;
|
||||
if (import.meta.env.MODE === 'desktop' && !useStyleUrl) {
|
||||
const apiStyleStore = new ApiStyleStore({
|
||||
onLocalStyleChange: mapStyle => onStyleChanged(mapStyle, {save: false}),
|
||||
});
|
||||
try {
|
||||
await apiStyleStore.init();
|
||||
styleStore = apiStyleStore;
|
||||
} catch {
|
||||
styleStore = new StyleStore();
|
||||
}
|
||||
} else {
|
||||
styleStore = new StyleStore();
|
||||
}
|
||||
const styleToLoad = useStyleUrl ? await loadStyleUrl(styleUrl) : await styleStore.getLatestStyle();
|
||||
onStyleChanged(styleToLoad, {initialLoad: true, save: false});
|
||||
return styleStore;
|
||||
}
|
||||
|
||||
export type { IStyleStore };
|
||||
@@ -1,7 +1,7 @@
|
||||
import style from './style'
|
||||
import {loadStyleUrl} from './urlopen'
|
||||
import publicSources from '../config/styles.json'
|
||||
import type {StyleSpecification} from 'maplibre-gl'
|
||||
import style from '../style'
|
||||
import {loadStyleUrl} from '../urlopen'
|
||||
import publicSources from '../../config/styles.json'
|
||||
import type {IStyleStore, StyleSpecificationWithId} from '../definitions'
|
||||
|
||||
const storagePrefix = "maputnik"
|
||||
const stylePrefix = 'style'
|
||||
@@ -13,8 +13,8 @@ const storageKeys = {
|
||||
const defaultStyleUrl = publicSources[0].url
|
||||
|
||||
// Fetch a default style via URL and return it or a fallback style via callback
|
||||
export function loadDefaultStyle(cb: (...args: any[]) => void) {
|
||||
loadStyleUrl(defaultStyleUrl, cb)
|
||||
export function loadDefaultStyle(): Promise<StyleSpecificationWithId> {
|
||||
return loadStyleUrl(defaultStyleUrl);
|
||||
}
|
||||
|
||||
// Return style ids and dates of all styles stored in local storage
|
||||
@@ -51,7 +51,7 @@ function styleKey(styleId: string) {
|
||||
}
|
||||
|
||||
// Manages many possible styles that are stored in the local storage
|
||||
export class StyleStore {
|
||||
export class StyleStore implements IStyleStore {
|
||||
/**
|
||||
* List of style ids
|
||||
*/
|
||||
@@ -63,10 +63,6 @@ export class StyleStore {
|
||||
this.mapStyles = loadStoredStyles();
|
||||
}
|
||||
|
||||
init(cb: (...args: any[]) => void) {
|
||||
cb(null)
|
||||
}
|
||||
|
||||
// Delete entire style history
|
||||
purge() {
|
||||
for (let i = 0; i < window.localStorage.length; i++) {
|
||||
@@ -78,17 +74,21 @@ export class StyleStore {
|
||||
}
|
||||
|
||||
// Find the last edited style
|
||||
latestStyle(cb: (...args: any[]) => void) {
|
||||
if(this.mapStyles.length === 0) return loadDefaultStyle(cb)
|
||||
async getLatestStyle(): Promise<StyleSpecificationWithId> {
|
||||
if(this.mapStyles.length === 0) {
|
||||
return loadDefaultStyle();
|
||||
}
|
||||
const styleId = window.localStorage.getItem(storageKeys.latest) as string;
|
||||
const styleItem = window.localStorage.getItem(styleKey(styleId))
|
||||
|
||||
if(styleItem) return cb(JSON.parse(styleItem))
|
||||
loadDefaultStyle(cb)
|
||||
if (styleItem) {
|
||||
return JSON.parse(styleItem) as StyleSpecificationWithId;
|
||||
}
|
||||
return loadDefaultStyle();
|
||||
}
|
||||
|
||||
// Save current style replacing previous version
|
||||
save(mapStyle: StyleSpecification & { id: string }) {
|
||||
save(mapStyle: StyleSpecificationWithId) {
|
||||
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||
const key = styleKey(mapStyle.id)
|
||||
|
||||
+10
-6
@@ -1,6 +1,7 @@
|
||||
import {derefLayers} from '@maplibre/maplibre-gl-style-spec'
|
||||
import type {StyleSpecification, LayerSpecification} from 'maplibre-gl'
|
||||
import tokens from '../config/tokens.json'
|
||||
import type {StyleSpecificationWithId} from './definitions'
|
||||
|
||||
// Empty style is always used if no style could be restored or fetched
|
||||
const emptyStyle = ensureStyleValidity({
|
||||
@@ -13,15 +14,14 @@ function generateId() {
|
||||
return Math.random().toString(36).substring(2, 9)
|
||||
}
|
||||
|
||||
function ensureHasId(style: StyleSpecification & { id?: string }): StyleSpecification & { id: string } {
|
||||
function ensureHasId(style: StyleSpecification & { id?: string }): StyleSpecificationWithId {
|
||||
if(!('id' in style) || !style.id) {
|
||||
style.id = generateId();
|
||||
return style as StyleSpecification & { id: string };
|
||||
}
|
||||
return style as StyleSpecification & { id: string };
|
||||
return style as StyleSpecificationWithId;
|
||||
}
|
||||
|
||||
function ensureHasNoInteractive(style: StyleSpecification & {id: string}) {
|
||||
function ensureHasNoInteractive(style: StyleSpecificationWithId) {
|
||||
const changedLayers = style.layers.map(layer => {
|
||||
const changedLayer: LayerSpecification & { interactive?: any } = { ...layer }
|
||||
delete changedLayer.interactive
|
||||
@@ -34,14 +34,14 @@ function ensureHasNoInteractive(style: StyleSpecification & {id: string}) {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureHasNoRefs(style: StyleSpecification & {id: string}) {
|
||||
function ensureHasNoRefs(style: StyleSpecificationWithId) {
|
||||
return {
|
||||
...style,
|
||||
layers: derefLayers(style.layers)
|
||||
}
|
||||
}
|
||||
|
||||
function ensureStyleValidity(style: StyleSpecification): StyleSpecification & { id: string } {
|
||||
function ensureStyleValidity(style: StyleSpecification): StyleSpecificationWithId {
|
||||
return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style)))
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ function replaceSourceAccessToken(mapStyle: StyleSpecification, sourceName: stri
|
||||
// so we need to check the source URL.
|
||||
authSourceName = "stadia"
|
||||
}
|
||||
else if (("url" in source) && source.url?.match(/\.locationiq\.com/)) {
|
||||
authSourceName = "locationiq"
|
||||
}
|
||||
|
||||
const accessToken = getAccessToken(authSourceName, mapStyle, opts)
|
||||
|
||||
@@ -138,6 +141,7 @@ function stripAccessTokens(mapStyle: StyleSpecification) {
|
||||
delete changedMetadata['maputnik:openmaptiles_access_token'];
|
||||
delete changedMetadata['maputnik:thunderforest_access_token'];
|
||||
delete changedMetadata['maputnik:stadia_access_token'];
|
||||
delete changedMetadata['maputnik:locationiq_access_token'];
|
||||
return {
|
||||
...mapStyle,
|
||||
metadata: changedMetadata
|
||||
|
||||
+20
-23
@@ -1,30 +1,27 @@
|
||||
import style from './style'
|
||||
import { StyleSpecificationWithId } from './definitions';
|
||||
|
||||
export function initialStyleUrl() {
|
||||
export function getStyleUrlFromAddressbarAndRemoveItIfNeeded(): string | null {
|
||||
const initialUrl = new URL(window.location.href);
|
||||
return initialUrl.searchParams.get('style');
|
||||
const styleUrl = initialUrl.searchParams.get('style');
|
||||
if (styleUrl) {
|
||||
initialUrl.searchParams.delete('style');
|
||||
window.history.replaceState({}, document.title, initialUrl.toString())
|
||||
}
|
||||
return styleUrl;
|
||||
}
|
||||
|
||||
export function loadStyleUrl(styleUrl: string, cb: (...args: any[]) => void) {
|
||||
export async function loadStyleUrl(styleUrl: string): Promise<StyleSpecificationWithId> {
|
||||
console.log('Loading style', styleUrl)
|
||||
fetch(styleUrl, {
|
||||
mode: 'cors',
|
||||
credentials: "same-origin"
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(body) {
|
||||
cb(style.ensureStyleValidity(body))
|
||||
})
|
||||
.catch(function() {
|
||||
console.warn('Could not fetch default style', styleUrl)
|
||||
cb(style.emptyStyle)
|
||||
})
|
||||
}
|
||||
|
||||
export function removeStyleQuerystring() {
|
||||
const initialUrl = new URL(window.location.href);
|
||||
initialUrl.searchParams.delete('style');
|
||||
window.history.replaceState({}, document.title, initialUrl.toString())
|
||||
try {
|
||||
const response = await fetch(styleUrl, {
|
||||
mode: 'cors',
|
||||
credentials: "same-origin"
|
||||
});
|
||||
const body = await response.json();
|
||||
return style.ensureStyleValidity(body);
|
||||
} catch {
|
||||
console.warn('Could not fetch default style: ' + styleUrl)
|
||||
return style.emptyStyle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,13 @@
|
||||
"Must provide protocol: <1>https://</1>": "Protokoll erforderlich: <1>https://</1>",
|
||||
"Must provide protocol: <1>http://</1> or <3>https://</3>": "Protokoll erforderlich: <1>http://</1> oder <3>https://</3>",
|
||||
"CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain": "Die CORS-Politik erlaubt es nicht, Ressourcen über http von https zu laden, benutze eine <1>https://</1>-Domain",
|
||||
"General layout properties": "Allgemeine Layouteigenschaften",
|
||||
"Text layout properties": "Text-Layouteigenschaften",
|
||||
"Icon layout properties": "Icon-Layouteigenschaften",
|
||||
"Text paint properties": "Text-Darstellungseigenschaften",
|
||||
"Icon paint properties": "Icon-Darstellungseigenschaften",
|
||||
"Paint properties": "Darstellungseigenschaften",
|
||||
"Layout properties": "Layouteigenschaften",
|
||||
"Layer": "Ebene",
|
||||
"JSON Editor": "JSON-Editor",
|
||||
"Delete": "Löschen",
|
||||
@@ -72,7 +79,6 @@
|
||||
"Collapse": "Einklappen",
|
||||
"Expand": "Ausklappen",
|
||||
"Add Layer": "Ebene hinzufügen",
|
||||
"Layer ID already exists": "Layer-ID existiert bereits",
|
||||
"Search": "Suche",
|
||||
"Zoom:": "Zoom:",
|
||||
"Close popup": "Popup schließen",
|
||||
@@ -80,6 +86,7 @@
|
||||
"center:": "Zentrum:",
|
||||
"rotation:": "Rotation:",
|
||||
"Close modal": "Modale Fenster schließen",
|
||||
"Layer ID already exists": "Layer-ID existiert bereits",
|
||||
"Debug": "Debug",
|
||||
"Options": "Optionen",
|
||||
"Save Style": "Stil Speichern",
|
||||
@@ -183,20 +190,15 @@
|
||||
"Public access token for Thunderforest services.": "Öffentlicher Zugriffstoken für Thunderforest-Dienste.",
|
||||
"Stadia Maps API Key": "Stadia Maps API-Schlüssel",
|
||||
"API key for Stadia Maps.": "API-Schlüssel für Stadia Maps.",
|
||||
"LocationIQ Access Token": "LocationIQ Zugriffstoken",
|
||||
"Public access token for LocationIQ services.": "Öffentlicher Zugriffstoken für LocationIQ-Dienste.",
|
||||
"Style Renderer": "Stil-Renderer",
|
||||
"Choose the default Maputnik renderer for this style.": "Wähle den Standard-Renderer für diesen Stil aus.",
|
||||
"Paint properties": "Darstellungseigenschaften",
|
||||
"Opacity": "Deckkraft",
|
||||
"Radius": "Radius",
|
||||
"Text paint properties": "Text-Darstellungseigenschaften",
|
||||
"Icon paint properties": "Icon-Darstellungseigenschaften",
|
||||
"Text layout properties": "Text-Layouteigenschaften",
|
||||
"Icon layout properties": "Icon-Layouteigenschaften",
|
||||
"General layout properties": "Allgemeine Layouteigenschaften",
|
||||
"Color": "Farbe",
|
||||
"Outline color": "Umrissfarbe",
|
||||
"Language": "Sprache",
|
||||
"Layout properties": "Layouteigenschaften",
|
||||
"Width": "Breite",
|
||||
"Height": "Höhe",
|
||||
"Field": "Feld",
|
||||
|
||||
@@ -59,6 +59,13 @@
|
||||
"Must provide protocol: <1>https://</1>": "Protocole requis : <1>https://</1>",
|
||||
"Must provide protocol: <1>http://</1> or <3>https://</3>": "Protocole requis : <1>http://</1> ou <3>https://</3>",
|
||||
"CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain": "La politique CORS ne permet pas de récupérer des ressources servies sur http depuis https, utilisez un protocole <1>https://</1>",
|
||||
"General layout properties": "Propriétés générales de mise en page",
|
||||
"Text layout properties": "Propriétés de mise en page du texte",
|
||||
"Icon layout properties": "Propriétés de mise en page de l'icône",
|
||||
"Text paint properties": "Propriétés de peinture du texte",
|
||||
"Icon paint properties": "Propriétés de peinture de l'icône",
|
||||
"Paint properties": "Propriétés de peinture",
|
||||
"Layout properties": "Propriétés de mise en page",
|
||||
"Layer": "Calque",
|
||||
"JSON Editor": "Éditeur JSON",
|
||||
"Delete": "Supprimer",
|
||||
@@ -72,7 +79,6 @@
|
||||
"Collapse": "Réduire",
|
||||
"Expand": "Développer",
|
||||
"Add Layer": "Ajouter un calque",
|
||||
"Layer ID already exists": "L'identifiant du calque existe déjà",
|
||||
"Search": "Recherche",
|
||||
"Zoom:": "Zoom :",
|
||||
"Close popup": "Fermer la fenêtre",
|
||||
@@ -80,6 +86,7 @@
|
||||
"center:": "centre :",
|
||||
"rotation:": "rotation :",
|
||||
"Close modal": "Fermer la fenêtre modale",
|
||||
"Layer ID already exists": "L'identifiant du calque existe déjà",
|
||||
"Debug": "Déboguer",
|
||||
"Options": "Options",
|
||||
"Save Style": "Enregistrer le style",
|
||||
@@ -183,15 +190,10 @@
|
||||
"Public access token for Thunderforest services.": "Jeton d'accès public pour les services Thunderforest.",
|
||||
"Stadia Maps API Key": "Clé d'API Stadia Maps",
|
||||
"API key for Stadia Maps.": "Clé d'API pour Stadia Maps.",
|
||||
"LocationIQ Access Token": "Jeton d'accès LocationIQ",
|
||||
"Public access token for LocationIQ services.": "Jeton d'accès public pour les services LocationIQ.",
|
||||
"Style Renderer": "Moteur de rendu pour le style",
|
||||
"Choose the default Maputnik renderer for this style.": "Choisissez le moteur de rendu Maputnik par défaut pour ce style.",
|
||||
"Language": "Langue",
|
||||
"Layer options": "Options du calque",
|
||||
"Paint properties": "Propriétés de peinture",
|
||||
"Layout properties": "Propriétés de mise en page",
|
||||
"General layout properties": "Propriétés générales de mise en page",
|
||||
"Text layout properties": "Propriétés de mise en page du texte",
|
||||
"Icon layout properties": "Propriétés de mise en page de l'icône",
|
||||
"Text paint properties": "Propriétés de peinture du texte",
|
||||
"Icon paint properties": "Propriétés de peinture de l'icône"
|
||||
"Layer options": "Options du calque"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,13 @@
|
||||
"Must provide protocol: <1>https://</1>": "נדרש פרוטוקול: <1>https://</1>",
|
||||
"Must provide protocol: <1>http://</1> or <3>https://</3>": "נדרשים פרוטוקולים: <1>http://</1> או <3>https://</3>",
|
||||
"CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain": "CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain",
|
||||
"General layout properties": "תכונות פריסה כלליות",
|
||||
"Text layout properties": "תכונות פריסה של טקסט",
|
||||
"Icon layout properties": "תכונות פריסה של סמליל",
|
||||
"Text paint properties": "תכונות ציור של טקסט",
|
||||
"Icon paint properties": "תכונות ציור של סמליל",
|
||||
"Paint properties": "תכונות ציור",
|
||||
"Layout properties": "תכונות פריסה",
|
||||
"Layer": "שכבה",
|
||||
"JSON Editor": "עורך JSON",
|
||||
"Delete": "מחיקה",
|
||||
@@ -72,7 +79,6 @@
|
||||
"Collapse": "הקטנה",
|
||||
"Expand": "הגדלה",
|
||||
"Add Layer": "הוספת שכבה",
|
||||
"Layer ID already exists": "מזהה השכבה כבר קיים",
|
||||
"Search": "חיפוש",
|
||||
"Zoom:": "זום:",
|
||||
"Close popup": "סגירת החלון",
|
||||
@@ -80,6 +86,7 @@
|
||||
"center:": "מרכז:",
|
||||
"rotation:": "סיבוב:",
|
||||
"Close modal": "סגירת חלונית",
|
||||
"Layer ID already exists": "מזהה השכבה כבר קיים",
|
||||
"Debug": "דיבאג",
|
||||
"Options": "אפשרויות",
|
||||
"Save Style": "שמירת הסטייל",
|
||||
@@ -183,6 +190,8 @@
|
||||
"Public access token for Thunderforest services.": "Public access token for Thunderforest services.",
|
||||
"Stadia Maps API Key": "Stadia Maps API Key",
|
||||
"API key for Stadia Maps.": "API key for Stadia Maps",
|
||||
"LocationIQ Access Token": "LocationIQ Access Token",
|
||||
"Public access token for LocationIQ services.": "Public access token for LocationIQ services.",
|
||||
"Style Renderer": "צייר הסטייל",
|
||||
"Choose the default Maputnik renderer for this style.": "בחירת צייר ברירת המחדל של מפוטניק עבור הסטייל הזה",
|
||||
"Language": "שפה",
|
||||
|
||||
@@ -59,6 +59,13 @@
|
||||
"Must provide protocol: <1>https://</1>": "Protocollo richiesto: <1>https://</1>",
|
||||
"Must provide protocol: <1>http://</1> or <3>https://</3>": "Protocollo richiesto: <1>http://</1> o <3>https://</3>",
|
||||
"CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain": "La politica CORS non permette di caricare risorse tramite http da https, utilizza un dominio <1>https://</1>.",
|
||||
"General layout properties": "Proprietà generali del layout",
|
||||
"Text layout properties": "Proprietà di layout del testo",
|
||||
"Icon layout properties": "Proprietà di layout dell'icona",
|
||||
"Text paint properties": "Proprietà di pittura del testo",
|
||||
"Icon paint properties": "Proprietà di pittura dell'icona",
|
||||
"Paint properties": "Proprietà di pittura",
|
||||
"Layout properties": "Proprietà di layout",
|
||||
"Layer": "Livello",
|
||||
"JSON Editor": "Editor JSON",
|
||||
"Delete": "Elimina",
|
||||
@@ -72,7 +79,6 @@
|
||||
"Collapse": "Coprimi",
|
||||
"Expand": "Espandi",
|
||||
"Add Layer": "Aggiungi Livello",
|
||||
"Layer ID already exists": "L'ID del layer esiste già",
|
||||
"Search": "Cerca",
|
||||
"Zoom:": "Zoom:",
|
||||
"Close popup": "Chiudi popup",
|
||||
@@ -80,6 +86,7 @@
|
||||
"center:": "centro:",
|
||||
"rotation:": "rotazione:",
|
||||
"Close modal": "Chiudi finestra modale",
|
||||
"Layer ID already exists": "L'ID del layer esiste già",
|
||||
"Debug": "Debug",
|
||||
"Options": "Opzioni",
|
||||
"Save Style": "Opzioni stile",
|
||||
@@ -183,6 +190,8 @@
|
||||
"Public access token for Thunderforest services.": "Token di accesso pubblico per i servizi Thunderforest.",
|
||||
"Stadia Maps API Key": "Chiave API di Stadia Maps.",
|
||||
"API key for Stadia Maps.": "Chiave API per Stadia Maps.",
|
||||
"LocationIQ Access Token": "Token di accesso LocationIQ",
|
||||
"Public access token for LocationIQ services.": "Token di accesso pubblico per i servizi LocationIQ.",
|
||||
"Style Renderer": "Renderer dello stile",
|
||||
"Choose the default Maputnik renderer for this style.": "Scegli il renderer predefinito di Maputnik per questo stile.",
|
||||
"Language": "Lingua",
|
||||
|
||||
@@ -59,6 +59,13 @@
|
||||
"Must provide protocol: <1>https://</1>": "プロトコルを指定してください: <1>https://</1>",
|
||||
"Must provide protocol: <1>http://</1> or <3>https://</3>": "プロトコルを指定してください: <1>http://</1> または <3>https://</3>",
|
||||
"CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain": "CORS使用時は、http経由で提供されるリソースをhttpsから取得することはできません。<1>https://</1> ドメインを使用してください。",
|
||||
"General layout properties": "一般レイアウトプロパティ",
|
||||
"Text layout properties": "文字レイアウトプロパティ",
|
||||
"Icon layout properties": "アイコンレイアウトプロパティ",
|
||||
"Text paint properties": "文字ペイントプロパティ",
|
||||
"Icon paint properties": "アイコンペイントプロパティ",
|
||||
"Paint properties": "ペイントプロパティ",
|
||||
"Layout properties": "レイアウトプロパティ",
|
||||
"Layer": "レイヤー",
|
||||
"JSON Editor": "JSONエディタ",
|
||||
"Delete": "削除",
|
||||
@@ -72,7 +79,6 @@
|
||||
"Collapse": "畳む",
|
||||
"Expand": "展開",
|
||||
"Add Layer": "レイヤー追加",
|
||||
"Layer ID already exists": "レイヤーIDは既に存在します",
|
||||
"Search": "検索",
|
||||
"Zoom:": "ズーム:",
|
||||
"Close popup": "ポップアップを閉じる",
|
||||
@@ -80,6 +86,7 @@
|
||||
"center:": "中央:",
|
||||
"rotation:": "回転角度:",
|
||||
"Close modal": "モーダルを閉じる",
|
||||
"Layer ID already exists": "レイヤーIDは既に存在します",
|
||||
"Debug": "デバッグ",
|
||||
"Options": "設定",
|
||||
"Save Style": "スタイルを保存",
|
||||
@@ -183,16 +190,11 @@
|
||||
"Public access token for Thunderforest services.": "Thunderforest サービスの公開用アクセストークン",
|
||||
"Stadia Maps API Key": "Stadia Maps API キー",
|
||||
"API key for Stadia Maps.": "Stadia Maps の API キー",
|
||||
"LocationIQ Access Token": "LocationIQ アクセストークン",
|
||||
"Public access token for LocationIQ services.": "LocationIQ サービス用のパブリックアクセストークン。",
|
||||
"Style Renderer": "スタイルレンダラ",
|
||||
"Choose the default Maputnik renderer for this style.": "このスタイルのデフォルトの Maputnik レンダラを選択してください",
|
||||
"Language": "言語",
|
||||
"<0>Open in OSM</0> — Opens the current view on openstreetmap.org": "現在のビューを <0>openstreetmap.org で開く</0>",
|
||||
"Layer options": "レイヤー設定",
|
||||
"Paint properties": "ペイントプロパティ",
|
||||
"Layout properties": "レイアウトプロパティ",
|
||||
"General layout properties": "一般レイアウトプロパティ",
|
||||
"Text layout properties": "文字レイアウトプロパティ",
|
||||
"Icon layout properties": "アイコンレイアウトプロパティ",
|
||||
"Text paint properties": "文字ペイントプロパティ",
|
||||
"Icon paint properties": "アイコンペイントプロパティ"
|
||||
"Layer options": "レイヤー設定"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,13 @@
|
||||
"Must provide protocol: <1>https://</1>": "必须提供协议:<1>https://</1>",
|
||||
"Must provide protocol: <1>http://</1> or <3>https://</3>": "必须提供协议:<1>http://</1> 或 <3>https://</3>",
|
||||
"CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain": "CORS 策略不允许从 https 访问通过 http 提供的资源,请使用 <1>https://</1> 域名。",
|
||||
"General layout properties": "常规布局属性",
|
||||
"Text layout properties": "文本布局属性",
|
||||
"Icon layout properties": "图标布局属性",
|
||||
"Text paint properties": "文本绘制属性",
|
||||
"Icon paint properties": "图标绘制属性",
|
||||
"Paint properties": "绘制属性",
|
||||
"Layout properties": "布局属性",
|
||||
"Layer": "图层",
|
||||
"JSON Editor": "JSON编辑器",
|
||||
"Delete": "删除",
|
||||
@@ -72,7 +79,6 @@
|
||||
"Collapse": "折叠",
|
||||
"Expand": "展开",
|
||||
"Add Layer": "添加图层",
|
||||
"Layer ID already exists": "图层ID已存在",
|
||||
"Search": "搜索",
|
||||
"Zoom:": "缩放:",
|
||||
"Close popup": "关闭弹出窗口",
|
||||
@@ -80,6 +86,7 @@
|
||||
"center:": "中心:",
|
||||
"rotation:": "旋转:",
|
||||
"Close modal": "关闭模态框",
|
||||
"Layer ID already exists": "图层ID已存在",
|
||||
"Debug": "调试",
|
||||
"Options": "选项",
|
||||
"Save Style": "保存样式",
|
||||
@@ -183,16 +190,11 @@
|
||||
"Public access token for Thunderforest services.": "Thunderforest 服务的公共访问令牌。",
|
||||
"Stadia Maps API Key": "Stadia Maps API 密钥",
|
||||
"API key for Stadia Maps.": "Stadia Maps 的 API 密钥",
|
||||
"LocationIQ Access Token": "LocationIQ 访问令牌",
|
||||
"Public access token for LocationIQ services.": "LocationIQ 服务的公共访问令牌。",
|
||||
"Style Renderer": "样式渲染器",
|
||||
"Choose the default Maputnik renderer for this style.": "为这种样式选择默认的Maputnik渲染器。",
|
||||
"Language": "语言",
|
||||
"<0>Open in OSM</0> — Opens the current view on openstreetmap.org": "在 openstreetmap.org 打开当前视图",
|
||||
"Layer options": "图层选项",
|
||||
"Paint properties": "绘制属性",
|
||||
"Layout properties": "布局属性",
|
||||
"General layout properties": "常规布局属性",
|
||||
"Text layout properties": "文本布局属性",
|
||||
"Icon layout properties": "图标布局属性",
|
||||
"Text paint properties": "文本绘制属性",
|
||||
"Icon paint properties": "图标绘制属性"
|
||||
"Layer options": "图层选项"
|
||||
}
|
||||
|
||||
+4
-3
@@ -3,7 +3,7 @@ import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
import istanbul from "vite-plugin-istanbul";
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ mode }) => ({
|
||||
server: {
|
||||
port: 8888,
|
||||
},
|
||||
@@ -27,7 +27,8 @@ export default defineConfig({
|
||||
forceBuildInstrument: true, //Instrument the source code for cypress runs
|
||||
}),
|
||||
],
|
||||
base: mode === "desktop" ? "/" : "/maputnik/",
|
||||
define: {
|
||||
global: "window",
|
||||
global: "window"
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user