Compare commits

..

2 Commits

Author SHA1 Message Date
HarelM 285dca8d2c Fix lint 2024-02-04 18:07:43 +02:00
HarelM 2a3b6aa770 Improve inspect hover 2024-02-04 18:02:37 +02:00
90 changed files with 7077 additions and 5933 deletions
-11
View File
@@ -1,11 +0,0 @@
## Launch Checklist
<!-- Thanks for the PR! Feel free to add or remove items from the checklist. -->
- [ ] Briefly describe the changes in this PR.
- [ ] Link to related issues.
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
- [ ] Write tests for all new functionality.
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
+18 -12
View File
@@ -54,39 +54,45 @@ jobs:
- run: npm ci
- run: npm run build
- name: artifacts/maputnik
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v1
with:
name: maputnik
path: dist
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
go-version: ^1.19.x
id: go
- name: Build desktop artifacts
run: npm run build-desktop
- name: Check out code into the Go module directory
uses: actions/checkout@v4
with:
repository: maputnik/desktop
ref: master
path: ./src/github.com/maputnik/desktop/
- name: Make
run: cd src/github.com/maputnik/desktop/ && make
- name: Artifacts/linux
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v1
with:
name: maputnik-linux
path: ./desktop/bin/linux/
path: ./src/github.com/maputnik/desktop/bin/linux/
- name: Artifacts/darwin
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v1
with:
name: maputnik-darwin
path: ./desktop/bin/darwin/
path: ./src/github.com/maputnik/desktop/bin/darwin/
- name: Artifacts/windows
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v1
with:
name: maputnik-windows
path: ./desktop/bin/windows/
path: ./src/github.com/maputnik/desktop/bin/windows/
e2e-tests:
name: "E2E tests using ${{ matrix.browser }}"
@@ -1,39 +0,0 @@
name: Create bump version PR
on:
workflow_dispatch:
inputs:
version:
description: Version to change to.
required: true
type: string
jobs:
bump-version-pr:
name: Bump version PR
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Bump version
run: |
npm version --commit-hooks false --git-tag-version false ${{ inputs.version }}
./build/bump-version-changelog.js ${{ inputs.version }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
commit-message: Bump version to ${{ inputs.version }}
branch: bump-version-to-${{ inputs.version }}
title: Bump version to ${{ inputs.version }}
-104
View File
@@ -1,104 +0,0 @@
name: Release
on:
push:
branches: [main]
workflow_dispatch:
jobs:
release-check:
name: Check if version changed
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Check if version has been updated
id: check
uses: EndBug/version-check@v2
outputs:
publish: ${{ steps.check.outputs.changed }}
release:
name: Release
needs: release-check
if: ${{ needs.release-check.outputs.publish == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build
uses: actions/setup-go@v5
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.3.1
- name: Install
run: npm ci
- name: Build
run: |
npm run build
npm run build-desktop
- name: Tag commit and push
id: tag_version
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ steps.package-version.outputs.current-version }}
- name: Create Archives
run: |
zip -r dist dist
zip -r desktop desktop/bin/
- name: Build Release Notes
id: release_notes
run: |
RELEASE_NOTES_PATH="${PWD}/release_notes.txt"
./build/release-notes.js > ${RELEASE_NOTES_PATH}
echo "release_notes=${RELEASE_NOTES_PATH}" >> $GITHUB_OUTPUT
- name: Create GitHub Release
id: create_regular_release
uses: ncipollo/release-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: ${{ steps.tag_version.outputs.new_tag }}
bodyFile: ${{ steps.release_notes.outputs.release_notes }}
artifacts: "dist.zip,desktop.zip"
allowUpdates: true
draft: false
prerelease: false
+1 -1
View File
@@ -33,6 +33,6 @@ node_modules
public
/errorShots
/old
/build
/cypress/screenshots
/dist/
/desktop/version.go
-1
View File
@@ -1 +0,0 @@
legacy-peer-deps = true
-24
View File
@@ -1,24 +0,0 @@
## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
## 2.1.0
### ✨ Features and improvements
- Add GitHub workflows for releasing new versions
- Update desktop build to pull from this repo (#922)
## 2.0.0
- Update MapLibre to version 4 (#872)
- Start continuous deployment of maputnik website
## 1.7.0
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
+2 -2
View File
@@ -2,8 +2,8 @@ FROM node:18 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
COPY package.json package-lock.json ./
RUN npm install
# Build maputnik
COPY . .
+4 -10
View File
@@ -18,7 +18,7 @@ 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:8888 ghcr.io/maplibre/maputnik:main
```
## Documentation
@@ -66,8 +66,7 @@ Lint the JavaScript code.
```
# run linter
npm run lint
npm run lint-css
npm run sort-styles
npm run lint-styles
```
@@ -94,15 +93,10 @@ You can also see the tests as they run or select which suites to run by executin
npm run cy:open
```
## Release process
1. Review [`CHANGELOG.md`](/CHANGELOG.md)
- Double-check that all changes included in the release are appropriately documented.
- To-be-released changes should be under the "main" header.
- Commit any final changes to the changelog.
2. Run [Create bump version PR](https://github.com/maplibre/maputnik/actions/workflows/create-bump-version-pr.yml) by manual workflow dispatch and set the version number in the input. This will create a PR that changes the changelog and `package.json` file to review and merge.
3. Once merged, an automatic process will kick in and creates a GitHub release and uploads release assets.
## Related Projects
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
## Sponsors
-11
View File
@@ -1,11 +0,0 @@
# Build Scripts
This folder holds common build scripts used by some of the Github workflows.
The scripts are borrowed from [maplibre/maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/tree/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build).
## Generate Release Notes
`bump-version-changelog.js` Used to update the changelog with the current notes, and set up a space for new notes
`release-notes.js` Used to generate release notes when releasing a new version
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env node
/**
* This script updates the changelog.md file with the version given in the arguments
* It replaces ## main with ## <version>
* Removes _...Add new stuff here..._
* And adds on top a ## main with add stuff here.
*
* Copied from maplibre/maplibre-gl-js
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
*/
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
let changelog = fs.readFileSync(changelogPath, 'utf8');
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
changelog = `## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
` + changelog;
fs.writeFileSync(changelogPath, changelog, 'utf8');
-48
View File
@@ -1,48 +0,0 @@
#!/usr/bin/env node
// Copied from maplibre/maplibre-gl-js
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
const changelog = fs.readFileSync(changelogPath, 'utf8');
/*
Parse the raw changelog text and split it into individual releases.
This regular expression:
- Matches lines starting with "## x.x.x".
- Groups the version number.
- Skips the (optional) release date.
- Groups the changelog content.
- Ends when another "## x.x.x" is found.
*/
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
let releaseNotes = [];
let match;
// eslint-disable-next-line no-cond-assign
while (match = regex.exec(changelog)) {
releaseNotes.push({
'version': match[1],
'changelog': match[2].trim(),
});
}
const latest = releaseNotes[0];
const previous = releaseNotes[1];
// Print the release notes template.
let header = 'Changes since previous version'
if (previous) {
header = `https://github.com/maplibre/maputnik
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`
}
const templatedReleaseNotes = `${header}
${latest.changelog}
// eslint-disable-next-line eol-last
process.stdout.write(templatedReleaseNotes.trimEnd());
-35
View File
@@ -1,35 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("i18n", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("language detector", () => {
it("English", () => {
const url = "?lng=en";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
});
it("Japanese", () => {
const url = "?lng=ja";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
describe("language switcher", () => {
beforeEach(() => {
when.setStyle("layer");
});
it("the language switcher switches to Japanese", () => {
const selector = "maputnik-lang-select";
then(get.elementByTestId(selector)).shouldExist();
when.select(selector, "ja");
then(get.elementByTestId(selector)).shouldHaveValue("ja");
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
});
});
});
-19
View File
@@ -280,25 +280,6 @@ describe("layers", () => {
});
});
});
describe("opacity", () => {
let bgId: string;
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.type("spec-field-input:background-opacity", "0.");
});
it("should keep '.' in the input field", () => {
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
});
it("should revert to a valid value when focus out", () => {
when.click("layer-list-item:background:" + bgId);
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue('0');
});
});
});
describe("filter", () => {
+2 -6
View File
@@ -1,5 +1,3 @@
/// <reference types="cypress-plugin-tab" />
import { CypressHelper } from "@shellygo/cypress-test-utils";
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
import MaputnikCypressHelper from "./maputnik-cypress-helper";
@@ -16,7 +14,7 @@ const styleFromWindow = (win: Window) => {
export class MaputnikAssertable<T> extends Assertable<T> {
shouldEqualToStoredStyle = () =>
then(
new CypressHelper().get.window().then((win: Window) => {
new CypressHelper().get.window().then((win) => {
const style = styleFromWindow(win);
then(this.chainable).shouldDeepNestedInclude(style);
})
@@ -131,9 +129,7 @@ export class MaputnikDriver {
this.helper.when.acceptConfirm();
}
// when methods should not include assertions
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
toolbarLink.scrollIntoView();
toolbarLink.should("be.visible");
this.helper.get.elementByTestId("toolbar:link").should("be.visible");
},
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
+2 -2
View File
@@ -142,7 +142,7 @@ describe("modals", () => {
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().then((style) => style.metadata)
get.styleFromLocalStorage().pipe((style) => style.metadata)
).shouldInclude({
"maputnik:openmaptiles_access_token": apiKey,
});
@@ -156,7 +156,7 @@ describe("modals", () => {
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().then((style) => style.metadata)
get.styleFromLocalStorage().pipe((style) => style.metadata)
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
});
-31
View File
@@ -1,31 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
editor
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Binary version of pubilic/editor
rice-box.go
# Built binary
maputnik
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2016 Maputnik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-39
View File
@@ -1,39 +0,0 @@
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
BINARY=maputnik
DESKTOP_VERSION := 1.1.1
EDITOR_VERSION := $(shell node -p "require('../package.json').version")
GOPATH := $(if $(GOPATH),$(GOPATH),$(HOME)/go)
GOBIN := $(if $(GOBIN),$(GOBIN),$(HOME)/go/bin)
all: $(BINARY)
$(BINARY): $(GOBIN)/gox $(SOURCES) version.go rice-box.go
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
# Copy the current release into ./editor/maputnik so it can be
# embedded in the binary
editor/pull_release:
mkdir -p editor
cp -r ../dist/* editor
$(GOBIN)/gox:
go install github.com/mitchellh/gox@v1.0.1
$(GOBIN)/rice:
go install github.com/GeertJohan/go.rice/rice@v1.0.3
# Embed the current version numbers in the executable by writing version.go
.PHONY: version.go
version.go:
@echo "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
@echo "package main\n" >> version.go
@echo "const DesktopVersion = \"$(DESKTOP_VERSION)\"" >> version.go
@echo "const EditorVersion = \"$(EDITOR_VERSION)\"" >> version.go
rice-box.go: $(GOBIN)/rice editor/pull_release
$(GOBIN)/rice embed-go
.PHONY: clean
clean:
rm -rf editor && rm -f rice-box.go && rm -rf bin
-72
View File
@@ -1,72 +0,0 @@
# Maputnik Desktop [![GitHub CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci]
---
A Golang based cross platform executable for integrating Maputnik locally.
This binary packages up the JavaScript and CSS bundle produced by maputnik
and embeds it in the program for easy distribution. It also allows
exposing a local style file and work on it both in Maputnik and with your favorite
editor.
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
## Install
You can download a single binary for Linux, OSX or Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/editor/releases/latest).
### Usage
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
```bash
maputnik
```
Expose a local style file to Maputnik allowing the web based editor
to save to the local filesystem.
```bash
maputnik --file basic-v9.json
```
Watch the local style for changes and inform the editor via web socket.
This makes it possible to edit the style with a local text editor and still
use Maputnik.
```bash
maputnik --watch --file basic-v9.json
```
Choose a local port to listen on, instead of using the default port 8000.
```bash
maputnik --port 8001
```
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
Could be used to serve sprites and glyphs.
```bash
maputnik --static ./localFolder
```
### API
`maputnik` exposes the configured styles via a HTTP API.
| Method | Description
|---------------------------------|---------------------------------------
| `GET /styles` | List the ID of all configured style files
| `GET /styles/{filename}` | Get contents of a single style file
| `PUT /styles/{filename}` | Update contents of a style file
| `WEBSOCKET /ws` | Listen to change events for the configured style files
### Build
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
```
npm install
npm run build-desktop
```
You should now find the `maputnik` binary in your `bin` directory.
-81
View File
@@ -1,81 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/gorilla/mux"
)
func StyleFileAccessor(filename string) styleFileAccessor {
return styleFileAccessor{filename, styleId(filename)}
}
func styleId(filename string) string {
raw, err := ioutil.ReadFile(filename)
if err != nil {
log.Panicln(err)
}
var spec styleSpec
err = json.Unmarshal(raw, &spec)
if err != nil {
log.Panicln(err)
}
if spec.Id == "" {
fmt.Println("No id in style")
}
return spec.Id
}
type styleSpec struct {
Id string `json:"id"`
}
// Allows access to a single style file
type styleFileAccessor struct {
filename string
id string
}
func (fa styleFileAccessor) ListFiles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.Encode([]string{fa.id})
}
func (fa styleFileAccessor) ReadFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Choose right file
// right now we just return the single file we know of
w.Header().Set("Content-Type", "application/json")
raw, err := ioutil.ReadFile(fa.filename)
if err != nil {
log.Panicln(err)
}
w.Write(raw)
}
func (fa styleFileAccessor) SaveFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Save to right file
w.Header().Set("Content-Type", "application/json")
body, _ := ioutil.ReadAll(r.Body)
var out bytes.Buffer
json.Indent(&out, body, "", " ")
if err := ioutil.WriteFile(fa.filename, out.Bytes(), 0666); err != nil {
log.Fatalf("Can not copy from request to file: %s", err.Error())
}
}
-69
View File
@@ -1,69 +0,0 @@
package filewatch
import (
"io/ioutil"
"log"
"net/http"
"github.com/fsnotify/fsnotify"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func writer(ws *websocket.Conn, filename string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("Modified file:", event.Name)
var p []byte
var err error
p, err = ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
if p != nil {
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
return
}
}
}
case err := <-watcher.Errors:
log.Println("Watch error:", err)
}
}
}()
if err = watcher.Add(filename); err != nil {
log.Fatal(err)
}
<-done
}
func ServeWebsocketFileWatcher(filename string, w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Println(err)
}
return
}
writer(ws, filename)
defer ws.Close()
}
-27
View File
@@ -1,27 +0,0 @@
module maputnik/desktop
go 1.19
require (
github.com/GeertJohan/go.rice v1.0.3
github.com/fsnotify/fsnotify v1.6.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/maputnik/desktop v1.0.7
github.com/urfave/cli v1.22.12
)
require (
github.com/GeertJohan/go.incremental v1.0.0 // indirect
github.com/akavel/rsrc v0.8.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/daaku/go.zipexe v1.0.2 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/nkovacs/streamquote v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.0.1 // indirect
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
)
-54
View File
@@ -1,54 +0,0 @@
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4=
github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ=
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-80
View File
@@ -1,80 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/GeertJohan/go.rice"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/maputnik/desktop/filewatch"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "maputnik"
app.Usage = "Server for integrating Maputnik locally"
app.Version = "Editor: " + EditorVersion + "; Desktop: " + DesktopVersion
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "file, f",
Usage: "Allow access to JSON style from web client",
},
&cli.BoolFlag{
Name: "watch",
Usage: "Notify web client about JSON style file changes",
},
&cli.IntFlag{
Name: "port",
Value: 8000,
Usage: "TCP port to listen on",
},
&cli.StringFlag{
Name: "static",
Usage: "Serve directory under /static/",
},
}
app.Action = func(c *cli.Context) error {
gui := http.FileServer(rice.MustFindBox("editor").HTTPBox())
router := mux.NewRouter().StrictSlash(true)
filename := c.String("file")
if filename != "" {
fmt.Printf("%s is accessible via Maputnik\n", filename)
// Allow access to reading and writing file on the local system
path, _ := filepath.Abs(filename)
accessor := StyleFileAccessor(path)
router.Path("/styles").Methods("GET").HandlerFunc(accessor.ListFiles)
router.Path("/styles/{styleId}").Methods("GET").HandlerFunc(accessor.ReadFile)
router.Path("/styles/{styleId}").Methods("PUT").HandlerFunc(accessor.SaveFile)
// Register websocket to notify we clients about file changes
if c.Bool("watch") {
router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
filewatch.ServeWebsocketFileWatcher(filename, w, r)
})
}
}
staticDir := c.String("static")
if staticDir != "" {
h := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
router.PathPrefix("/static/").Handler(h)
}
router.PathPrefix("/").Handler(http.StripPrefix("/", gui))
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter)
fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port"))
return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter)
}
app.Run(os.Args)
}
-17
View File
@@ -1,17 +0,0 @@
export default {
output: 'src/locales/$LOCALE/$NAMESPACE.json',
locales: [ 'ja', 'he','zh' ],
// Because some keys are dynamically generated, i18next-parser can't detect them.
// We add these keys manually, so we don't want to remove them.
keepRemoved: true,
// We use plain English keys, so we disable key and namespace separators.
keySeparator: false,
namespaceSeparator: false,
defaultValue: (locale, ns, key) => {
// The default value is a string that indicates that the string is not translated.
return '__STRING_NOT_TRANSLATED__';
}
}
+6359 -3596
View File
File diff suppressed because it is too large Load Diff
+47 -58
View File
@@ -1,19 +1,16 @@
{
"name": "maputnik",
"version": "2.1.0",
"version": "2.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",
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "cypress run",
"cy:open": "cypress open",
"lint-css": "stylelint \"src/styles/*.scss\"",
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
"lint-css": "stylelint \"src/styles/*.scss\""
},
"repository": {
"type": "git",
@@ -24,26 +21,20 @@
"homepage": "https://github.com/maplibre/maputnik#readme",
"dependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@maplibre/maplibre-gl-geocoder": "^1.6.0",
"@maplibre/maplibre-gl-inspect": "^1.6.3",
"@maplibre/maplibre-gl-style-spec": "^20.1.1",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"@maplibre/maplibre-gl-geocoder": "^1.5.0",
"@maplibre/maplibre-gl-style-spec": "^20.1.0",
"@mdi/js": "^6.6.96",
"@mdi/react": "^1.5.0",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^6.16.0",
"array-move": "^4.0.0",
"buffer": "^6.0.3",
"classnames": "^2.5.1",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
"color": "^4.2.3",
"cypress-plugin-tab": "^1.0.5",
"detect-browser": "^5.3.0",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
"json-stringify-pretty-compact": "^3.0.0",
"json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.21",
@@ -53,30 +44,30 @@
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"maplibre-gl": "^4.1.2",
"mapbox-gl-inspect": "^1.3.1",
"maplibre-gl": "^4.0.0",
"maputnik-design": "github:maputnik/design#172b06c",
"ol": "^6.14.1",
"ol-mapbox-style": "^7.1.1",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-accessible-accordion": "^5.0.0",
"react": "^16.0.0",
"react-accessible-accordion": "^4.0.0",
"react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^5.0.2",
"react-aria-modal": "^4.0.1",
"react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-dom": "^16.0.0",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.0.1",
"react-icon-base": "^2.1.2",
"react-icons": "^5.0.1",
"react-icons": "^4.3.1",
"react-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0",
"sass": "^1.72.0",
"slugify": "^1.6.6",
"sass": "^1.50.0",
"slugify": "^1.6.5",
"string-hash": "^1.1.3",
"url": "^0.11.3"
"url": "^0.11.0"
},
"jshintConfig": {
"esversion": 6
@@ -96,15 +87,15 @@
}
},
"devDependencies": {
"@cypress/code-coverage": "^3.12.30",
"@cypress/code-coverage": "^3.12.15",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^5.0.5",
"@shellygo/cypress-test-utils": "^2.1.9",
"@shellygo/cypress-test-utils": "^2.0.17",
"@types/codemirror": "^5.60.15",
"@types/color": "^3.0.6",
"@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7",
"@types/geojson": "^7946.0.14",
"@types/geojson": "^7946.0.13",
"@types/json-to-ast": "^2.1.4",
"@types/lodash.capitalize": "^4.2.9",
"@types/lodash.clamp": "^4.0.9",
@@ -112,38 +103,36 @@
"@types/lodash.get": "^4.4.9",
"@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9",
"@types/mocha": "^10.0.6",
"@types/randomcolor": "^0.5.9",
"@types/react": "^18.2.67",
"@types/react-aria-menubutton": "^6.2.14",
"@types/react-aria-modal": "^4.0.10",
"@types/react-autocomplete": "^1.8.10",
"@types/react": "^16.14.52",
"@types/react-aria-menubutton": "^6.2.13",
"@types/react-aria-modal": "^4.0.9",
"@types/react-autocomplete": "^1.8.9",
"@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.12",
"@types/react-dom": "^18.2.22",
"@types/react-color": "^3.0.10",
"@types/react-dom": "^16.9.24",
"@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6",
"@types/string-hash": "^1.1.3",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react": "^4.2.1",
"@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.0",
"cors": "^2.8.5",
"cypress": "^13.13.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"cypress": "^13.6.1",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"i18next-parser": "^9.0.1",
"eslint-plugin-react-refresh": "^0.4.5",
"express": "^4.17.3",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2",
"mocha": "^10.3.0",
"postcss": "^8.4.38",
"react-hot-loader": "^4.13.1",
"stylelint": "^16.2.1",
"stylelint-config-recommended-scss": "^14.0.0",
"stylelint-scss": "^6.2.1",
"typescript": "^5.4.3",
"uuid": "^9.0.1",
"vite": "^5.2.6",
"vite-plugin-istanbul": "^6.0.0"
"istanbul-lib-coverage": "^3.2.0",
"mocha": "^9.2.2",
"postcss": "^8.4.12",
"react-hot-loader": "^4.13.0",
"stylelint": "^14.6.1",
"stylelint-config-recommended-scss": "^6.0.0",
"stylelint-scss": "^4.2.0",
"typescript": "^5.3.3",
"uuid": "^8.3.2",
"vite": "^5.0.12",
"vite-plugin-istanbul": "^5.0.0"
}
}
+13
View File
@@ -24,6 +24,7 @@ import ModalExport from './ModalExport'
import ModalSources from './ModalSources'
import ModalOpen from './ModalOpen'
import ModalShortcuts from './ModalShortcuts'
import ModalSurvey from './ModalSurvey'
import ModalDebug from './ModalDebug'
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
@@ -127,6 +128,7 @@ type AppState = {
open: boolean
shortcuts: boolean
export: boolean
survey: boolean
debug: boolean
}
}
@@ -135,6 +137,7 @@ export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore;
styleStore: StyleStore | ApiStyleStore;
layerWatcher: LayerWatcher;
shortcutEl: ModalShortcuts | null = null;
constructor(props: any) {
super(props)
@@ -274,6 +277,7 @@ export default class App extends React.Component<any, AppState> {
shortcuts: false,
export: false,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
survey: false,
debug: false,
},
maplibreGlDebugOptions: {
@@ -835,6 +839,10 @@ export default class App extends React.Component<any, AppState> {
}
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
this.setState({
isOpen: {
...this.state.isOpen,
@@ -934,6 +942,7 @@ export default class App extends React.Component<any, AppState> {
mapView={this.state.mapView}
/>
<ModalShortcuts
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>
@@ -961,6 +970,10 @@ export default class App extends React.Component<any, AppState> {
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<ModalSurvey
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div>
return <AppLayout
+12 -18
View File
@@ -1,18 +1,17 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
import { WithTranslation, withTranslation } from 'react-i18next';
type AppLayoutInternalProps = {
type AppLayoutProps = {
toolbar: React.ReactElement
layerList: React.ReactElement
layerEditor?: React.ReactElement
map: React.ReactElement
bottom?: React.ReactElement
modals?: React.ReactNode
} & WithTranslation;
};
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
class AppLayout extends React.Component<AppLayoutProps> {
static childContextTypes = {
reactIconBase: PropTypes.object
}
@@ -24,21 +23,17 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
}
render() {
document.body.dir = this.props.i18n.dir();
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-main">
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
@@ -48,5 +43,4 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
}
}
const AppLayout = withTranslation()(AppLayoutInternal);
export default AppLayout;
export default AppLayout
+7 -12
View File
@@ -1,24 +1,23 @@
import React from 'react'
import {formatLayerId} from '../libs/format';
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
type AppMessagePanelInternalProps = {
type AppMessagePanelProps = {
errors?: unknown[]
infos?: string[]
infos?: unknown[]
mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown
currentLayer?: LayerSpecification
selectedLayerIndex?: number
} & WithTranslation;
};
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const {t, selectedLayerIndex} = this.props;
const {selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
@@ -26,9 +25,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
content = (
<>
<Trans t={t}>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
</Trans>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
@@ -36,7 +33,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
>
{t("switch to layer")}
switch to layer
</button>
</>
}
@@ -62,5 +59,3 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
}
}
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
export default AppMessagePanel;
+21 -51
View File
@@ -2,12 +2,10 @@ import React from 'react'
import classnames from 'classnames'
import {detect} from 'detect-browser';
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdLanguage} from 'react-icons/md'
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage} from 'react-icons/md'
import pkgJson from '../../package.json'
//@ts-ignore
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
import { withTranslation, WithTranslation } from 'react-i18next';
import { supportedLanguages } from '../i18n';
// 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();
@@ -82,7 +80,7 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
type AppToolbarInternalProps = {
type AppToolbarProps = {
mapStyle: object
inspectModeEnabled: boolean
onStyleChanged(...args: unknown[]): unknown
@@ -95,9 +93,9 @@ type AppToolbarInternalProps = {
onSetMapState(mapState: MapState): unknown
mapState?: MapState
renderer?: string
} & WithTranslation;
};
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
export default class AppToolbar extends React.Component<AppToolbarProps> {
state = {
isOpen: {
settings: false,
@@ -112,10 +110,6 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
this.props.onSetMapState(val);
}
handleLanguageChange(val: string) {
this.props.i18n.changeLanguage(val);
}
onSkip = (target: string) => {
if (target === "map") {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
@@ -127,41 +121,40 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
}
render() {
const t = this.props.t;
const views = [
{
id: "map",
group: "general",
title: t("Map"),
title: "Map",
},
{
id: "inspect",
group: "general",
title: t("Inspect"),
title: "Inspect",
disabled: this.props.renderer === 'ol',
},
{
id: "filter-deuteranopia",
group: "color-accessibility",
title: t("Deuteranopia filter"),
title: "Deuteranopia filter",
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-protanopia",
group: "color-accessibility",
title: t("Protanopia filter"),
title: "Protanopia filter",
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-tritanopia",
group: "color-accessibility",
title: t("Tritanopia filter"),
title: "Tritanopia filter",
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-achromatopsia",
group: "color-accessibility",
title: t("Achromatopsia filter"),
title: "Achromatopsia filter",
disabled: !colorAccessibilityFiltersEnabled,
},
];
@@ -181,21 +174,21 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-list")}
>
{t("Layers list")}
Layers list
</button>
<button
data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-editor")}
>
{t("Layer editor")}
Layer editor
</button>
<button
data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("map")}
>
{t("Map view")}
Map view
</button>
<a
className="maputnik-toolbar-logo"
@@ -203,7 +196,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
rel="noreferrer noopener"
href="https://github.com/maplibre/maputnik"
>
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
<img src={maputnikLogo} alt="Maputnik on GitHub" />
<h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
@@ -213,24 +206,24 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser />
<IconText>{t("Open")}</IconText>
<IconText>Open</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>{t("Export")}</IconText>
<IconText>Export</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers />
<IconText>{t("Data Sources")}</IconText>
<IconText>Data Sources</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings />
<IconText>{t("Style Settings")}</IconText>
<IconText>Style Settings</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect">
<MdFindInPage />
<label>{t("View")}
<label>View
<select
className="maputnik-select"
data-wd-key="maputnik-select"
@@ -244,7 +237,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</option>
);
})}
<optgroup label={t("Color accessibility")}>
<optgroup label="Color accessibility">
{views.filter(v => v.group === "color-accessibility").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled}>
@@ -257,35 +250,12 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</label>
</ToolbarSelect>
<ToolbarSelect wdKey="nav:language">
<MdLanguage />
<label>{t("Language")}
<select
className="maputnik-select"
data-wd-key="maputnik-lang-select"
onChange={(e) => this.handleLanguageChange(e.target.value)}
value={this.props.i18n.language}
>
{Object.entries(supportedLanguages).map(([code, name]) => {
return (
<option key={code} value={code}>
{name}
</option>
);
})}
</select>
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
<MdHelpOutline />
<IconText>{t("Help")}</IconText>
<IconText>Help</IconText>
</ToolbarLink>
</div>
</div>
</nav>
}
}
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;
+2 -2
View File
@@ -1,10 +1,10 @@
import React, {PropsWithChildren, SyntheticEvent} from 'react'
import React, {SyntheticEvent} from 'react'
import classnames from 'classnames'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
type BlockProps = PropsWithChildren & {
type BlockProps = {
"data-wd-key"?: string
label?: string
action?: React.ReactElement
+6 -11
View File
@@ -2,23 +2,21 @@ import React from 'react'
import Block from './Block'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldCommentInternalProps = {
type FieldCommentProps = {
value?: string
onChange(value: string | undefined): unknown
error: {message: string}
} & WithTranslation;
};
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
export default class FieldComment extends React.Component<FieldCommentProps> {
render() {
const t = this.props.t;
const fieldSpec = {
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
doc: "Comments for the current layer. This is non-standard and not in the spec."
};
return <Block
label={t("Comments")}
label={"Comments"}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
error={this.props.error}
@@ -27,12 +25,9 @@ class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default={t("Comment...")}
default="Comment..."
data-wd-key="layer-comment.input"
/>
</Block>
}
}
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
type FieldDocLabelProps = {
label: JSX.Element | string | undefined
label: object | string | undefined
fieldSpec?: {
doc?: string
}
+1 -1
View File
@@ -128,7 +128,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
}
}
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
// Because otherwise when editing values we end up accidentally changing field type.
if (state.isEditing) {
return {};
+1 -2
View File
@@ -13,8 +13,7 @@ type FieldIdProps = {
export default class FieldId extends React.Component<FieldIdProps> {
render() {
return <Block label="ID" fieldSpec={latest.layer.id}
return <Block label={"ID"} fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
+4 -9
View File
@@ -3,18 +3,16 @@ import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMaxZoomInternalProps = {
type FieldMaxZoomProps = {
value?: number
onChange(value: number | undefined): unknown
error?: {message: string}
} & WithTranslation;
};
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
render() {
const t = this.props.t;
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
@@ -30,6 +28,3 @@ class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
</Block>
}
}
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
export default FieldMaxZoom;
+4 -9
View File
@@ -3,18 +3,16 @@ import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMinZoomInternalProps = {
type FieldMinZoomProps = {
value?: number
onChange(...args: unknown[]): unknown
error?: {message: string}
} & WithTranslation;
};
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
render() {
const t = this.props.t;
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
@@ -30,6 +28,3 @@ class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
</Block>
}
}
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
export default FieldMinZoom;
+4 -9
View File
@@ -3,26 +3,24 @@ import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceInternalProps = {
type FieldSourceProps = {
value?: string
wdKey?: string
onChange?(value: string| undefined): unknown
sourceIds?: unknown[]
error?: {message: string}
} & WithTranslation;
};
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
export default class FieldSource extends React.Component<FieldSourceProps> {
static defaultProps = {
onChange: () => {},
sourceIds: [],
}
render() {
const t = this.props.t;
return <Block
label={t("Source")}
label={"Source"}
fieldSpec={latest.layer.source}
error={this.props.error}
data-wd-key={this.props.wdKey}
@@ -35,6 +33,3 @@ class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
</Block>
}
}
const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource;
+4 -9
View File
@@ -3,17 +3,16 @@ import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceLayerInternalProps = {
type FieldSourceLayerProps = {
value?: string
onChange?(...args: unknown[]): unknown
sourceLayerIds?: unknown[]
isFixed?: boolean
error?: {message: string}
} & WithTranslation;
};
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
@@ -21,9 +20,8 @@ class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalP
}
render() {
const t = this.props.t;
return <Block
label={t("Source Layer")}
label={"Source Layer"}
fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
error={this.props.error}
@@ -37,6 +35,3 @@ class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalP
</Block>
}
}
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
export default FieldSourceLayer;
+4 -9
View File
@@ -4,24 +4,22 @@ import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputSelect from './InputSelect'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldTypeInternalProps = {
type FieldTypeProps = {
value: string
wdKey?: string
onChange(value: string): unknown
error?: {message: string}
disabled?: boolean
} & WithTranslation;
};
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
export default class FieldType extends React.Component<FieldTypeProps> {
static defaultProps = {
disabled: false,
}
render() {
const t = this.props.t;
return <Block label={t("Type")} fieldSpec={latest.layer.type}
return <Block label={"Type"} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
@@ -52,6 +50,3 @@ class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
</Block>
}
}
const FieldType = withTranslation()(FieldTypeInternal);
export default FieldType;
+2 -2
View File
@@ -1,9 +1,9 @@
import React, { PropsWithChildren, ReactElement } from 'react'
import React, { ReactElement } from 'react'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
import generateUniqueId from '../libs/document-uid';
type FieldsetProps = PropsWithChildren & {
type FieldsetProps = {
label?: string,
fieldSpec?: { doc?: string },
action?: ReactElement,
+18 -27
View File
@@ -13,10 +13,9 @@ import FilterEditorBlock from './FilterEditorBlock'
import InputButton from './InputButton'
import Doc from './Doc'
import ExpressionProperty from './_ExpressionProperty';
import { WithTranslation, withTranslation } from 'react-i18next';
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
const filter = props.filter || ['all'];
if (!Array.isArray(filter)) {
@@ -90,13 +89,13 @@ function hasNestedCombiningFilter(filter: LegacyFilterSpecification | Expression
return false
}
type FilterEditorInternalProps = {
type FilterEditorProps = {
/** Properties of the vector layer and the available fields */
properties?: {[key:string]: any}
filter?: any[]
errors?: {[key:string]: any}
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
} & WithTranslation;
};
type FilterEditorState = {
showDoc: boolean
@@ -104,12 +103,12 @@ type FilterEditorState = {
valueIsSimpleFilter?: boolean
};
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
static defaultProps = {
filter: ["all"],
}
constructor (props: FilterEditorInternalProps) {
constructor (props: FilterEditorProps) {
super(props);
this.state = {
showDoc: false,
@@ -156,17 +155,17 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
})
}
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
// Upgrade but never downgrade
if (!displaySimpleFilter && state.displaySimpleFilter === true) {
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
return {
displaySimpleFilter: false,
valueIsSimpleFilter: false,
};
}
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
return {
valueIsSimpleFilter: true,
}
@@ -179,7 +178,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
}
render() {
const {errors, t} = this.props;
const {errors} = this.props;
const {displaySimpleFilter} = this.state;
const fieldSpec={
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
@@ -191,16 +190,16 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
if (isNestedCombiningFilter) {
return <div className="maputnik-filter-editor-unsupported">
<p>
{t("Nested filters are not supported.")}
Nested filters are not supported.
</p>
<InputButton
onClick={this.makeExpression}
title={t("Convert to expression")}
title="Convert to expression"
>
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
{t("Upgrade to expression")}
Upgrade to expression
</InputButton>
</div>
}
@@ -213,7 +212,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
<div>
<InputButton
onClick={this.makeExpression}
title={t("Convert to expression")}
title="Convert to expression"
className="maputnik-make-zoom-function"
>
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
@@ -248,17 +247,13 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
<Block
key="top"
fieldSpec={fieldSpec}
label={t("Filter")}
label={"Filter"}
action={actions}
>
<InputSelect
value={combiningOp}
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
options={[
["all", t("every filter matches")],
["none", t("no filter matches")],
["any", t("any filter matches")]
]}
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
/>
</Block>
{editorBlocks}
@@ -273,7 +268,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> {t("Add filter")}
</svg> Add filter
</InputButton>
</div>
<div
@@ -304,13 +299,12 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
/>
{this.state.valueIsSimpleFilter &&
<div className="maputnik-expr-infobox">
{t("You've entered an old style filter.")}
{' '}
You&apos;ve entered a old style filter,{' '}
<button
onClick={this.makeFilter}
className="maputnik-expr-infobox__button"
>
{t("Switch to filter editor.")}
switch to filter editor
</button>
</div>
}
@@ -319,6 +313,3 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
}
}
}
const FilterEditor = withTranslation()(FilterEditorInternal);
export default FilterEditor;
+5 -9
View File
@@ -1,21 +1,19 @@
import React, { PropsWithChildren } from 'react'
import React from 'react'
import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
type FilterEditorBlockInternalProps = PropsWithChildren & {
type FilterEditorBlockProps = {
onDelete(...args: unknown[]): unknown
} & WithTranslation;
};
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> {
export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
render() {
const t = this.props.t;
return <div className="maputnik-filter-editor-block">
<div className="maputnik-filter-editor-block-action">
<InputButton
className="maputnik-delete-filter"
onClick={this.props.onDelete}
title={t("Delete filter block")}
title="Delete filter block"
>
<MdDelete />
</InputButton>
@@ -27,5 +25,3 @@ class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInterna
}
}
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
export default FilterEditorBlock;
+1 -1
View File
@@ -32,7 +32,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
};
}
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) {
static getDerivedStateFromProps(props: FieldArrayProps, state: FieldArrayState) {
const value: any[] = [];
const initialPropsValue = state.initialPropsValue.slice(0);
+6 -17
View File
@@ -1,7 +1,6 @@
import React from 'react'
import capitalize from 'lodash.capitalize'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
import InputString from './InputString'
import InputNumber from './InputNumber'
@@ -22,11 +21,10 @@ export type FieldDynamicArrayProps = {
}
'aria-label'?: string
label: string
}
};
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation;
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> {
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
changeValue(idx: number, newValue: string | number | undefined) {
const values = this.values.slice(0)
values[idx] = newValue
@@ -64,13 +62,8 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
}
render() {
const t = this.props.t;
const i18nProps = { t, i18n: this.props.i18n, tReady: this.props.tReady };
const inputs = this.values.map((v, i) => {
const deleteValueBtn= <DeleteValueInputButton
onClick={this.deleteValue.bind(this, i)}
{...i18nProps}
/>;
const deleteValueBtn= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
let input;
if(this.props.type === 'url') {
input = <InputUrl
@@ -124,27 +117,23 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
className="maputnik-array-add-value"
onClick={this.addValue}
>
{t("Add value")}
Add value
</InputButton>
</div>
);
}
}
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
export default FieldDynamicArray;
type DeleteValueInputButtonProps = {
onClick?(...args: unknown[]): unknown
} & WithTranslation;
};
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
render() {
const t = this.props.t;
return <InputButton
className="maputnik-delete-stop"
onClick={this.props.onClick}
title={t("Remove array item")}
title="Remove array item"
>
<FieldDocLabel
label={<MdDelete />}
+3 -11
View File
@@ -1,7 +1,6 @@
import React from 'react'
import classnames from 'classnames';
import CodeMirror, { ModeSpec } from 'codemirror';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import 'codemirror/mode/javascript/javascript'
import 'codemirror/addon/lint/lint'
@@ -28,7 +27,6 @@ export type InputJsonProps = {
mode?: ModeSpec<any>
lint?: boolean | object
};
type InputJsonInternalProps = InputJsonProps & WithTranslation;
type InputJsonState = {
isEditing: boolean
@@ -36,7 +34,7 @@ type InputJsonState = {
prevValue: string
};
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
export default class InputJson extends React.Component<InputJsonProps, InputJsonState> {
static defaultProps = {
lineNumbers: true,
lineWrapping: false,
@@ -54,7 +52,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
_el: HTMLDivElement | null = null;
_cancelNextChange: boolean = false;
constructor(props: InputJsonInternalProps) {
constructor(props: InputJsonProps) {
super(props);
this._keyEvent = "keyboard";
this.state = {
@@ -158,7 +156,6 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
}
render() {
const t = this.props.t;
const {showMessage} = this.state;
const style = {} as {maxHeight?: number};
if (this.props.maxHeight) {
@@ -167,9 +164,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
<Trans t={t}>
Press <kbd>ESC</kbd> to lose focus
</Trans>
Press <kbd>ESC</kbd> to lose focus
</div>
<div
className={classnames("codemirror-container", this.props.className)}
@@ -179,6 +174,3 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
</div>
}
}
const InputJson = withTranslation()(InputJsonInternal);
export default InputJson;
+4 -7
View File
@@ -19,10 +19,7 @@ type InputNumberState = {
editing: boolean
editingRange?: boolean
value?: number
/**
* This is the value that is currently being edited. It can be an invalid value.
*/
dirtyValue?: number | string | undefined
dirtyValue?: number
}
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
@@ -41,7 +38,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}
}
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
static getDerivedStateFromProps(props: InputNumberProps, state: InputNumberState) {
if (!state.editing && props.value !== state.value) {
return {
value: props.value,
@@ -69,7 +66,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}
this.setState({
dirtyValue: newValue === "" ? undefined : newValue,
dirtyValue: newValue === "" ? undefined : value,
})
}
@@ -128,7 +125,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
// for example we might go from 13 to 13.23, however because we know
// that came from a keyboard event we always want to increase by a
// single step value.
if (value < +this.state.dirtyValue!) {
if (value < this.state.dirtyValue!) {
value = this.state.value! - step;
}
else {
-1
View File
@@ -49,7 +49,6 @@ export default class SpecField extends React.Component<SpecFieldProps> {
value: this.props.value,
default: this.props.fieldSpec?.default,
name: this.props.fieldName,
"data-wd-key": "spec-field-input:" + this.props.fieldName,
onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
'aria-label': this.props['aria-label'],
}
+1 -1
View File
@@ -33,7 +33,7 @@ export default class InputString extends React.Component<InputStringProps, Input
}
}
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
static getDerivedStateFromProps(props: InputStringProps, state: InputStringState) {
if (!state.editing) {
return {
value: props.value
+17 -28
View File
@@ -1,10 +1,9 @@
import React from 'react'
import InputString from './InputString'
import SmallError from './SmallError'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
function validate(url: string, t: TFunction): JSX.Element | undefined {
function validate(url: string) {
if (url === "") {
return;
}
@@ -23,19 +22,15 @@ function validate(url: string, t: TFunction): JSX.Element | undefined {
const isSsl = window.location.protocol === "https:";
if (!protocol) {
if (isSsl) {
error = (
<SmallError>
<Trans t={t}>Must provide protocol: <code>https://</code></Trans>
</SmallError>
);
} else {
error = (
<SmallError>
<Trans t={t}>Must provide protocol: <code>http://</code> or <code>https://</code></Trans>
</SmallError>
);
}
error = (
<SmallError>
Must provide protocol {
isSsl
? <code>https://</code>
: <><code>http://</code> or <code>https://</code></>
}
</SmallError>
);
}
else if (
protocol &&
@@ -44,9 +39,7 @@ function validate(url: string, t: TFunction): JSX.Element | undefined {
) {
error = (
<SmallError>
<Trans t={t}>
CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain
</Trans>
CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain
</SmallError>
);
}
@@ -68,34 +61,32 @@ export type FieldUrlProps = {
className?: string
};
type FieldUrlInternalProps = FieldUrlProps & WithTranslation;
type FieldUrlState = {
error?: React.ReactNode
}
class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlState> {
export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlState> {
static defaultProps = {
onInput: () => {},
}
constructor (props: FieldUrlInternalProps) {
constructor (props: FieldUrlProps) {
super(props);
this.state = {
error: validate(props.value, props.t),
error: validate(props.value)
};
}
onInput = (url: string) => {
this.setState({
error: validate(url, this.props.t),
error: validate(url)
});
if (this.props.onInput) this.props.onInput(url);
}
onChange = (url: string) => {
this.setState({
error: validate(url, this.props.t),
error: validate(url)
});
this.props.onChange(url);
}
@@ -115,5 +106,3 @@ class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlSt
}
}
const FieldUrl = withTranslation()(FieldUrlInternal);
export default FieldUrl;
+25 -48
View File
@@ -19,45 +19,31 @@ 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';
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 getLayoutForType(type: LayerSpecification["type"]) {
return layout[type] ? layout[type] : layout.invalid;
}
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
function layoutGroups(layerType: LayerSpecification["type"]): {title: string, type: string, fields?: string[]}[] {
const layerGroup = {
id: 'layer',
title: t('Layer'),
title: 'Layer',
type: 'layer'
}
const filterGroup = {
id: 'filter',
title: t('Filter'),
title: 'Filter',
type: 'filter'
}
const editorGroup = {
id: 'jsoneditor',
title: t('JSON Editor'),
title: 'JSON Editor',
type: 'jsoneditor'
}
return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType, t).groups)
.concat(getLayoutForType(layerType).groups)
.concat([editorGroup])
}
type LayerEditorInternalProps = {
type LayerEditorProps = {
layer: LayerSpecification
sources: {[key: string]: SourceSpecification}
vectorLayers: {[key: string]: any}
@@ -72,14 +58,14 @@ type LayerEditorInternalProps = {
isLastLayer?: boolean
layerIndex: number
errors?: any[]
} & WithTranslation;
};
type LayerEditorState = {
editorGroups: {[keys:string]: boolean}
};
/** Layer editor supporting multiple types of layers. */
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
export default class LayerEditor extends React.Component<LayerEditorProps, LayerEditorState> {
static defaultProps = {
onLayerChanged: () => {},
onLayerIdChange: () => {},
@@ -90,22 +76,22 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
reactIconBase: PropTypes.object
}
constructor(props: LayerEditorInternalProps) {
constructor(props: LayerEditorProps) {
super(props)
//TODO: Clean this up and refactor into function
const editorGroups: {[keys:string]: boolean} = {}
layoutGroups(this.props.layer.type, props.t).forEach(group => {
layoutGroups(this.props.layer.type).forEach(group => {
editorGroups[group.title] = true
})
this.state = { editorGroups }
}
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
static getDerivedStateFromProps(props: LayerEditorProps, state: LayerEditorState) {
const additionalGroups = { ...state.editorGroups }
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
getLayoutForType(props.layer.type).groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
@@ -256,19 +242,17 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
}
render() {
const t = this.props.t;
const groupIds: string[] = [];
const layerType = this.props.layer.type
const groups = layoutGroups(layerType, t).filter(group => {
const groups = layoutGroups(layerType).filter(group => {
return !(layerType === 'background' && group.type === 'source')
}).map(group => {
const groupId = group.id;
const groupId = group.title.replace(/ /g, "_");
groupIds.push(groupId);
return <LayerEditorGroup
data-wd-key={group.title}
id={groupId}
key={groupId}
key={group.title}
title={group.title}
isActive={this.state.editorGroups[group.title]}
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
@@ -281,25 +265,25 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
delete: {
text: t("Delete"),
text: "Delete",
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
},
duplicate: {
text: t("Duplicate"),
text: "Duplicate",
handler: () => this.props.onLayerCopy(this.props.layerIndex)
},
hide: {
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
text: (layout.visibility === "none") ? "Show" : "Hide",
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
},
moveLayerUp: {
text: t("Move layer up"),
text: "Move layer up",
// Not actually used...
disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1)
},
moveLayerDown: {
text: t("Move layer down"),
text: "Move layer down",
// Not actually used...
disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1)
@@ -313,12 +297,12 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
return <section className="maputnik-layer-editor"
role="main"
aria-label={t("Layer editor")}
aria-label="Layer editor"
>
<header>
<div className="layer-header">
<h2 className="layer-header__title">
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
Layer: {formatLayerId(this.props.layer.id)}
</h2>
<div className="layer-header__info">
<Wrapper
@@ -326,11 +310,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
onSelection={handleSelection}
closeOnSelection={false}
>
<Button
id="skip-target-layer-editor"
data-wd-key="skip-target-layer-editor"
className='more-menu__button'
title={"Layer options"}>
<Button id="skip-target-layer-editor" data-wd-key="skip-target-layer-editor" className='more-menu__button' title="Layer options">
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
@@ -360,6 +340,3 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
</section>
}
}
const LayerEditor = withTranslation()(LayerEditorInternal);
export default LayerEditor;
+7 -22
View File
@@ -10,7 +10,6 @@ import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
import type {LayerSpecification} from 'maplibre-gl';
import generateUniqueId from '../libs/document-uid';
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
import { WithTranslation, withTranslation } from 'react-i18next';
type LayerListContainerProps = {
layers: LayerSpecification[]
@@ -23,7 +22,6 @@ type LayerListContainerProps = {
sources: object
errors: any[]
};
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
type LayerListContainerState = {
collapsedGroups: {[ket: string]: boolean}
@@ -33,14 +31,14 @@ type LayerListContainerState = {
};
// List of collapsible layer editors
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
class LayerListContainer extends React.Component<LayerListContainerProps, LayerListContainerState> {
static defaultProps = {
onLayerSelect: () => {},
}
selectedItemRef: React.RefObject<any>;
scrollContainerRef: React.RefObject<HTMLElement>;
constructor(props: LayerListContainerInternalProps) {
constructor(props: LayerListContainerProps) {
super(props);
this.selectedItemRef = React.createRef();
this.scrollContainerRef = React.createRef();
@@ -261,12 +259,10 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
})
})
const t = this.props.t;
return <section
className="maputnik-layer-list"
role="complementary"
aria-label={t("Layers list")}
aria-label="Layers list"
ref={this.scrollContainerRef}
>
<ModalAdd
@@ -278,7 +274,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
onLayersChange={this.props.onLayersChange}
/>
<header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
<span className="maputnik-layer-list-header-title">Layers</span>
<span className="maputnik-space" />
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
@@ -287,11 +283,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
data-wd-key="skip-target-layer-list"
onClick={this.toggleLayers}
className="maputnik-button">
{this.state.areAllGroupsExpanded === true ?
t("Collapse")
:
t("Expand")
}
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
</button>
</div>
</div>
@@ -301,14 +293,14 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
onClick={this.toggleModal.bind(this, 'add')}
data-wd-key="layer-list:add-layer"
className="maputnik-button maputnik-button-selected">
{t("Add Layer")}
Add Layer
</button>
</div>
</div>
</header>
<div
role="navigation"
aria-label={t("Layers list")}
aria-label="Layers list"
>
<ul className="maputnik-layer-list-container">
{listItems}
@@ -318,13 +310,6 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
}
}
// The next two lines have react-refresh/only-export-components disabled because they are
// internal components that are not intended to be used outside of this file.
// For some reason, the linter is not recognizing these components correctly.
// When these components are migrated to functional components, the HOCs will no longer be needed
// and the comments can be removed.
// eslint-disable-next-line react-refresh/only-export-components
const LayerListContainer = withTranslation()(LayerListContainerInternal);
// eslint-disable-next-line react-refresh/only-export-components
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
+1 -1
View File
@@ -139,6 +139,6 @@ class LayerListItem extends React.Component<LayerListItemProps> {
}
}
const LayerListItemSortable = SortableElement<LayerListItemProps>((props: LayerListItemProps) => <LayerListItem {...props} />);
const LayerListItemSortable = SortableElement((props: LayerListItemProps) => <LayerListItem {...props} />);
export default LayerListItemSortable;
+52 -47
View File
@@ -1,8 +1,10 @@
import React, {type JSX} from 'react'
import ReactDOM from 'react-dom'
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'
// @ts-ignore
import MapboxInspect from 'mapbox-gl-inspect'
// @ts-ignore
import colors from 'mapbox-gl-inspect/lib/colors'
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'
import MapMaplibreGlFeaturePropertyPopup, { InspectFeature } from './MapMaplibreGlFeaturePropertyPopup'
import Color from 'color'
@@ -11,13 +13,13 @@ import { HighlightedLayer, colorHighlightedLayer } from '../libs/highlight'
import 'maplibre-gl/dist/maplibre-gl.css'
import '../maplibregl.css'
import '../libs/maplibre-rtl'
import MaplibreGeocoder, { MaplibreGeocoderApi, MaplibreGeocoderApiConfig } from '@maplibre/maplibre-gl-geocoder';
//@ts-ignore
import MaplibreGeocoder from '@maplibre/maplibre-gl-geocoder';
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
import { withTranslation, WithTranslation } from 'react-i18next'
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container) {
ReactDOM.render(popup, mountNode);
return mountNode as HTMLElement;
return mountNode;
}
function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) {
@@ -35,7 +37,6 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
}
const sources: {[key:string]: SourceSpecification} = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster' && source.type !== 'raster-dem') {
@@ -51,7 +52,7 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
return inspectStyle
}
type MapMaplibreGlInternalProps = {
type MapMaplibreGlProps = {
onDataChange?(event: {map: Map | null}): unknown
onLayerSelect(...args: unknown[]): unknown
mapStyle: StyleSpecification
@@ -64,15 +65,15 @@ type MapMaplibreGlInternalProps = {
}
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
onChange(value: {center: LngLat, zoom: number}): unknown
} & WithTranslation;
};
type MapMaplibreGlState = {
map: Map | null
inspect: MaplibreInspect | null
inspect: MapboxInspect | null
zoom?: number
};
class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> {
export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, MapMaplibreGlState> {
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
@@ -82,7 +83,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
}
container: HTMLDivElement | null = null
constructor(props: MapMaplibreGlInternalProps) {
constructor(props: MapMaplibreGlProps) {
super(props)
this.state = {
map: null,
@@ -90,8 +91,18 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
}
}
updateMapFromProps(props: MapMaplibreGlProps) {
if(!this.state.map) return
shouldComponentUpdate(nextProps: MapMaplibreGlInternalProps, nextState: MapMaplibreGlState) {
//Maplibre GL now does diffing natively so we don't need to calculate
//the necessary operations ourselves!
this.state.map.setStyle(
this.props.replaceAccessTokens(props.mapStyle),
{diff: true}
)
}
shouldComponentUpdate(nextProps: MapMaplibreGlProps, nextState: MapMaplibreGlState) {
let should = false;
try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
@@ -104,27 +115,30 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
componentDidUpdate() {
const map = this.state.map;
const styleWithTokens = this.props.replaceAccessTokens(this.props.mapStyle);
this.updateMapFromProps(this.props);
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
// HACK: Fix for <https://github.com/maplibre/maputnik/issues/576>, while we wait for a proper fix.
// eslint-disable-next-line
this.state.inspect._popupBlocked = false;
this.state.inspect.toggleInspector()
}
if (map) {
// Maplibre GL now does diffing natively so we don't need to calculate
// the necessary operations ourselves!
// We also need to update the style for inspect to work properly
map.setStyle(styleWithTokens, {diff: true});
if (this.props.inspectModeEnabled) {
// HACK: We need to work out why we need to do this and what's causing
// this error. I'm assuming an issue with maplibre-gl update and
// mapbox-gl-inspect.
try {
this.state.inspect.render();
} catch(err) {
console.error("FIXME: Caught error", err);
}
}
map.showTileBoundaries = this.props.options?.showTileBoundaries!;
map.showCollisionBoxes = this.props.options?.showCollisionBoxes!;
map.showOverdrawInspector = this.props.options?.showOverdrawInspector!;
}
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
this.state.inspect.toggleInspector()
}
if (this.state.inspect && this.props.inspectModeEnabled) {
this.state.inspect.setOriginalStyle(styleWithTokens);
// In case the sources are the same, there's a need to refresh the style
setTimeout(() => {
this.state.inspect!.render();
}, 500);
}
}
componentDidMount() {
@@ -133,11 +147,8 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
container: this.container!,
style: this.props.mapStyle,
hash: true,
maxZoom: 24,
// setting to always load glyphs of CJK fonts from server
// https://maplibre.org/maplibre-gl-js/docs/examples/local-ideographs/
localIdeographFontFamily: false
} satisfies MapOptions;
maxZoom: 24
}
const map = new MapLibreGl.Map(mapOpts);
@@ -154,7 +165,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
this.initGeocoder(map);
const zoomControl = new ZoomControl(this.props.t("Zoom:"));
const zoomControl = new ZoomControl;
map.addControl(zoomControl, 'top-right');
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
@@ -162,7 +173,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
const tmpNode = document.createElement('div');
const inspect = new MaplibreInspect({
const inspect = new MapboxInspect({
popup: new MapLibreGl.Popup({
closeOnClick: false
}),
@@ -221,7 +232,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
initGeocoder(map: Map) {
const geocoderConfig = {
forwardGeocode: async (config: MaplibreGeocoderApiConfig) => {
forwardGeocode: async (config:{query: string, limit: number, language: string[]}) => {
const features = [];
try {
const request = `https://nominatim.openstreetmap.org/search?q=${config.query}&format=geojson&polygon_geojson=1&addressdetails=1`;
@@ -254,26 +265,20 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
return {
features
};
},
} as unknown as MaplibreGeocoderApi;
const geocoder = new MaplibreGeocoder(geocoderConfig, {
placeholder: this.props.t("Search"),
maplibregl: MapLibreGl,
});
}
};
const geocoder = new MaplibreGeocoder(geocoderConfig, {maplibregl: MapLibreGl});
map.addControl(geocoder, 'top-left');
}
render() {
const t = this.props.t;
return <div
className="maputnik-map__map"
role="region"
aria-label={t("Map view")}
aria-label="Map view"
ref={x => this.container = x}
data-wd-key="maplibre:map"
></div>
}
}
const MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
export default MapMaplibreGl;
@@ -1,12 +1,16 @@
import React from 'react'
import type { GeoJSONFeatureWithSourceLayer } from '@maplibre/maplibre-gl-inspect'
export type InspectFeature = GeoJSONFeatureWithSourceLayer & {
export type InspectFeature = {
id: string
properties: {[key:string]: any}
layer: {[key:string]: any}
geometry: GeoJSON.Geometry
sourceLayer: string
inspectModeCounter?: number
counter?: number
}
function displayValue(value: string | number | Date | object | undefined) {
function displayValue(value: string | number | Date | object) {
if (typeof value === 'undefined' || value === null) return value;
if (value instanceof Date) return value.toLocaleString();
if (typeof value === 'object' ||
@@ -15,25 +19,25 @@ function displayValue(value: string | number | Date | object | undefined) {
return value;
}
function renderKeyValueTableRow(key: string, value: string | undefined) {
return <tr key={key}>
<td className="maputnik-popup-table-cell">{key}</td>
<td className="maputnik-popup-table-cell">{value}</td>
</tr>
}
function renderFeature(feature: InspectFeature, idx: number) {
return <React.Fragment key={idx}>
<tr>
<td colSpan={2} className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</td>
</tr>
{renderKeyValueTableRow("$type", feature.geometry.type)}
{renderKeyValueTableRow("$id", displayValue(feature.id))}
return <div key={`${feature.sourceLayer}-${idx}`}>
<div className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
<div className="maputnik-popup-layer">
<div className="maputnik-popup-feature-left">$type</div>
<div className="maputnik-popup-feature-right">{feature.geometry.type}</div>
</div>
<div className="maputnik-popup-layer">
<div className="maputnik-popup-feature-left">feature_id</div>
<div className="maputnik-popup-feature-right">{displayValue(feature.id)}</div>
</div>
{Object.keys(feature.properties).map(propertyName => {
const property = feature.properties[propertyName];
return renderKeyValueTableRow(propertyName, displayValue(property))
return <div key={propertyName} className="maputnik-popup-layer">
<div className="maputnik-popup-feature-left">{propertyName}</div>
<div className="maputnik-popup-feature-right">{displayValue(property)}</div>
</div>
})}
</React.Fragment>
</div>
}
function removeDuplicatedFeatures(features: InspectFeature[]) {
@@ -67,11 +71,7 @@ class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
render() {
const features = removeDuplicatedFeatures(this.props.features)
return <div className="maputnik-feature-property-popup">
<table className="maputnik-popup-table">
<tbody>
{features.map(renderFeature)}
</tbody>
</table>
{features.map(renderFeature)}
</div>
}
}
+21 -14
View File
@@ -8,16 +8,15 @@ function groupFeaturesBySourceLayer(features: InspectFeature[]) {
const returnedFeatures: {[key: string]: number} = {}
features.forEach(feature => {
const sourceKey = feature.layer['source-layer'] as string;
if(Object.prototype.hasOwnProperty.call(returnedFeatures, feature.layer.id)) {
returnedFeatures[feature.layer.id]++
const featureObject = sources[sourceKey].find((f: InspectFeature) => f.layer.id === feature.layer.id)
const featureObject = sources[feature.layer['source-layer']].find((f: InspectFeature) => f.layer.id === feature.layer.id)
featureObject!.counter = returnedFeatures[feature.layer.id]
} else {
sources[sourceKey] = sources[sourceKey] || []
sources[sourceKey].push(feature)
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
sources[feature.layer['source-layer']].push(feature)
returnedFeatures[feature.layer.id] = 1
}
@@ -41,21 +40,29 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
try {
const paintProps = feature.layer.paint;
let propName;
if("text-color" in paintProps && paintProps["text-color"]) {
return String(paintProps["text-color"]);
if(Object.prototype.hasOwnProperty.call(paintProps, "text-color") && paintProps["text-color"]) {
propName = "text-color";
}
if ("fill-color" in paintProps && paintProps["fill-color"]) {
return String(paintProps["fill-color"]);
else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-color") && paintProps["fill-color"]) {
propName = "fill-color";
}
if ("line-color" in paintProps && paintProps["line-color"]) {
return String(paintProps["line-color"]);
else if (Object.prototype.hasOwnProperty.call(paintProps, "line-color") && paintProps["line-color"]) {
propName = "line-color";
}
if ("fill-extrusion-color" in paintProps && paintProps["fill-extrusion-color"]) {
return String(paintProps["fill-extrusion-color"]);
else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
propName = "fill-extrusion-color";
}
if(propName) {
const color = feature.layer.paint[propName];
return String(color);
}
else {
// Default color
return "black";
}
// Default color
return "black";
}
// This is quite complex, just incase there's an edgecase we're missing
// always return black if we get an unexpected error.
+11 -16
View File
@@ -1,11 +1,9 @@
import React from 'react'
import {throttle} from 'lodash';
import { WithTranslation, withTranslation } from 'react-i18next';
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup';
import 'ol/ol.css'
//@ts-ignore
import {apply} from 'ol-mapbox-style';
import {Map, View, Overlay} from 'ol';
@@ -24,7 +22,7 @@ function renderCoords (coords: string[]) {
}
}
type MapOpenLayersInternalProps = {
type MapOpenLayersProps = {
onDataChange?(...args: unknown[]): unknown
mapStyle: object
accessToken?: string
@@ -33,7 +31,7 @@ type MapOpenLayersInternalProps = {
debugToolbox: boolean
replaceAccessTokens(...args: unknown[]): unknown
onChange(...args: unknown[]): unknown
} & WithTranslation;
};
type MapOpenLayersState = {
zoom: string
@@ -43,7 +41,7 @@ type MapOpenLayersState = {
selectedFeatures?: any[]
};
class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps, MapOpenLayersState> {
export default class MapOpenLayers extends React.Component<MapOpenLayersProps, MapOpenLayersState> {
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
@@ -55,7 +53,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
overlay: Overlay | undefined;
popupContainer: HTMLElement | null = null;
constructor(props: MapOpenLayersInternalProps) {
constructor(props: MapOpenLayersProps) {
super(props);
this.state = {
zoom: "0",
@@ -74,7 +72,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
apply(this.map, newMapStyle);
}
componentDidUpdate(prevProps: MapOpenLayersInternalProps) {
componentDidUpdate(prevProps: MapOpenLayersProps) {
if (this.props.mapStyle !== prevProps.mapStyle) {
this.updateStyle(
this.props.replaceAccessTokens(this.props.mapStyle)
@@ -152,7 +150,6 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
}
render() {
const t = this.props.t;
return <div className="maputnik-ol-container">
<div
ref={x => this.popupContainer = x}
@@ -162,7 +159,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
<button
className="maplibregl-popup-close-button"
onClick={this.closeOverlay}
aria-label={t("Close popup")}
aria-label="Close popup"
>
×
</button>
@@ -172,20 +169,20 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
/>
</div>
<div className="maputnik-ol-zoom">
{t("Zoom:")} {this.state.zoom}
Zoom: {this.state.zoom}
</div>
{this.props.debugToolbox &&
<div className="maputnik-ol-debug">
<div>
<label>{t("cursor:")} </label>
<label>cursor: </label>
<span>{renderCoords(this.state.cursor)}</span>
</div>
<div>
<label>{t("center:")} </label>
<label>center: </label>
<span>{renderCoords(this.state.center)}</span>
</div>
<div>
<label>{t("rotation:")} </label>
<label>rotation: </label>
<span>{this.state.rotation}</span>
</div>
</div>
@@ -194,7 +191,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
className="maputnik-ol"
ref={x => this.container = x}
role="region"
aria-label={t("Map view")}
aria-label="Map view"
style={{
...this.props.style,
}}>
@@ -203,5 +200,3 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
}
}
const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
export default MapOpenLayers;
+6 -9
View File
@@ -1,10 +1,10 @@
import React, { PropsWithChildren } from 'react'
import React from 'react'
import {MdClose} from 'react-icons/md'
import AriaModal from 'react-aria-modal'
import classnames from 'classnames';
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalInternalProps = PropsWithChildren & {
type ModalProps = {
"data-wd-key"?: string
isOpen: boolean
title: string
@@ -12,10 +12,10 @@ type ModalInternalProps = PropsWithChildren & {
underlayClickExits?: boolean
underlayProps?: any
className?: string
} & WithTranslation;
};
class ModalInternal extends React.Component<ModalInternalProps> {
export default class Modal extends React.Component<ModalProps> {
static defaultProps = {
underlayClickExits: true
}
@@ -32,7 +32,6 @@ class ModalInternal extends React.Component<ModalInternalProps> {
}
render() {
const t = this.props.t;
if(this.props.isOpen) {
return <AriaModal
titleText={this.props.title}
@@ -50,7 +49,7 @@ class ModalInternal extends React.Component<ModalInternalProps> {
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
<span className="maputnik-modal-header-space"></span>
<button className="maputnik-modal-header-toggle"
title={t("Close modal")}
title="Close modal"
onClick={this.onClose}
data-wd-key={this.props["data-wd-key"]+".close-modal"}
>
@@ -69,5 +68,3 @@ class ModalInternal extends React.Component<ModalInternalProps> {
}
}
const Modal = withTranslation()(ModalInternal);
export default Modal;
+8 -12
View File
@@ -7,16 +7,15 @@ import FieldId from './FieldId'
import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer'
import type {LayerSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalAddInternalProps = {
type ModalAddProps = {
layers: LayerSpecification[]
onLayersChange(layers: LayerSpecification[]): unknown
isOpen: boolean
onOpenToggle(open: boolean): unknown
// A dict of source id's and the available source layers
sources: any
} & WithTranslation;
};
type ModalAddState = {
type: LayerSpecification["type"]
@@ -25,7 +24,7 @@ type ModalAddState = {
'source-layer'?: string
};
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
export default class ModalAdd extends React.Component<ModalAddProps, ModalAddState> {
addLayer = () => {
const changedLayers = this.props.layers.slice(0)
const layer: ModalAddState = {
@@ -46,7 +45,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
this.props.onOpenToggle(false)
}
constructor(props: ModalAddInternalProps) {
constructor(props: ModalAddProps) {
super(props)
const state: ModalAddState = {
type: 'fill',
@@ -55,12 +54,12 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
if(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]
state['source-layer'] = this.props.sources[state.source as keyof ModalAddProps["sources"]][0]
}
this.state = state;
}
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
componentDidUpdate(_prevProps: ModalAddProps, prevState: ModalAddState) {
// Check if source is valid for new type
const oldType = prevState.type;
const newType = this.state.type;
@@ -126,14 +125,13 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
render() {
const t = this.props.t;
const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source!);
return <Modal
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t('Add Layer')}
title={'Add Layer'}
data-wd-key="modal:add-layer"
className="maputnik-add-modal"
>
@@ -171,12 +169,10 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
onClick={this.addLayer}
data-wd-key="add-layer"
>
{t("Add Layer")}
Add Layer
</InputButton>
</div>
</Modal>
}
}
const ModalAdd = withTranslation()(ModalAddInternal);
export default ModalAdd;
+13 -18
View File
@@ -1,10 +1,9 @@
import React from 'react'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal'
type ModalDebugInternalProps = {
type ModalDebugProps = {
isOpen: boolean
renderer: string
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
@@ -19,12 +18,12 @@ type ModalDebugInternalProps = {
lat: number
}
}
} & WithTranslation;
};
class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
export default class ModalDebug extends React.Component<ModalDebugProps> {
render() {
const {t, mapView} = this.props;
const {mapView} = this.props;
const osmZoom = Math.round(mapView.zoom)+1;
const osmLon = +(mapView.center.lng).toFixed(5);
@@ -34,10 +33,10 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
data-wd-key="modal:debug"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t('Debug')}
title={'Debug'}
>
<section className="maputnik-modal-section maputnik-modal-shortcuts">
<h1>{t("Options")}</h1>
<h1>Options</h1>
{this.props.renderer === 'mlgljs' &&
<ul>
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
@@ -64,20 +63,16 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
<section className="maputnik-modal-section">
<h1>Links</h1>
<p>
<Trans t={t}>
<a
target="_blank"
rel="noopener noreferrer"
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
>
Open in OSM
</a> &mdash; Opens the current view on openstreetmap.org
</Trans>
<a
target="_blank"
rel="noopener noreferrer"
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
>
Open in OSM
</a> &mdash; Opens the current view on openstreetmap.org
</p>
</section>
</Modal>
}
}
const ModalDebug = withTranslation()(ModalDebugInternal);
export default ModalDebug;
+13 -18
View File
@@ -5,7 +5,6 @@ import {version} from 'maplibre-gl/package.json'
import {format} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification} from 'maplibre-gl'
import {MdFileDownload} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
import FieldString from './FieldString'
import InputButton from './InputButton'
@@ -17,15 +16,15 @@ import fieldSpecAdditional from '../libs/field-spec-additional'
const MAPLIBRE_GL_VERSION = version;
type ModalExportInternalProps = {
type ModalExportProps = {
mapStyle: StyleSpecification & { id: string }
onStyleChanged(...args: unknown[]): unknown
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
} & WithTranslation;
};
class ModalExportInternal extends React.Component<ModalExportInternalProps> {
export default class ModalExport extends React.Component<ModalExportProps> {
tokenizedStyle () {
return format(
@@ -49,7 +48,7 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
downloadHtml() {
const tokenStyle = this.tokenizedStyle();
const htmlTitle = this.props.mapStyle.name || this.props.t("Map");
const htmlTitle = this.props.mapStyle.name || "Map";
const html = `<!DOCTYPE html>
<html>
<head>
@@ -101,32 +100,30 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
render() {
const t = this.props.t;
const fsa = fieldSpecAdditional(t);
return <Modal
data-wd-key="modal:export"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t('Export Style')}
title={'Export Style'}
className="maputnik-export-modal"
>
<section className="maputnik-modal-section">
<h1>{t("Download Style")}</h1>
<h1>Download Style</h1>
<p>
{t("Download a JSON style to your computer.")}
Download a JSON style to your computer.
</p>
<div>
<FieldString
label={fsa.maputnik.maptiler_access_token.label}
fieldSpec={fsa.maputnik.maptiler_access_token}
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/>
<FieldString
label={fsa.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token}
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:thunderforest_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
@@ -137,14 +134,14 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
onClick={this.downloadStyle.bind(this)}
>
<MdFileDownload />
{t("Download Style")}
Download Style
</InputButton>
<InputButton
onClick={this.downloadHtml.bind(this)}
>
<MdFileDownload />
{t("Download HTML")}
Download HTML
</InputButton>
</div>
</section>
@@ -153,5 +150,3 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
}
}
const ModalExport = withTranslation()(ModalExportInternal);
export default ModalExport;
+4 -8
View File
@@ -2,25 +2,23 @@ import React from 'react'
import InputButton from './InputButton'
import Modal from './Modal'
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalLoadingInternalProps = {
type ModalLoadingProps = {
isOpen: boolean
onCancel(...args: unknown[]): unknown
title: string
message: React.ReactNode
} & WithTranslation;
};
class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
export default class ModalLoading extends React.Component<ModalLoadingProps> {
underlayOnClick(e: Event) {
// This stops click events falling through to underlying modals.
e.stopPropagation();
}
render() {
const t = this.props.t;
return <Modal
data-wd-key="modal:loading"
isOpen={this.props.isOpen}
@@ -37,12 +35,10 @@ class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
</p>
<p className="maputnik-dialog__buttons">
<InputButton onClick={(e) => this.props.onCancel(e)}>
{t("Cancel")}
Cancel
</InputButton>
</p>
</Modal>
}
}
const ModalLoading = withTranslation()(ModalLoadingInternal);
export default ModalLoading;
+17 -23
View File
@@ -2,7 +2,6 @@ import React, { FormEvent } from 'react'
import {MdFileUpload} from 'react-icons/md'
import {MdAddCircleOutline} from 'react-icons/md'
import FileReaderInput, { Result } from 'react-file-reader-input'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import ModalLoading from './ModalLoading'
import Modal from './Modal'
@@ -43,11 +42,11 @@ class PublicStyle extends React.Component<PublicStyleProps> {
}
}
type ModalOpenInternalProps = {
type ModalOpenProps = {
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
onStyleOpen(...args: unknown[]): unknown
} & WithTranslation;
};
type ModalOpenState = {
styleUrl: string
@@ -56,8 +55,8 @@ type ModalOpenState = {
activeRequestUrl?: string | null
};
class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> {
constructor(props: ModalOpenInternalProps) {
export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpenState> {
constructor(props: ModalOpenProps) {
super(props);
this.state = {
styleUrl: ""
@@ -175,7 +174,6 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
}
render() {
const t = this.props.t;
const styleOptions = publicStyles.map(style => {
return <PublicStyle
key={style.id}
@@ -202,31 +200,29 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
data-wd-key="modal:open"
isOpen={this.props.isOpen}
onOpenToggle={() => this.onOpenToggle()}
title={t('Open Style')}
title={'Open Style'}
>
{errorElement}
<section className="maputnik-modal-section">
<h1>{t("Upload Style")}</h1>
<p>{t("Upload a JSON style from your computer.")}</p>
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label={t("Style file")}>
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Upload")}</InputButton>
<h1>Upload Style</h1>
<p>Upload a JSON style from your computer.</p>
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label="Style file">
<InputButton className="maputnik-upload-button"><MdFileUpload /> Upload</InputButton>
</FileReaderInput>
</section>
<section className="maputnik-modal-section">
<form onSubmit={this.onSubmitUrl}>
<h1>{t("Load from URL")}</h1>
<h1>Load from URL</h1>
<p>
<Trans t={t}>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
</Trans>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
</p>
<InputUrl
aria-label={t("Style URL")}
aria-label="Style URL"
data-wd-key="modal:open.url.input"
type="text"
className="maputnik-input"
default={t("Enter URL...")}
default="Enter URL..."
value={this.state.styleUrl}
onInput={this.onChangeUrl}
onChange={this.onChangeUrl}
@@ -243,9 +239,9 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
</section>
<section className="maputnik-modal-section maputnik-modal-section--shrink">
<h1>{t("Gallery Styles")}</h1>
<h1>Gallery Styles</h1>
<p>
{t("Open one of the publicly available styles to start from.")}
Open one of the publicly available styles to start from.
</p>
<div className="maputnik-style-gallery-container">
{styleOptions}
@@ -255,14 +251,12 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
<ModalLoading
isOpen={!!this.state.activeRequest}
title={t('Loading style')}
title={'Loading style'}
onCancel={(e: Event) => this.onCancelActiveRequest(e)}
message={t("Loading: {{requestUrl}}", { requestUrl: this.state.activeRequestUrl })}
message={"Loading: "+this.state.activeRequestUrl}
/>
</div>
)
}
}
const ModalOpen = withTranslation()(ModalOpenInternal);
export default ModalOpen;
+29 -33
View File
@@ -1,7 +1,6 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import type {LightSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
import FieldArray from './FieldArray'
import FieldNumber from './FieldNumber'
@@ -13,15 +12,15 @@ import FieldColor from './FieldColor'
import Modal from './Modal'
import fieldSpecAdditional from '../libs/field-spec-additional'
type ModalSettingsInternalProps = {
type ModalSettingsProps = {
mapStyle: StyleSpecification
onStyleChanged(...args: unknown[]): unknown
onChangeMetadataProperty(...args: unknown[]): unknown
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
} & WithTranslation;
};
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
export default class ModalSettings extends React.Component<ModalSettingsProps> {
changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
const transition = {
...this.props.mapStyle.transition,
@@ -96,8 +95,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
render() {
const metadata = this.props.mapStyle.metadata || {} as any;
const {t, onChangeMetadataProperty, mapStyle} = this.props;
const fsa = fieldSpecAdditional(t);
const {onChangeMetadataProperty, mapStyle} = this.props;
const light = this.props.mapStyle.light || {};
const transition = this.props.mapStyle.transition || {};
@@ -107,33 +105,33 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
data-wd-key="modal:settings"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t('Style Settings')}
title={'Style Settings'}
>
<div className="modal:settings">
<FieldString
label={t("Name")}
label={"Name"}
fieldSpec={latest.$root.name}
data-wd-key="modal:settings.name"
value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")}
/>
<FieldString
label={t("Owner")}
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
label={"Owner"}
fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}
data-wd-key="modal:settings.owner"
value={(this.props.mapStyle as any).owner}
onChange={this.changeStyleProperty.bind(this, "owner")}
/>
<FieldUrl
fieldSpec={latest.$root.sprite}
label={t("Sprite URL")}
label="Sprite URL"
data-wd-key="modal:settings.sprite"
value={this.props.mapStyle.sprite as string}
onChange={this.changeStyleProperty.bind(this, "sprite")}
/>
<FieldUrl
label={t("Glyphs URL")}
label="Glyphs URL"
fieldSpec={latest.$root.glyphs}
data-wd-key="modal:settings.glyphs"
value={this.props.mapStyle.glyphs as string}
@@ -141,23 +139,23 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldString
label={fsa.maputnik.maptiler_access_token.label}
fieldSpec={fsa.maputnik.maptiler_access_token}
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
value={metadata['maputnik:openmaptiles_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/>
<FieldString
label={fsa.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token}
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
data-wd-key="modal:settings.maputnik:thunderforest_access_token"
value={metadata['maputnik:thunderforest_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
<FieldArray
label={t("Center")}
label={"Center"}
fieldSpec={latest.$root.center}
length={2}
type="number"
@@ -167,7 +165,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldNumber
label={t("Zoom")}
label={"Zoom"}
fieldSpec={latest.$root.zoom}
value={mapStyle.zoom}
default={0}
@@ -175,7 +173,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldNumber
label={t("Bearing")}
label={"Bearing"}
fieldSpec={latest.$root.bearing}
value={mapStyle.bearing}
default={latest.$root.bearing.default}
@@ -183,7 +181,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldNumber
label={t("Pitch")}
label={"Pitch"}
fieldSpec={latest.$root.pitch}
value={mapStyle.pitch}
default={latest.$root.pitch.default}
@@ -191,7 +189,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldEnum
label={t("Light anchor")}
label={"Light anchor"}
fieldSpec={latest.light.anchor}
name="light-anchor"
value={light.anchor as string}
@@ -201,7 +199,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldColor
label={t("Light color")}
label={"Light color"}
fieldSpec={latest.light.color}
value={light.color as string}
default={latest.light.color.default}
@@ -209,7 +207,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldNumber
label={t("Light intensity")}
label={"Light intensity"}
fieldSpec={latest.light.intensity}
value={light.intensity as number}
default={latest.light.intensity.default}
@@ -217,7 +215,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldArray
label={t("Light position")}
label={"Light position"}
fieldSpec={latest.light.position}
type="number"
length={latest.light.position.length}
@@ -227,7 +225,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldString
label={t("Terrain source")}
label={"Terrain source"}
fieldSpec={latest.terrain.source}
data-wd-key="modal:settings.maputnik:terrain_source"
value={terrain.source}
@@ -235,7 +233,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldNumber
label={t("Terrain exaggeration")}
label={"Terrain exaggeration"}
fieldSpec={latest.terrain.exaggeration}
value={terrain.exaggeration}
default={latest.terrain.exaggeration.default}
@@ -243,7 +241,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldNumber
label={t("Transition delay")}
label={"Transition delay"}
fieldSpec={latest.transition.delay}
value={transition.delay}
default={latest.transition.delay.default}
@@ -251,7 +249,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldNumber
label={t("Transition duration")}
label={"Transition duration"}
fieldSpec={latest.transition.duration}
value={transition.duration}
default={latest.transition.duration.default}
@@ -259,12 +257,12 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/>
<FieldSelect
label={fsa.maputnik.style_renderer.label}
fieldSpec={fsa.maputnik.style_renderer}
label={fieldSpecAdditional.maputnik.style_renderer.label}
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
data-wd-key="modal:settings.maputnik:renderer"
options={[
['mlgljs', 'MapLibreGL JS'],
['ol', t('Open Layers (experimental)')],
['ol', 'Open Layers (experimental)'],
]}
value={metadata['maputnik:renderer'] || 'mlgljs'}
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')}
@@ -274,5 +272,3 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
}
}
const ModalSettings = withTranslation()(ModalSettingsInternal)
export default ModalSettings;
+26 -32
View File
@@ -1,50 +1,48 @@
import React from 'react'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal'
type ModalShortcutsInternalProps = {
type ModalShortcutsProps = {
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
} & WithTranslation;
};
class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps> {
export default class ModalShortcuts extends React.Component<ModalShortcutsProps> {
render() {
const t = this.props.t;
const help = [
{
key: <kbd>?</kbd>,
text: t("Shortcuts menu")
text: "Shortcuts menu"
},
{
key: <kbd>o</kbd>,
text: t("Open modal")
text: "Open modal"
},
{
key: <kbd>e</kbd>,
text: t("Export modal")
text: "Export modal"
},
{
key: <kbd>d</kbd>,
text: t("Data Sources modal")
text: "Data Sources modal"
},
{
key: <kbd>s</kbd>,
text: t("Style Settings modal")
text: "Style Settings modal"
},
{
key: <kbd>i</kbd>,
text: t("Toggle inspect")
text: "Toggle inspect"
},
{
key: <kbd>m</kbd>,
text: t("Focus map")
text: "Focus map"
},
{
key: <kbd>!</kbd>,
text: t("Debug modal")
text: "Debug modal"
},
]
@@ -52,51 +50,51 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
const mapShortcuts = [
{
key: <kbd>+</kbd>,
text: t("Increase the zoom level by 1.",)
text: "Increase the zoom level by 1.",
},
{
key: <><kbd>Shift</kbd> + <kbd>+</kbd></>,
text: t("Increase the zoom level by 2.",)
text: "Increase the zoom level by 2.",
},
{
key: <kbd>-</kbd>,
text: t("Decrease the zoom level by 1.",)
text: "Decrease the zoom level by 1.",
},
{
key: <><kbd>Shift</kbd> + <kbd>-</kbd></>,
text: t("Decrease the zoom level by 2.",)
text: "Decrease the zoom level by 2.",
},
{
key: <kbd>Up</kbd>,
text: t("Pan up by 100 pixels.",)
text: "Pan up by 100 pixels.",
},
{
key: <kbd>Down</kbd>,
text: t("Pan down by 100 pixels.",)
text: "Pan down by 100 pixels.",
},
{
key: <kbd>Left</kbd>,
text: t("Pan left by 100 pixels.",)
text: "Pan left by 100 pixels.",
},
{
key: <kbd>Right</kbd>,
text: t("Pan right by 100 pixels.",)
text: "Pan right by 100 pixels.",
},
{
key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>,
text: t("Increase the rotation by 15 degrees.",)
text: "Increase the rotation by 15 degrees.",
},
{
key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>,
text: t("Decrease the rotation by 15 degrees.")
text: "Decrease the rotation by 15 degrees."
},
{
key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>,
text: t("Increase the pitch by 10 degrees.")
text: "Increase the pitch by 10 degrees."
},
{
key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>,
text: t("Decrease the pitch by 10 degrees.")
text: "Decrease the pitch by 10 degrees."
},
]
@@ -105,13 +103,11 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
data-wd-key="modal:shortcuts"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t('Shortcuts')}
title={'Shortcuts'}
>
<section className="maputnik-modal-section maputnik-modal-shortcuts">
<p>
<Trans t={t}>
Press <code>ESC</code> to lose focus of any active elements, then press one of:
</Trans>
Press <code>ESC</code> to lose focus of any active elements, then press one of:
</p>
<dl>
{help.map((item, idx) => {
@@ -121,7 +117,7 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
</div>
})}
</dl>
<p>{t("If the Map is in focused you can use the following shortcuts")}</p>
<p>If the Map is in focused you can use the following shortcuts</p>
<ul>
{mapShortcuts.map((item, idx) => {
return <li key={idx}>
@@ -134,5 +130,3 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
}
}
const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
export default ModalShortcuts;
+27 -35
View File
@@ -2,7 +2,6 @@ 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 { WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal'
import InputButton from './InputButton'
@@ -75,17 +74,16 @@ type ActiveModalSourcesTypeEditorProps = {
source: SourceSpecification
onDelete(...args: unknown[]): unknown
onChange(...args: unknown[]): unknown
} & WithTranslation;
};
class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTypeEditorProps> {
render() {
const t = this.props.t;
return <div className="maputnik-active-source-type-editor">
<div className="maputnik-active-source-type-editor-header">
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
<span className="maputnik-space" />
<InputButton
aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})}
aria-label={`Remove '${this.props.sourceId}' source`}
className="maputnik-active-source-type-editor-header-delete"
onClick={()=> this.props.onDelete(this.props.sourceId)}
style={{backgroundColor: 'transparent'}}
@@ -106,7 +104,7 @@ class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTyp
type AddSourceProps = {
onAdd(...args: unknown[]): unknown
} & WithTranslation;
};
type AddSourceState = {
mode: EditorMode
@@ -204,7 +202,6 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
}
render() {
const t = this.props.t;
// Kind of a hack because the type changes, however maputnik has 1..n
// options per type, for example
//
@@ -218,25 +215,25 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
return <div className="maputnik-add-source">
<FieldString
label={t("Source ID")}
fieldSpec={{doc: t("Unique ID that identifies the source and is used in the layer to reference the source.")}}
label={"Source ID"}
fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}
value={this.state.sourceId}
onChange={(v: string) => this.setState({ sourceId: v})}
/>
<FieldSelect
label={t("Source Type")}
label={"Source Type"}
fieldSpec={sourceTypeFieldSpec}
options={[
['geojson_json', t('GeoJSON (JSON)')],
['geojson_url', t('GeoJSON (URL)')],
['tilejson_vector', t('Vector (TileJSON URL)')],
['tilexyz_vector', t('Vector (XYZ URLs)')],
['tilejson_raster', t('Raster (TileJSON URL)')],
['tilexyz_raster', t('Raster (XYZ URL)')],
['tilejson_raster-dem', t('Raster DEM (TileJSON URL)')],
['tilexyz_raster-dem', t('Raster DEM (XYZ URLs)')],
['image', t('Image')],
['video', t('Video')],
['geojson_json', 'GeoJSON (JSON)'],
['geojson_url', 'GeoJSON (URL)'],
['tilejson_vector', 'Vector (TileJSON URL)'],
['tilexyz_vector', 'Vector (XYZ URLs)'],
['tilejson_raster', 'Raster (TileJSON URL)'],
['tilexyz_raster', 'Raster (XYZ URL)'],
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
['image', 'Image'],
['video', 'Video'],
]}
onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
value={this.state.mode as string}
@@ -250,20 +247,20 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
className="maputnik-add-source-button"
onClick={this.onAdd}
>
{t("Add Source")}
Add Source
</InputButton>
</div>
}
}
type ModalSourcesInternalProps = {
type ModalSourcesProps = {
mapStyle: StyleSpecification
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
onStyleChanged(...args: unknown[]): unknown
} & WithTranslation;
};
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
export default class ModalSources extends React.Component<ModalSourcesProps> {
stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
const strippedSource = {...source}
delete strippedSource['title']
@@ -271,8 +268,7 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
}
render() {
const {t, mapStyle} = this.props;
const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady};
const mapStyle = this.props.mapStyle
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
const source = mapStyle.sources[sourceId]
return <ActiveModalSourcesTypeEditor
@@ -281,7 +277,6 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
source={source}
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
{...i18nProps}
/>
})
@@ -300,17 +295,17 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
data-wd-key="modal:sources"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t('Sources')}
title={'Sources'}
>
<section className="maputnik-modal-section">
<h1>{t("Active Sources")}</h1>
<h1>Active Sources</h1>
{activeSources}
</section>
<section className="maputnik-modal-section">
<h1>{t("Choose Public Source")}</h1>
<h1>Choose Public Source</h1>
<p>
{t("Add one of the publicly available sources to your style.")}
Add one of the publicly available sources to your style.
</p>
<div className="maputnik-public-sources" style={{maxWidth: 500}}>
{tilesetOptions}
@@ -318,16 +313,13 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
</section>
<section className="maputnik-modal-section">
<h1>{t("Add New Source")}</h1>
<p>{t("Add a new source to your style. You can only choose the source type and id at creation time!")}</p>
<h1>Add New Source</h1>
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
<AddSource
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
{...i18nProps}
/>
</section>
</Modal>
}
}
const ModalSources = withTranslation()(ModalSourcesInternal);
export default ModalSources;
+26 -47
View File
@@ -8,8 +8,6 @@ import FieldDynamicArray from './FieldDynamicArray'
import FieldArray from './FieldArray'
import FieldJson from './FieldJson'
import FieldCheckbox from './FieldCheckbox'
import { WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next'
export type EditorMode = "video" | "image" | "tilejson_vector" | "tilexyz_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "tilexyz_vector" | "geojson_url" | "geojson_json" | null;
@@ -19,15 +17,14 @@ type TileJSONSourceEditorProps = {
}
onChange(...args: unknown[]): unknown
children?: React.ReactNode
} & WithTranslation;
};
class TileJSONSourceEditor extends React.Component<TileJSONSourceEditorProps> {
render() {
const t = this.props.t;
return <div>
<FieldUrl
label={t("TileJSON URL")}
label={"TileJSON URL"}
fieldSpec={latest.source_vector.url}
value={this.props.source.url}
onChange={url => this.props.onChange({
@@ -48,7 +45,7 @@ type TileURLSourceEditorProps = {
}
onChange(...args: unknown[]): unknown
children?: React.ReactNode
} & WithTranslation;
};
class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
changeTileUrls(tiles: string[]) {
@@ -61,7 +58,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
renderTileUrls() {
const tiles = this.props.source.tiles || [];
return <FieldDynamicArray
label={this.props.t("Tile URL")}
label={"Tile URL"}
fieldSpec={latest.source_vector.tiles}
type="url"
value={tiles}
@@ -70,11 +67,10 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
}
render() {
const t = this.props.t;
return <div>
{this.renderTileUrls()}
<FieldNumber
label={t("Min Zoom")}
label={"Min Zoom"}
fieldSpec={latest.source_vector.minzoom}
value={this.props.source.minzoom || 0}
onChange={minzoom => this.props.onChange({
@@ -83,7 +79,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
})}
/>
<FieldNumber
label={t("Max Zoom")}
label={"Max Zoom"}
fieldSpec={latest.source_vector.maxzoom}
value={this.props.source.maxzoom || 22}
onChange={maxzoom => this.props.onChange({
@@ -97,24 +93,16 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
}
}
const createCornerLabels: (t: TFunction) => { label: string, key: string }[] = (t) => ([
{ label: t("Coord top left"), key: "top left" },
{ label: t("Coord top right"), key: "top right" },
{ label: t("Coord bottom right"), key: "bottom right" },
{ label: t("Coord bottom left"), key: "bottom left" },
]);
type ImageSourceEditorProps = {
source: {
coordinates: [number, number][]
url: string
}
onChange(...args: unknown[]): unknown
} & WithTranslation;
};
class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
render() {
const t = this.props.t;
const changeCoord = (idx: number, val: [number, number]) => {
const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
@@ -127,7 +115,7 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
return <div>
<FieldUrl
label={t("Image URL")}
label={"Image URL"}
fieldSpec={latest.source_image.url}
value={this.props.source.url}
onChange={url => this.props.onChange({
@@ -135,11 +123,11 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
url,
})}
/>
{createCornerLabels(t).map(({label, key}, idx) => {
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
return (
<FieldArray
label={label}
key={key}
label={`Coord ${label}`}
key={label}
length={2}
type="number"
value={this.props.source.coordinates[idx]}
@@ -158,11 +146,10 @@ type VideoSourceEditorProps = {
urls: string[]
}
onChange(...args: unknown[]): unknown
} & WithTranslation;
};
class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
render() {
const t = this.props.t;
const changeCoord = (idx: number, val: [number, number]) => {
const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
@@ -182,18 +169,18 @@ class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
return <div>
<FieldDynamicArray
label={t("Video URL")}
label={"Video URL"}
fieldSpec={latest.source_video.urls}
type="string"
value={this.props.source.urls}
default={[]}
onChange={changeUrls}
/>
{createCornerLabels(t).map(({label, key}, idx) => {
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
return (
<FieldArray
label={label}
key={key}
label={`Coord ${label}`}
key={label}
length={2}
type="number"
value={this.props.source.coordinates[idx]}
@@ -211,13 +198,12 @@ type GeoJSONSourceUrlEditorProps = {
data: string
}
onChange(...args: unknown[]): unknown
} & WithTranslation;
};
class GeoJSONSourceUrlEditor extends React.Component<GeoJSONSourceUrlEditorProps> {
render() {
const t = this.props.t;
return <FieldUrl
label={t("GeoJSON URL")}
label={"GeoJSON URL"}
fieldSpec={latest.source_geojson.data}
value={this.props.source.data}
onChange={data => this.props.onChange({
@@ -234,13 +220,12 @@ type GeoJSONSourceFieldJsonEditorProps = {
cluster: boolean
}
onChange(...args: unknown[]): unknown
} & WithTranslation;
};
class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJsonEditorProps> {
render() {
const t = this.props.t;
return <div>
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}>
<Block label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
<FieldJson
layer={this.props.source.data}
maxHeight={200}
@@ -258,7 +243,7 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
/>
</Block>
<FieldCheckbox
label={t('Cluster')}
label={'Cluster'}
value={this.props.source.cluster}
onChange={cluster => {
this.props.onChange({
@@ -271,22 +256,18 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
}
}
type ModalSourcesTypeEditorInternalProps = {
type ModalSourcesTypeEditorProps = {
mode: EditorMode
source: any
onChange(...args: unknown[]): unknown
} & WithTranslation;
};
class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEditorInternalProps> {
export default class ModalSourcesTypeEditor extends React.Component<ModalSourcesTypeEditorProps> {
render() {
const t = this.props.t;
const commonProps = {
source: this.props.source,
onChange: this.props.onChange,
t: this.props.t,
i18n: this.props.i18n,
tReady: this.props.tReady,
};
}
switch(this.props.mode) {
case 'geojson_url': return <GeoJSONSourceUrlEditor {...commonProps} />
case 'geojson_json': return <GeoJSONSourceFieldJsonEditor {...commonProps} />
@@ -297,7 +278,7 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
<FieldSelect
label={t("Encoding")}
label={"Encoding"}
fieldSpec={latest.source_raster_dem.encoding}
options={Object.keys(latest.source_raster_dem.encoding.values)}
onChange={encoding => this.props.onChange({
@@ -314,5 +295,3 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
}
}
const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
export default ModalSourcesTypeEditor;
+38
View File
@@ -0,0 +1,38 @@
import React from 'react'
import InputButton from './InputButton'
import Modal from './Modal'
// @ts-ignore
import logoImage from 'maputnik-design/logos/logo-color.svg'
type ModalSurveyProps = {
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
};
export default class ModalSurvey extends React.Component<ModalSurveyProps> {
onClick = () => {
window.open('https://gregorywolanski.typeform.com/to/cPgaSY', '_blank');
this.props.onOpenToggle();
}
render() {
return <Modal
data-wd-key="modal:survey"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title="Maputnik Survey"
>
<div className="maputnik-modal-survey">
<img src={logoImage} className="maputnik-modal-survey__logo" />
<h1>You + Maputnik = Maputnik better for you</h1>
<p className="maputnik-modal-survey__description">We dont track you, so we dont know how you use Maputnik. Help us make Maputnik better for you by completing a 7minute survey carried out by our contributing designer.</p>
<InputButton onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</InputButton>
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
</div>
</Modal>
}
}
+6 -10
View File
@@ -1,22 +1,18 @@
import React from 'react'
import { WithTranslation, withTranslation } from 'react-i18next';
import './SmallError.scss';
type SmallErrorInternalProps = {
type SmallErrorProps = {
children?: React.ReactNode
} & WithTranslation;
};
class SmallErrorInternal extends React.Component<SmallErrorInternalProps> {
export default class SmallError extends React.Component<SmallErrorProps> {
render () {
const t = this.props.t;
return (
<div className="SmallError">
{t("Error:")} {this.props.children}
Error: {this.props.children}
</div>
);
}
}
const SmallError = withTranslation()(SmallErrorInternal);
export default SmallError;
}
+19 -25
View File
@@ -11,14 +11,13 @@ import Block from './Block'
import docUid from '../libs/document-uid'
import sortNumerically from '../libs/sort-numerically'
import {findDefaultFromSpec} from '../libs/spec-helper';
import { WithTranslation, withTranslation } from 'react-i18next';
import labelFromFieldName from '../libs/label-from-field-name'
import DeleteStopButton from './_DeleteStopButton'
function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState) {
function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
// This is initialsed below only if required to improved performance.
let newRefs: {[key: number]: string} | undefined;
@@ -36,7 +35,7 @@ function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState)
return newRefs;
}
type DataPropertyInternalProps = {
type DataPropertyProps = {
onChange?(fieldName: string, value: any): unknown
onDeleteStop?(...args: unknown[]): unknown
onAddStop?(...args: unknown[]): unknown
@@ -47,7 +46,7 @@ type DataPropertyInternalProps = {
fieldSpec?: object
value?: DataPropertyValue
errors?: object
} & WithTranslation;
};
type DataPropertyState = {
refs: {[key: number]: string}
@@ -66,7 +65,7 @@ export type Stop = [{
value: number
}, number]
class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> {
export default class DataProperty extends React.Component<DataPropertyProps, DataPropertyState> {
state = {
refs: {} as {[key: number]: string}
}
@@ -81,7 +80,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
}
}
static getDerivedStateFromProps(props: Readonly<DataPropertyInternalProps>, state: DataPropertyState) {
static getDerivedStateFromProps(props: DataPropertyProps, state: DataPropertyState) {
const newRefs = setStopRefs(props, state);
if(newRefs) {
return {
@@ -214,8 +213,6 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
}
render() {
const t = this.props.t;
if (typeof this.props.value?.type === "undefined") {
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec)
}
@@ -230,8 +227,8 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
const dataProps = {
'aria-label': t("Input value"),
label: t("Data value"),
'aria-label': "Input value",
label: "Data value",
value: dataLevel as any,
onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
}
@@ -266,7 +263,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
</td>
<td>
<InputSpec
aria-label={t("Output value")}
aria-label="Output value"
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={value}
@@ -285,21 +282,21 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner">
<Block
label={t("Function")}
label={"Function"}
key="function"
>
<div className="maputnik-data-spec-property-input">
<InputSelect
value={this.props.value!.type}
onChange={(propVal: string) => this.changeDataType(propVal)}
title={t("Select a type of data scale (default is 'categorical').")}
title={"Select a type of data scale (default is 'categorical')."}
options={this.getDataFunctionTypes(this.props.fieldSpec)}
/>
</div>
</Block>
{this.props.value?.type !== "identity" &&
<Block
label={t("Base")}
label={"Base"}
key="base"
>
<div className="maputnik-data-spec-property-input">
@@ -319,14 +316,14 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
<div className="maputnik-data-spec-property-input">
<InputString
value={this.props.value?.property}
title={t("Input a data property to base styles off of.")}
title={"Input a data property to base styles off of."}
onChange={propVal => this.changeDataProperty("property", propVal)}
/>
</div>
</Block>
{dataFields &&
<Block
label={t("Default")}
label={"Default"}
key="default"
>
<InputSpec
@@ -340,12 +337,12 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
{dataFields &&
<div className="maputnik-function-stop">
<table className="maputnik-function-stop-table">
<caption>{t("Stops")}</caption>
<caption>Stops</caption>
<thead>
<tr>
<th>{t("Zoom")}</th>
<th>{t("Input value")}</th>
<th rowSpan={2}>{t("Output value")}</th>
<th>Zoom</th>
<th>Input value</th>
<th rowSpan={2}>Output value</th>
</tr>
</thead>
<tbody>
@@ -362,7 +359,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> {t("Add stop")}
</svg> Add stop
</InputButton>
}
<InputButton
@@ -371,7 +368,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> {t("Convert to expression")}
</svg> Convert to expression
</InputButton>
</div>
</div>
@@ -379,6 +376,3 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
</div>
}
}
const DataProperty = withTranslation()(DataPropertyInternal);
export default DataProperty;
+4 -9
View File
@@ -2,26 +2,21 @@ import React from 'react'
import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
type DeleteStopButtonInternalProps = {
type DeleteStopButtonProps = {
onClick?(...args: unknown[]): unknown
} & WithTranslation;
};
class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalProps> {
export default class DeleteStopButton extends React.Component<DeleteStopButtonProps> {
render() {
const t = this.props.t;
return <InputButton
className="maputnik-delete-stop"
onClick={this.props.onClick}
title={t("Remove zoom level from stop")}
title={"Remove zoom level from stop"}
>
<MdDelete />
</InputButton>
}
}
const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
export default DeleteStopButton;
+8 -12
View File
@@ -1,7 +1,6 @@
import React from 'react'
import {MdDelete, MdUndo} from 'react-icons/md'
import stringifyPretty from 'json-stringify-pretty-compact'
import { WithTranslation, withTranslation } from 'react-i18next';
import Block from './Block'
import InputButton from './InputButton'
@@ -9,7 +8,7 @@ import labelFromFieldName from '../libs/label-from-field-name'
import FieldJson from './FieldJson'
type ExpressionPropertyInternalProps = {
type ExpressionPropertyProps = {
onDelete?(...args: unknown[]): unknown
fieldName: string
fieldType?: string
@@ -21,20 +20,20 @@ type ExpressionPropertyInternalProps = {
canUndo?(...args: unknown[]): unknown
onFocus?(...args: unknown[]): unknown
onBlur?(...args: unknown[]): unknown
} & WithTranslation;
};
type ExpressionPropertyState = {
jsonError: boolean
};
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps, ExpressionPropertyState> {
export default class ExpressionProperty extends React.Component<ExpressionPropertyProps, ExpressionPropertyState> {
static defaultProps = {
errors: {},
onFocus: () => {},
onBlur: () => {},
}
constructor(props: ExpressionPropertyInternalProps) {
constructor (props:ExpressionPropertyProps) {
super(props);
this.state = {
jsonError: false,
@@ -54,7 +53,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
}
render() {
const {t, errors, fieldName, fieldType, value, canUndo} = this.props;
const {errors, fieldName, fieldType, value, canUndo} = this.props;
const {jsonError} = this.state;
const undoDisabled = canUndo ? !canUndo() : true;
@@ -66,7 +65,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
onClick={this.props.onUndo}
disabled={undoDisabled}
className="maputnik-delete-stop"
title={t("Revert from expression")}
title="Revert from expression"
>
<MdUndo />
</InputButton>
@@ -75,7 +74,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
key="delete_action"
onClick={this.props.onDelete}
className="maputnik-delete-stop"
title={t("Delete expression")}
title="Delete expression"
>
<MdDelete />
</InputButton>
@@ -113,7 +112,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
// this feels like an incorrect type...? `foundErrors` is an array of objects, not a single object
error={foundErrors as any}
fieldSpec={this.props.fieldSpec}
label={t(labelFromFieldName(this.props.fieldName))}
label={labelFromFieldName(this.props.fieldName)}
action={deleteStopBtn}
wideMode={true}
>
@@ -138,6 +137,3 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
</Block>
}
}
const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
export default ExpressionProperty;
+6 -11
View File
@@ -3,18 +3,16 @@ import React from 'react'
import InputButton from './InputButton'
import {MdFunctions, MdInsertChart} from 'react-icons/md'
import {mdiFunctionVariant} from '@mdi/js';
import { WithTranslation, withTranslation } from 'react-i18next';
type FunctionInputButtonsInternalProps = {
type FunctionInputButtonsProps = {
fieldSpec?: any
onZoomClick?(...args: unknown[]): unknown
onDataClick?(...args: unknown[]): unknown
onExpressionClick?(...args: unknown[]): unknown
} & WithTranslation;
};
class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> {
export default class FunctionInputButtons extends React.Component<FunctionInputButtonsProps> {
render() {
const t = this.props.t;
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
@@ -22,7 +20,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
<InputButton
className="maputnik-make-zoom-function"
onClick={this.props.onExpressionClick}
title={t("Convert to expression")}
title="Convert to expression"
>
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
@@ -33,7 +31,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
makeZoomInputButton = <InputButton
className="maputnik-make-zoom-function"
onClick={this.props.onZoomClick}
title={t("Convert property into a zoom function")}
title="Convert property into a zoom function"
>
<MdFunctions />
</InputButton>
@@ -42,7 +40,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
makeDataInputButton = <InputButton
className="maputnik-make-data-function"
onClick={this.props.onDataClick}
title={t("Convert property to data function")}
title="Convert property to data function"
>
<MdInsertChart />
</InputButton>
@@ -58,6 +56,3 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
}
}
}
const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
export default FunctionInputButtons;
+18 -22
View File
@@ -1,7 +1,6 @@
import React from 'react'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import { WithTranslation, withTranslation } from 'react-i18next';
import InputButton from './InputButton'
import InputSpec from './InputSpec'
@@ -21,7 +20,7 @@ import sortNumerically from '../libs/sort-numerically'
*
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
*/
function setStopRefs(props: ZoomPropertyInternalProps, state: ZoomPropertyState) {
function setStopRefs(props: ZoomPropertyProps, state: ZoomPropertyState) {
// This is initialsed below only if required to improved performance.
let newRefs: {[key: number]: string} = {};
@@ -46,7 +45,7 @@ type ZoomWithStops = {
}
type ZoomPropertyInternalProps = {
type ZoomPropertyProps = {
onChange?(...args: unknown[]): unknown
onChangeToDataFunction?(...args: unknown[]): unknown
onDeleteStop?(...args: unknown[]): unknown
@@ -60,13 +59,13 @@ type ZoomPropertyInternalProps = {
}
errors?: object
value?: ZoomWithStops
} & WithTranslation;
};
type ZoomPropertyState = {
refs: {[key: number]: string}
}
class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> {
export default class ZoomProperty extends React.Component<ZoomPropertyProps, ZoomPropertyState> {
static defaultProps = {
errors: {},
}
@@ -85,7 +84,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
}
}
static getDerivedStateFromProps(props: Readonly<ZoomPropertyInternalProps>, state: ZoomPropertyState) {
static getDerivedStateFromProps(props: ZoomPropertyProps, state: ZoomPropertyState) {
const newRefs = setStopRefs(props, state);
if(newRefs) {
return {
@@ -153,17 +152,17 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
}
render() {
const t = this.props.t;
const zoomFields = this.props.value?.stops.map((stop, idx) => {
const zoomLevel = stop[0]
const key = this.state.refs[idx];
const value = stop[1]
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
return <tr
key={`${stop[0]}-${stop[1]}`}
key={key}
>
<td>
<InputNumber
aria-label={t("Zoom")}
aria-label="Zoom"
value={zoomLevel}
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
min={0}
@@ -172,7 +171,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
</td>
<td>
<InputSpec
aria-label={t("Output value")}
aria-label="Output value"
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec as any}
value={value}
@@ -191,19 +190,19 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner">
<Block
label={t("Function")}
label={"Function"}
>
<div className="maputnik-data-spec-property-input">
<InputSelect
value={"interpolate"}
onChange={(propVal: string) => this.changeDataType(propVal)}
title={t("Select a type of data scale (default is 'categorical').")}
title={"Select a type of data scale (default is 'categorical')."}
options={this.getDataFunctionTypes(this.props.fieldSpec!)}
/>
</div>
</Block>
<Block
label={t("Base")}
label={"Base"}
>
<div className="maputnik-data-spec-property-input">
<InputSpec
@@ -216,11 +215,11 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
</Block>
<div className="maputnik-function-stop">
<table className="maputnik-function-stop-table maputnik-function-stop-table--zoom">
<caption>{t("Stops")}</caption>
<caption>Stops</caption>
<thead>
<tr>
<th>{t("Zoom")}</th>
<th rowSpan={2}>{t("Output value")}</th>
<th>Zoom</th>
<th rowSpan={2}>Output value</th>
</tr>
</thead>
<tbody>
@@ -235,7 +234,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> {t("Add stop")}
</svg> Add stop
</InputButton>
<InputButton
className="maputnik-add-stop"
@@ -243,7 +242,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> {t("Convert to expression")}
</svg> Convert to expression
</InputButton>
</div>
</div>
@@ -263,6 +262,3 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
}
}
}
const ZoomProperty = withTranslation()(ZoomPropertyInternal);
export default ZoomProperty;
+36 -48
View File
@@ -1,40 +1,40 @@
[
{
"id": "0-empty-style",
"title": "Empty Style",
"url": "https://cdn.jsdelivr.net/gh/maputnik/editor@9cf74ca405d2be0608b57db8109cf3a6af5b9f49/src/config/empty-style.json",
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
},
{
"id": "dark-matter",
"title": "Dark Matter",
"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": "osm-liberty",
"title": "OSM Liberty",
"url": "https://maputnik.github.io/osm-liberty/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png"
},
{
"id": "maptiler-basic-gl-style",
"title": "Maptiler Basic",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.10/style.json",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
},
{
"id": "dark-matter",
"title": "Dark Matter",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.8/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
},
{
"id": "positron",
"title": "Positron",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.8/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
},
{
"id": "osm-bright",
"title": "OSM Bright",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
},
{
"id": "maptiler-toner-gl-style",
"title": "Toner",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@v1.0/style.json",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@339e5b7/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
},
{
"id": "os-zoomstack-light",
"title": "Zoomstack Light",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-light/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
},
{
"id": "os-zoomstack-night",
"title": "Zoomstack Night",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-night/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
},
{
"id": "os-zoomstack-outdoor",
"title": "Zoomstack Outdoor",
@@ -48,33 +48,21 @@
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
},
{
"id": "osm-bright",
"title": "OSM Bright",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.11/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
"id": "os-zoomstack-light",
"title": "Zoomstack Light",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-light/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
},
{
"id": "osm-liberty",
"title": "OSM Liberty",
"url": "https://maputnik.github.io/osm-liberty/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png"
"id": "os-zoomstack-night",
"title": "Zoomstack Night",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-night/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
},
{
"id": "positron",
"title": "Positron",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
},
{
"id": "stadia-outdoors",
"title": "Stadia Outdoors",
"url": "https://tiles.stadiamaps.com/styles/outdoors.json",
"thumbnail": "https://tiles.stadiamaps.com/static/outdoors.png?size=480x320&center=47.350259,8.49035&zoom=16"
},
{
"id": "versatiles-colorful",
"title": "Versatiles Colorful",
"url": "https://tiles.versatiles.org/assets/styles/colorful.json",
"thumbnail": "https://github.com/maplibre/maputnik/assets/649392/6cd69818-c541-46e4-a920-65fb4f654931"
"id": "empty-style",
"title": "Empty Style",
"url": "https://cdn.jsdelivr.net/gh/maputnik/editor@9cf74ca405d2be0608b57db8109cf3a6af5b9f49/src/config/empty-style.json",
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
}
]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"openmaptiles": {
"type": "vector",
"url": "https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key={key}",
"url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}",
"title": "OpenMapTiles v3"
},
"thunderforest_transport": {
-40
View File
@@ -1,40 +0,0 @@
import i18n from "i18next";
import detector from "i18next-browser-languagedetector";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next";
export const supportedLanguages = {
"en": "English",
"ja": "日本語",
"he": "עברית",
"zh": "简体中文"
} as const;
i18n
.use(detector) // detect user language from browser settings
.use(
resourcesToBackend((lang: string, ns: string) => {
if (lang === "en") {
// English is the default language, so we don't need to load any resources for it.
return {};
}
return import(`./locales/${lang}/${ns}.json`);
})
)
.use(initReactI18next) // required to initialize react-i18next
.init({
supportedLngs: Object.keys(supportedLanguages),
keySeparator: false, // we do not use keys in form messages.welcome
nsSeparator: false,
interpolation: {
escapeValue: false // React already escapes for us
},
saveMissing: true, // this needs to be set for missingKeyHandler to work
fallbackLng: false, // we set the fallback to false so we can get the correct language in the missingKeyHandler
missingKeyHandler: (lngs, _ns, key) => {
if (lngs[0] === "en") { return; }
console.warn(`Missing translation for "${key}" in "${lngs.join(", ")}"`);
}
});
export default i18n;
+4 -5
View File
@@ -1,16 +1,15 @@
import { IconContext } from "react-icons";
import { createRoot } from 'react-dom/client';
import ReactDOM from 'react-dom';
import './favicon.ico'
import './styles/index.scss'
import './i18n';
import App from './components/App';
const root = createRoot(document.querySelector("#app"));
root.render(
ReactDOM.render(
<IconContext.Provider value={{className: 'react-icons'}}>
<App/>
</IconContext.Provider>
</IconContext.Provider>,
document.querySelector("#app")
);
// Hide the loader.
+8 -10
View File
@@ -1,20 +1,18 @@
import { TFunction } from "i18next";
const spec = (t: TFunction) => ({
const spec = {
maputnik: {
maptiler_access_token: {
label: t("MapTiler Access Token"),
doc: t("Public access token for MapTiler Cloud.")
label: "MapTiler Access Token",
doc: "Public access token for MapTiler Cloud."
},
thunderforest_access_token: {
label: t("Thunderforest Access Token"),
doc: t("Public access token for Thunderforest services.")
label: "Thunderforest Access Token",
doc: "Public access token for Thunderforest services."
},
style_renderer: {
label: t("Style Renderer"),
doc: t("Choose the default Maputnik renderer for this style."),
label: "Style Renderer",
doc: "Choose the default Maputnik renderer for this style.",
},
}
})
}
export default spec;
+4 -2
View File
@@ -1,5 +1,7 @@
import stylegen from '@maplibre/maplibre-gl-inspect/lib/stylegen'
import colors from '@maplibre/maplibre-gl-inspect/lib/colors'
// @ts-ignore
import stylegen from 'mapbox-gl-inspect/lib/stylegen'
// @ts-ignore
import colors from 'mapbox-gl-inspect/lib/colors'
import type {FilterSpecification,LayerSpecification } from 'maplibre-gl'
export type HighlightedLayer = LayerSpecification & {filter?: FilterSpecification};
+1 -3
View File
@@ -5,15 +5,13 @@ export default class ZoomControl {
_container: HTMLDivElement | undefined = undefined;
_textEl: HTMLSpanElement | null = null;
constructor(public label: string) {}
onAdd(map: Map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-zoom';
this._container.setAttribute("data-wd-key", "maplibre:ctrl-zoom");
this._container.innerHTML = `
${this.label} <span></span>
Zoom: <span></span>
`;
this._textEl = this._container.querySelector("span");
-184
View File
@@ -1,184 +0,0 @@
{
"Input value": "ערך קבלה",
"Data value": "ערך מידע",
"Output value": "ערך החזרה",
"Function": "פונקציה",
"Select a type of data scale (default is 'categorical').": "בחרו סוג מידע לטובת הגדלה (ברירת המחדל היא קטגוריה)",
"Base": "בסיס",
"Input a data property to base styles off of.": "הכניסו מידע עליו יש לבסס את הסטייל",
"Default": "ברירת המחדל",
"Stops": "עצירות",
"Zoom": "זום",
"Add stop": "הוספת עצירה",
"Convert to expression": "המחרה לביטוי",
"Remove zoom level from stop": "הסרת רמת זום מעצירה",
"Revert from expression": "החזרה מביטוי",
"Delete expression": "מחיקת ביטוי",
"Convert property into a zoom function": "המרה לפונקציית זום",
"Convert property to data function": "המרה לפונקציית מידע",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "שכבה <1>{formatLayerId(layerId)}</1>: {parsed.data.message}",
"switch to layer": "שינוי לשכבה",
"Map": "מפה",
"Inspect": "בדיקה",
"Deuteranopia filter": "Deuteranopia filter",
"Protanopia filter": "Protanopia filter",
"Tritanopia filter": "Tritanopia filter",
"Achromatopsia filter": "Achromatopsia filter",
"Layers list": "רשימת שכבות",
"Layer editor": "עורך השכבות",
"Map view": "תצוגת מפה",
"Maputnik on GitHub": "מפוטניק בגיטהב",
"Open": "פתיחה",
"Export": "ייצוא",
"Data Sources": "מקורות מידע",
"Style Settings": "הגדרות הסטייל",
"View": "תצוגה",
"Color accessibility": "נגישות צבעים",
"Language": "שפה",
"Help": "עזרה",
"Comments for the current layer. This is non-standard and not in the spec.": "הערות על השכבה הנוכחית. זה לא חלק מהספסיפיקציות",
"Comments": "הערות",
"Comment...": "הערה...",
"Max Zoom": "זום מקסימאלי",
"Min Zoom": "זום מינימאלי",
"Source": "מקור",
"Source Layer": "שכבת מקור",
"Type": "סוג",
"Nested filters are not supported.": "סינונים מקוננים אינם נתמכים",
"Upgrade to expression": "שדרוג לביטוי",
"Filter": "סינון",
"every filter matches": "כל הסינונים מתאימים",
"no filter matches": "אף סינון אינו מתאים",
"any filter matches": "אחד הסינוים מתאימים",
"Add filter": "הוספת סינון",
"You've entered an old style filter.": "הכנסתם סינון מסוג ישן,",
"Switch to filter editor.": "עברו לעורך הסינונים",
"Delete filter block": "מחיקת גוש מסנן",
"Add value": "הוספת ערך",
"Remove array item": "הסרת איבר מערך",
"Press <1>ESC</1> to lose focus": "לחצו על <1>ESC</1> על מנת לאבד פוקוס",
"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",
"Layer": "שכבה",
"JSON Editor": "עורך JSON",
"Delete": "מחיקה",
"Duplicate": "שכפול",
"Show": "הצגה",
"Hide": "הסתרה",
"Move layer up": "העלה שכבה",
"Move layer down": "הורד שכבה",
"Layer: {{layerId}}": "שכבה: {{layerId}}",
"Layers": "שכבות",
"Collapse": "הקטנה",
"Expand": "הגדלה",
"Add Layer": "הוספת שכבה",
"Zoom:": "זום:",
"Search": "חיפוש",
"Close popup": "סגירת החלון",
"cursor:": "סמן",
"center:": "מרכז:",
"rotation:": "סיבוב:",
"Close modal": "סגירת חלונית",
"Debug": "דיבאג",
"Options": "אפשרויות",
"<0>Open in OSM</0> &mdash; Opens the current view on openstreetmap.org": "<0>פתיחה ב-OSM</0> - פתיחה של התצוגה הנוכחית ב- openstreetmap.org",
"Export Style": "ייצוא של הסטייל",
"Download Style": "הורדה של הסטייל",
"Download a JSON style to your computer.": "הורדה של הסטייל למחשב",
"Download HTML": "הורדה כ-HTML",
"Cancel": "ביטול",
"Open Style": "פתיחת סטייל",
"Upload Style": "העלאה של סטייל",
"Upload a JSON style from your computer.": "העלאה של סטייל מהמחשב",
"Style file": "קובץ סטייל",
"Upload": "העלאה",
"Load from URL": "פתיחה מתוך כתובת",
"Load from a URL. Note that the URL must have <1>CORS enabled</1>.": "פתיחה מכתובת, שימו לב: הכתובת צריכה לתמוך ב- CORS",
"Style URL": "כתוסת סטייל",
"Enter URL...": "הכנסו כתובת",
"Gallery Styles": "גלריית סטיילים",
"Open one of the publicly available styles to start from.": "פתיחת אחד הסטייליםפ הציבוריים על מנת להתחיל מהם.",
"Loading style": "טוען סטייל",
"Loading: {{requestUrl}}": "טוען: {{requestUrl}}",
"Name": "שם",
"Owner": "שייך ל",
"Owner ID of the style. Used by Mapbox or future style APIs.": "מזהה השייכות של הסטייל.",
"Sprite URL": "כתובת ספרייט",
"Glyphs URL": "כתובת סמלילים",
"Center": "מרכז",
"Bearing": "כיוון",
"Pitch": "זווית",
"Light anchor": "עוגן אור",
"Light color": "צבע האור",
"Light intensity": "חוזק האור",
"Light position": "מיקום האור",
"Terrain source": "מקור terrain",
"Terrain exaggeration": "כמה \"להגזים\"",
"Transition delay": "זמן לפני מעבר",
"Transition duration": "זמן מעבר",
"Open Layers (experimental)": "Open Layers (experimental)",
"Shortcuts menu": "תפריט קיצורי דרך",
"Open modal": "פתיחת חלונית",
"Export modal": "חלונית ייצוא",
"Data Sources modal": "חלונית מקורות",
"Style Settings modal": "חלונית הגדרות סטייל",
"Toggle inspect": "שינוי בדיקה",
"Focus map": "פיקוס המפה",
"Debug modal": "חלונית דיבאג",
"Increase the zoom level by 1.": "העלאה של הזום ב-1",
"Increase the zoom level by 2.": "העלאה של הזום ב-2",
"Decrease the zoom level by 1.": "הורדה של הזום ב-1",
"Decrease the zoom level by 2.": "הורדה של הזום ב-2",
"Pan up by 100 pixels.": "תזוזה למעלה ב-100 פיקסלים",
"Pan down by 100 pixels.": "תזוזה למטה ב-100 פיקסלים",
"Pan left by 100 pixels.": "תזוזה שמאלה ב-100 פיקסלים",
"Pan right by 100 pixels.": "תזוזה ימינה ב-100 פיקסלים",
"Increase the rotation by 15 degrees.": "הוספת 15 מעלות לסיבוב",
"Decrease the rotation by 15 degrees.": "הורדת 15 מעלות לסיבוב",
"Increase the pitch by 10 degrees.": "הוספה של 10 מעלות לזיווית",
"Decrease the pitch by 10 degrees.": "הורדה של 10 מעלות מהזווית",
"Shortcuts": "קיצורי דרך",
"Press <1>ESC</1> to lose focus of any active elements, then press one of:": "לחיצה על <1>ESC</1> על מנת לאבד פוקוס מכל פקד פעיל, ואז ליחצו על אחד מאלו:",
"If the Map is in focused you can use the following shortcuts": "אם המפה נמצאת בפוקוס תוכלו להשתמש בקיצורי הדרך",
"Remove '{{sourceId}}' source": "הורדה של מקור '{{sourceId}}'",
"Source ID": "מזהה מקור",
"Unique ID that identifies the source and is used in the layer to reference the source.": "מזהה מקור ייחודי לטובת שימוש בשכבה",
"Source Type": "סוג מקור",
"GeoJSON (JSON)": "GeoJSON (JSON)",
"GeoJSON (URL)": "GeoJSON (URL)",
"Vector (TileJSON URL)": "Vector (TileJSON URL)",
"Vector (XYZ URLs)": "Vector (XYZ URLs)",
"Raster (TileJSON URL)": "Raster (TileJSON URL)",
"Raster (XYZ URL)": "Raster (XYZ URL)",
"Raster DEM (TileJSON URL)": "Raster DEM (TileJSON URL)",
"Raster DEM (XYZ URLs)": "Raster DEM (XYZ URLs)",
"Image": "תמונה",
"Video": "וידאו",
"Add Source": "הוספת מקור",
"Sources": "מקורות",
"Active Sources": "מקורות פעילים",
"Choose Public Source": "בחירת מקור ציבורי",
"Add one of the publicly available sources to your style.": "הוספת אחד המקורות הציבוריים הזמינים לסטייל שלך.",
"Add New Source": "הוספת מקור חדש",
"Add a new source to your style. You can only choose the source type and id at creation time!": "הוספת מקור לסטייל, ביכולתכם לעשות זאת רק בזמן מסויים",
"TileJSON URL": "כתובת TileJSON",
"Tile URL": "כתובת אריחים",
"Coord top left": "מיקום שמאלי עליון",
"Coord top right": "מיקום ימני עליון",
"Coord bottom right": "מיקום ימני תחתון",
"Coord bottom left": "מיקום שמאלי תחתון",
"Image URL": "כתובת תמונה",
"Video URL": "כתובת וידאו",
"GeoJSON URL": "כתובת GeoJSON",
"GeoJSON": "GeoJSON",
"Cluster": "קיבוץ",
"Encoding": "קידוד",
"Error:": "שגיאה",
"MapTiler Access Token": "MapTiler Access Token",
"Public access token for MapTiler Cloud.": "Public access token for MapTiler Cloud.",
"Thunderforest Access Token": "Thunderforest Access Token",
"Public access token for Thunderforest services.": "Public access token for Thunderforest services.",
"Style Renderer": "צייר הסטייל",
"Choose the default Maputnik renderer for this style.": "בחירת צייר ברירת המחדל של מפוטניק עבור הסטייל הזה"
}
-192
View File
@@ -1,192 +0,0 @@
{
"Input value": "入力値",
"Data value": "データ値",
"Output value": "値",
"Function": "関数",
"Select a type of data scale (default is 'categorical').": "データスケールの種類を選択してください(デフォルトは「categorical」です)。",
"Base": "ベース",
"Input a data property to base styles off of.": "スタイルのベースとなるデータプロパティを入力してください。",
"Default": "デフォルト",
"Stops": "ストップ",
"Zoom": "ズーム",
"Add stop": "ストップを追加",
"Convert to expression": "フィルタ式に変換",
"Remove zoom level from stop": "ズームレベルをストップから削除",
"Revert from expression": "式から戻す",
"Delete expression": "式を削除",
"Convert property into a zoom function": "プロパティをズーム関数に変換する",
"Convert property to data function": "プロパティをデータ関数に変換する",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "レイヤ<1>{formatLayerId(layerId)}</1>: {parsed.data.message}",
"switch to layer": "レイヤへ切替",
"Map": "地図",
"Inspect": "検査",
"Deuteranopia filter": "緑色盲フィルタ",
"Protanopia filter": "赤色盲フィルタ",
"Tritanopia filter": "青色盲フィルタ",
"Achromatopsia filter": "全色盲フィルタ",
"Layers list": "レイヤー一覧",
"Layer editor": "レイヤーエディタ",
"Map view": "地図画面",
"Maputnik on GitHub": "GitHubのMaputnik",
"Open": "開く",
"Export": "エクスポート",
"Data Sources": "データソース",
"Style Settings": "スタイル設定",
"View": "表示",
"Color accessibility": "色のアクセシビリティ",
"Language": "言語",
"Help": "ヘルプ",
"Comments for the current layer. This is non-standard and not in the spec.": "現在のレイヤーのコメント。注意:この機能は標準ではないため、他のライブラリとの互換性状況はわかりません。",
"Comments": "コメント",
"Comment...": "コメントを書く",
"Max Zoom": "最大ズーム",
"Min Zoom": "最小ズーム",
"Source": "ソース",
"Source Layer": "ソースレイヤ",
"Type": "タイプ",
"Nested filters are not supported.": "ネストされたフィルタはサポートされていません。",
"Upgrade to expression": "式にアップグレード",
"Filter": "フィルタ",
"every filter matches": "全てのフィルタが一致",
"no filter matches": "フィルタが一致しない",
"any filter matches": "いずれかのフィルタが一致",
"Add filter": "フィルタを追加",
"You've entered an old style filter.": "旧型フィルタを使用しております。",
"Switch to filter editor.": "新型に変換",
"Delete filter block": "フィルタブロックを削除",
"Add value": "値を追加する",
"Remove array item": "配列の項目を削除",
"Press <1>ESC</1> to lose focus": "<1>ESC</1>を押すとフォーカスが外れます",
"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> ドメインを使用してください。",
"Layer": "レイヤー",
"JSON Editor": "JSONエディタ",
"Delete": "削除",
"Duplicate": "複製",
"Show": "表示",
"Hide": "非表示",
"Move layer up": "上に移動",
"Move layer down": "下に移動",
"Layer: {{layerId}}": "レイヤー {{layerId}}",
"Layers": "レイヤー",
"Collapse": "畳む",
"Expand": "展開",
"Add Layer": "レイヤー追加",
"Zoom:": "ズーム:",
"Search": "検索",
"Close popup": "ポップアップを閉じる",
"cursor:": "カーソル",
"center:": "中央:",
"rotation:": "回転角度:",
"Close modal": "モーダルを閉じる",
"Debug": "デバッグ",
"Options": "設定",
"<0>Open in OSM</0> &mdash; Opens the current view on openstreetmap.org": "現在のビューを <0>openstreetmap.org で開く</0>",
"Export Style": "スタイルをエクスポート",
"Download Style": "スタイルをダウンロード",
"Download a JSON style to your computer.": "パソコンにJSONスタイルをダウンロードします。",
"Download HTML": "HTMLをダウンロード",
"Cancel": "キャンセル",
"Open Style": "スタイルを開く",
"Upload Style": "スタイルをアップロードする",
"Upload a JSON style from your computer.": "JSONスタイルをパソコンからアップロードする",
"Style file": "スタイルファイル",
"Upload": "アップロード",
"Load from URL": "URLから読み込む",
"Load from a URL. Note that the URL must have <1>CORS enabled</1>.": "URLから読み込む。注意: URLは <1>CORSを有効にする</1> 必要があります。",
"Style URL": "スタイルURL",
"Enter URL...": "URLを入力",
"Gallery Styles": "ギャラリースタイル",
"Open one of the publicly available styles to start from.": "公開スタイルを選んで開始しましょう。",
"Loading style": "スタイル読込中",
"Loading: {{requestUrl}}": "{{requestUrl}} を読み込み中",
"Name": "名前",
"Owner": "所有者",
"Owner ID of the style. Used by Mapbox or future style APIs.": "スタイルの所有者 ID。Mapbox または将来のスタイル API によって使用されます。",
"Sprite URL": "スプライトURL",
"Glyphs URL": "フォントグリフURL",
"Center": "中央",
"Bearing": "方位",
"Pitch": "ピッチ",
"Light anchor": "ライトアンカー",
"Light color": "ライトカラー",
"Light intensity": "ライト強度",
"Light position": "ライト位置",
"Terrain source": "地形ソース",
"Terrain exaggeration": "地形の誇張",
"Transition delay": "遷移遅延",
"Transition duration": "遷移期間",
"Open Layers (experimental)": "Open Layers (実験的)",
"Shortcuts menu": "ショートカットメニュー",
"Open modal": "モーダルを開く",
"Export modal": "書き出しのモーダル",
"Data Sources modal": "データソースのモーダル",
"Style Settings modal": "スタイル設定のモーダル",
"Toggle inspect": "検査の切り替え",
"Focus map": "地図にフォーカス",
"Debug modal": "デバッグモーダル",
"Increase the zoom level by 1.": "ズームレベルを1増やす",
"Increase the zoom level by 2.": "ズームレベルを2増やす",
"Decrease the zoom level by 1.": "ズームレベルを1減らす",
"Decrease the zoom level by 2.": "ズームレベルを2減らす",
"Pan up by 100 pixels.": "100ピクセル上に移動",
"Pan down by 100 pixels.": "100ピクセル下に移動",
"Pan left by 100 pixels.": "100ピクセル左に移動",
"Pan right by 100 pixels.": "100ピクセル右に移動",
"Increase the rotation by 15 degrees.": "回転角度を15度増やす",
"Decrease the rotation by 15 degrees.": "回転角度を15度減らす",
"Increase the pitch by 10 degrees.": "ピッチを10度増やす",
"Decrease the pitch by 10 degrees.": "ピッチを10度減らす",
"Shortcuts": "ショートカット",
"Press <1>ESC</1> to lose focus of any active elements, then press one of:": "<1>ESC</1> を押してアクティブな要素からフォーカスを外し、次に以下のいずれかを押してください:",
"If the Map is in focused you can use the following shortcuts": "地図がフォーカスされている場合、以下のショートカットを使用できます",
"Remove '{{sourceId}}' source": "'{{sourceId}}' ソースを削除",
"Source ID": "ソースID",
"Unique ID that identifies the source and is used in the layer to reference the source.": "ソースを識別するためのユニークID。レイヤーでソースを参照するために使用されます。",
"Source Type": "ソースタイプ",
"GeoJSON (JSON)": "GeoJSON (JSON)",
"GeoJSON (URL)": "GeoJSON (URL)",
"Vector (TileJSON URL)": "ベクトル (TileJSON URL)",
"Vector (XYZ URLs)": "ベクトル (XYZ URL)",
"Raster (TileJSON URL)": "ラスタ (TileJSON URL)",
"Raster (XYZ URL)": "ラスタ (XYZ URL)",
"Raster DEM (TileJSON URL)": "ラスタ DEM (TileJSON URL)",
"Raster DEM (XYZ URLs)": "ラスタ DEM (XYZ URL)",
"Image": "画像",
"Video": "動画",
"Add Source": "ソースを追加",
"Sources": "ソース一覧",
"Active Sources": "使用中ソース",
"Choose Public Source": "公開ソースから選択",
"Add one of the publicly available sources to your style.": "公開ソースをスタイルに追加しましょう",
"Add New Source": "新規ソースを追加",
"Add a new source to your style. You can only choose the source type and id at creation time!": "スタイルに新規ソースを追加します。注意: 作成時にソースタイプとIDのみを選択できます。",
"TileJSON URL": "TileJSON URL",
"Tile URL": "タイルURL",
"Coord top left": "左上座標",
"Coord top right": "右上座標",
"Coord bottom right": "右下座標",
"Coord bottom left": "左下座標",
"Image URL": "画像URL",
"Video URL": "動画URL",
"GeoJSON URL": "GeoJSON URL",
"GeoJSON": "GeoJSON",
"Cluster": "クラスタ",
"Encoding": "エンコーディング",
"Error:": "エラー:",
"MapTiler Access Token": "MapTiler アクセストークン",
"Public access token for MapTiler Cloud.": "MapTiler Cloud の公開用アクセストークン",
"Thunderforest Access Token": "Thunderforest アクセストークン",
"Public access token for Thunderforest services.": "Thunderforest サービスの公開用アクセストークン",
"Style Renderer": "スタイルレンダラ",
"Choose the default Maputnik renderer for this style.": "このスタイルのデフォルトの Maputnik レンダラを選択してください",
"Layer options": "レイヤー設定",
"Paint properties": "ペイントプロパティ",
"Layout properties": "レイアウトプロパティ",
"General layout properties": "一般レイアウトプロパティ",
"Text layout properties": "文字レイアウトプロパティ",
"Icon layout properties": "アイコンレイアウトプロパティ",
"Text paint properties": "文字ペイントプロパティ",
"Icon paint properties": "アイコンペイントプロパティ"
}
-192
View File
@@ -1,192 +0,0 @@
{
"Input value": "输入值",
"Data value": "数据值",
"Output value": "输出值",
"Function": "函数",
"Select a type of data scale (default is 'categorical').": "选择数据尺度的类型(默认为“分类”)。",
"Base": "基础",
"Input a data property to base styles off of.": "输入一个数据属性作为样式的基础。",
"Default": "默认",
"Stops": "停靠点",
"Zoom": "缩放",
"Add stop": "添加停靠点",
"Convert to expression": "转换为表达式",
"Remove zoom level from stop": "从停靠点移除缩放级别",
"Revert from expression": "从表达式恢复",
"Delete expression": "删除表达式",
"Convert property into a zoom function": "将属性转换为缩放函数",
"Convert property to data function": "将属性转换为数据函数",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "图层<1>{formatLayerId(layerId)}</1>: {parsed.data.message}",
"switch to layer": "切换到图层",
"Map": "地图",
"Inspect": "检查",
"Deuteranopia filter": "绿色盲滤镜",
"Protanopia filter": "红色盲滤镜",
"Tritanopia filter": "蓝色盲滤镜",
"Achromatopsia filter": "全色盲滤镜",
"Layers list": "图层列表",
"Layer editor": "图层编辑器",
"Map view": "地图视图",
"Maputnik on GitHub": "GitHub上的Maputnik",
"Open": "打开",
"Export": "导出",
"Data Sources": "数据源",
"Style Settings": "样式设置",
"View": "视图",
"Color accessibility": "颜色可访问性",
"Language": "语言",
"Help": "帮助",
"Comments for the current layer. This is non-standard and not in the spec.": "当前图层的注释。注意:这不是标准功能,可能与其他库不兼容。",
"Comments": "注释",
"Comment...": "写注释...",
"Max Zoom": "最大缩放",
"Min Zoom": "最小缩放",
"Source": "源",
"Source Layer": "源图层",
"Type": "类型",
"Nested filters are not supported.": "不支持嵌套过滤器。",
"Upgrade to expression": "升级为表达式",
"Filter": "过滤器",
"every filter matches": "每个过滤器都匹配",
"no filter matches": "没有过滤器匹配",
"any filter matches": "任何过滤器匹配",
"Add filter": "添加过滤器",
"You've entered an old style filter.": "您输入了一个旧风格的过滤器。",
"Switch to filter editor.": "切换到过滤器编辑器。",
"Delete filter block": "删除过滤器块",
"Add value": "添加值",
"Remove array item": "移除数组项目",
"Press <1>ESC</1> to lose focus": "按 <1>ESC</1> 键退出焦点",
"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> 域名。",
"Layer": "图层",
"JSON Editor": "JSON编辑器",
"Delete": "删除",
"Duplicate": "复制",
"Show": "显示",
"Hide": "隐藏",
"Move layer up": "向上移动图层",
"Move layer down": "向下移动图层",
"Layer: {{layerId}}": "图层: {{layerId}}",
"Layers": "图层",
"Collapse": "折叠",
"Expand": "展开",
"Add Layer": "添加图层",
"Zoom:": "缩放:",
"Search": "搜索",
"Close popup": "关闭弹出窗口",
"cursor:": "光标:",
"center:": "中心:",
"rotation:": "旋转:",
"Close modal": "关闭模态框",
"Debug": "调试",
"Options": "选项",
"<0>Open in OSM</0> — Opens the current view on openstreetmap.org": "在 openstreetmap.org 打开当前视图",
"Export Style": "导出样式",
"Download Style": "下载样式",
"Download a JSON style to your computer.": "将JSON样式下载到您的电脑。",
"Download HTML": "下载HTML",
"Cancel": "取消",
"Open Style": "打开样式",
"Upload Style": "上传样式",
"Upload a JSON style from your computer.": "从您的电脑上传JSON样式。",
"Style file": "样式文件",
"Upload": "上传",
"Load from URL": "从URL加载",
"Load from a URL. Note that the URL must have <1>CORS enabled</1>.": "从URL加载。注意:URL必须启用 <1>CORS</1>。",
"Style URL": "样式URL",
"Enter URL...": "输入URL...",
"Gallery Styles": "画廊样式",
"Open one of the publicly available styles to start from.": "打开一个公开可用的样式开始。",
"Loading style": "正在加载样式",
"Loading: {{requestUrl}}": "正在加载: {{requestUrl}}",
"Name": "名称",
"Owner": "所有者",
"Owner ID of the style. Used by Mapbox or future style APIs.": "样式的所有者ID。由Mapbox或未来的样式API使用。",
"Sprite URL": "精灵URL",
"Glyphs URL": "字形URL",
"Center": "中心",
"Bearing": "方位",
"Pitch": "俯仰角",
"Light anchor": "光源锚点",
"Light color": "光源颜色",
"Light intensity": "光源强度",
"Light position": "光源位置",
"Terrain source": "地形源",
"Terrain exaggeration": "地形夸张",
"Transition delay": "过渡延迟",
"Transition duration": "过渡持续时间",
"Open Layers (experimental)": "开放图层(实验性)",
"Shortcuts menu": "快捷方式菜单",
"Open modal": "打开模态框",
"Export modal": "导出模态框",
"Data Sources modal": "数据源模态框",
"Style Settings modal": "样式设置模态框",
"Toggle inspect": "切换检查",
"Focus map": "聚焦地图",
"Debug modal": "调试模态框",
"Increase the zoom level by 1.": "将缩放级别增加1。",
"Increase the zoom level by 2.": "将缩放级别增加2。",
"Decrease the zoom level by 1.": "将缩放级别减少1。",
"Decrease the zoom level by 2.": "将缩放级别减少2。",
"Pan up by 100 pixels.": "向上平移100像素。",
"Pan down by 100 pixels.": "向下平移100像素。",
"Pan left by 100 pixels.": "向左平移100像素。",
"Pan right by 100 pixels.": "向右平移100像素。",
"Increase the rotation by 15 degrees.": "将旋转角度增加15度。",
"Decrease the rotation by 15 degrees.": "将旋转角度减少15度。",
"Increase the pitch by 10 degrees.": "将俯仰角增加10度。",
"Decrease the pitch by 10 degrees.": "将俯仰角减少10度。",
"Shortcuts": "快捷键",
"Press <1>ESC</1> to lose focus of any active elements, then press one of:": "按下 <1>ESC</1> 退出任何活动元素的焦点,然后按下以下之一:",
"If the Map is in focused you can use the following shortcuts": "如果地图处于焦点状态,您可以使用以下快捷键",
"Remove '{{sourceId}}' source": "移除 '{{sourceId}}' 源",
"Source ID": "源ID",
"Unique ID that identifies the source and is used in the layer to reference the source.": "用于标识源并在图层中引用源的唯一ID。",
"Source Type": "源类型",
"GeoJSON (JSON)": "GeoJSON (JSON格式)",
"GeoJSON (URL)": "GeoJSON (URL格式)",
"Vector (TileJSON URL)": "矢量数据 (TileJSON URL)",
"Vector (XYZ URLs)": "矢量数据 (XYZ URLs)",
"Raster (TileJSON URL)": "栅格数据 (TileJSON URL)",
"Raster (XYZ URL)": "栅格数据 (XYZ URL)",
"Raster DEM (TileJSON URL)": "栅格高程数据 (TileJSON URL)",
"Raster DEM (XYZ URLs)": "栅格高程数据 (XYZ URLs)",
"Image": "图像",
"Video": "视频",
"Add Source": "添加源",
"Sources": "源列表",
"Active Sources": "激活的源",
"Choose Public Source": "选择公共源",
"Add one of the publicly available sources to your style.": "将公开可用的源之一添加到您的样式中。",
"Add New Source": "添加新源",
"Add a new source to your style. You can only choose the source type and id at creation time!": "向您的样式添加新源。在创建时,您只能选择源类型和ID!",
"TileJSON URL": "TileJSON URL",
"Tile URL": "瓦片URL",
"Coord top left": "左上角坐标",
"Coord top right": "右上角坐标",
"Coord bottom right": "右下角坐标",
"Coord bottom left": "左下角坐标",
"Image URL": "图像URL",
"Video URL": "视频URL",
"GeoJSON URL": "GeoJSON URL",
"GeoJSON": "GeoJSON",
"Cluster": "聚合",
"Encoding": "编码",
"Error:": "错误:",
"MapTiler Access Token": "MapTiler 访问令牌",
"Public access token for MapTiler Cloud.": "MapTiler Cloud 的公共访问令牌。",
"Thunderforest Access Token": "Thunderforest 访问令牌",
"Public access token for Thunderforest services.": "Thunderforest 服务的公共访问令牌。",
"Style Renderer": "样式渲染器",
"Choose the default Maputnik renderer for this style.": "为这种样式选择默认的Maputnik渲染器。",
"Layer options": "图层选项",
"Paint properties": "绘制属性",
"Layout properties": "布局属性",
"General layout properties": "常规布局属性",
"Text layout properties": "文本布局属性",
"Icon layout properties": "图标布局属性",
"Text paint properties": "文本绘制属性",
"Icon paint properties": "图标绘制属性"
}
+5
View File
@@ -3,6 +3,11 @@
.maputnik-map__container {
background: white;
display: flex;
position: fixed !important;
top: $toolbar-height + $toolbar-offset;
right: 0;
bottom: 0;
height: calc(100% - #{$toolbar-height + $toolbar-offset});
width: $layout-map-width;
&--error {
+7 -9
View File
@@ -14,28 +14,26 @@
font-family: $font-family;
color: $color-white;
&-main {
&-list {
position: fixed;
bottom: 0;
height: calc(100% - #{$toolbar-height + $toolbar-offset});
top: $toolbar-height + $toolbar-offset;
left: 0;
right: 0;
z-index: 3;
display: flex;
}
&-list {
width: 200px;
background-color: $color-black;
}
&-drawer {
position: fixed;
bottom: 0;
height: calc(100% - #{$toolbar-height + $toolbar-offset});
top: $toolbar-height + $toolbar-offset;
left: 200px;
z-index: 1;
width: 370px;
background-color: $color-black;
// scroll-container is position: absolute
position: relative;
}
&-bottom {
+20
View File
@@ -308,6 +308,26 @@
}
}
.maputnik-modal-survey {
width: 400px;
}
.maputnik-modal-survey__logo {
display: block;
margin: 0 auto;
height: 128px;
width: 128px;
}
.maputnik-modal-survey__description {
line-height: 1.5;
}
.maputnik-modal-survey__footnote {
color: $color-green;
margin-top: 16px;
}
.modal-settings {
width: 400px;
}
+7 -6
View File
@@ -37,11 +37,12 @@
}
}
.maputnik-popup-table {
width: 100%;
}
.maputnik-popup-table-cell {
.maputnik-popup-feature-left {
margin-left: $margin-2;
color: $color-lowgray;
flex: 1;
}
.maputnik-popup-feature-right {
flex: 1;
color: $color-lowgray;
padding-left: $margin-2;
}