mirror of
https://github.com/maputnik/editor.git
synced 2026-09-01 09:57:27 +00:00
Compare commits
134 Commits
b74e5d4c74
..
v2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 482e6ec545 | |||
| ca45bcce86 | |||
| 4dd34e99fd | |||
| 3773acd5be | |||
| 6887d70194 | |||
| 66c5a5c953 | |||
| 8184ac8393 | |||
| 6a0d2e8ee5 | |||
| 58edd262b0 | |||
| 35840409b8 | |||
| d0f6e0fadb | |||
| 0de304ca3e | |||
| 48bf25c1b0 | |||
| c82c6158e6 | |||
| 41cd7dfad1 | |||
| 7591b031ce | |||
| 95b5324fd3 | |||
| f34529ef06 | |||
| a73b11805d | |||
| ff15b77b7f | |||
| 355b663e7e | |||
| 3c043fd5e0 | |||
| 5f54dd0ccf | |||
| 3727f5da48 | |||
| 079c5f67cc | |||
| a304d4e060 | |||
| 7ac1b03b5a | |||
| b9e32894b3 | |||
| bc5ecfade6 | |||
| c84c7a7b96 | |||
| cb77c6b4e2 | |||
| ea42f434eb | |||
| 6f82c12861 | |||
| 3b95b25777 | |||
| 1da65f2116 | |||
| a62db148cd | |||
| 6ed10a862f | |||
| 123e84f19b | |||
| d9b1b6f3ae | |||
| e0cef99c07 | |||
| eb48bed32a | |||
| 87cf81d1c9 | |||
| 8e35ed97e6 | |||
| 7265bf0aa4 | |||
| c9504fcaed | |||
| b7ef0943f4 | |||
| 4661677387 | |||
| 77ed14a340 | |||
| e24d390f7c | |||
| 698fdfc958 | |||
| 77b3655c3c | |||
| c264cd1771 | |||
| 1495d11462 | |||
| fda11e52e7 | |||
| d9e3aa6ac4 | |||
| aeca95a27f | |||
| 7dfcdac202 | |||
| 4f156ee3fd | |||
| 6d00214f55 | |||
| 1e7b6e809c | |||
| cdcc61e234 | |||
| 9c63172d36 | |||
| 6f21fd8dff | |||
| 0e788c5841 | |||
| 7229df704a | |||
| 686fd27b35 | |||
| 293342e4fb | |||
| 03d9b946e7 | |||
| 4a3825fa89 | |||
| 5371b0f9fb | |||
| 538cea7f45 | |||
| fdfc470ccc | |||
| 3153eea1da | |||
| 8471d0af3d | |||
| 1ce2d59b9b | |||
| d951256b1c | |||
| ec753869d5 | |||
| 2e58be1c90 | |||
| 562a4f7322 | |||
| e52a63e1dd | |||
| 4533fd06ed | |||
| b4fc62632c | |||
| e3a9a8a38c | |||
| 7c4e982fb3 | |||
| 85dd22b09a | |||
| 18e15eeb5c | |||
| 3a45b8dd41 | |||
| 5b8412765b | |||
| 69519df82f | |||
| 8052701021 | |||
| 35c0150522 | |||
| c55278e7da | |||
| d3ecef3de6 | |||
| 3ef0a90de4 | |||
| 87290889fd | |||
| 1997e31b6b | |||
| 5b21a2fa4f | |||
| d314add6a9 | |||
| 50d61cdb0e | |||
| 4ffea21c5f | |||
| d29a79e79f | |||
| 004177a3c8 | |||
| b9e70a943f | |||
| de853eb2d7 | |||
| f242c2c015 | |||
| d895cf079c | |||
| 147cbc1580 | |||
| 3e46c0d3ba | |||
| d233e0a14d | |||
| a73b2fd7e1 | |||
| 69f63f2844 | |||
| 10136c07db | |||
| 5e3156ab21 | |||
| 6be8959951 | |||
| d2cd84de2b | |||
| 60bea1777a | |||
| ce9216b2d5 | |||
| 35ed202cd0 | |||
| 0d77518a02 | |||
| 11375008fa | |||
| 009f4e105d | |||
| b1af4917e5 | |||
| 5b712d74ae | |||
| ca2df37c79 | |||
| 7d1890156d | |||
| ecab640a9a | |||
| f8cb0619f3 | |||
| d874b2503b | |||
| 3d09f2a0f3 | |||
| 1f1580276d | |||
| 0421a7f099 | |||
| 8b722fc967 | |||
| 66e3ce8743 | |||
| 45eb3a01e6 |
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"packages": [],
|
||||
"sandboxes": ["/"]
|
||||
}
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
custom: "https://maputnik.github.io/donate"
|
||||
github: [maplibre]
|
||||
open_collective: maplibre
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
## 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.
|
||||
|
||||
+15
-27
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
|
||||
- run: docker build -t test-docker-image-build .
|
||||
|
||||
# build the editor
|
||||
build-node:
|
||||
@@ -53,52 +53,40 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run build-storybook
|
||||
- name: artifacts/editor
|
||||
uses: actions/upload-artifact@v1
|
||||
- name: artifacts/maputnik
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: editor
|
||||
name: maputnik
|
||||
path: dist
|
||||
- name: artifacts/storybook
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: storybook
|
||||
path: build/storybook
|
||||
|
||||
# Build and upload desktop CLI artifacts
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.19.x
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
id: go
|
||||
|
||||
- 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: Build desktop artifacts
|
||||
run: npm run build-desktop
|
||||
|
||||
- name: Artifacts/linux
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maputnik-linux
|
||||
path: ./src/github.com/maputnik/desktop/bin/linux/
|
||||
path: ./desktop/bin/linux/
|
||||
|
||||
- name: Artifacts/darwin
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maputnik-darwin
|
||||
path: ./src/github.com/maputnik/desktop/bin/darwin/
|
||||
path: ./desktop/bin/darwin/
|
||||
|
||||
- name: Artifacts/windows
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maputnik-windows
|
||||
path: ./src/github.com/maputnik/desktop/bin/windows/
|
||||
path: ./desktop/bin/windows/
|
||||
|
||||
e2e-tests:
|
||||
name: "E2E tests using ${{ matrix.browser }}"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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 }}
|
||||
@@ -3,24 +3,49 @@ name: deploy
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
deploy-pages:
|
||||
name: deploy/pages
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: dist
|
||||
|
||||
# publish docker to GitHub registry
|
||||
deploy-docker:
|
||||
name: deploy/docker
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
|
||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
|
||||
- run: docker push docker.pkg.github.com/maputnik/editor/editor:main
|
||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
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
@@ -33,6 +33,6 @@ node_modules
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
/cypress/screenshots
|
||||
/dist/
|
||||
/desktop/version.go
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
const config = {
|
||||
stories: ['../stories/**/*.stories.jsx'],
|
||||
addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-a11y/register', '@storybook/addon-storysource'],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
@@ -1,7 +0,0 @@
|
||||
import { addons } from '@storybook/addons';
|
||||
import { themes } from '@storybook/theming';
|
||||
import theme from './maputnik.theme';
|
||||
|
||||
addons.setConfig({
|
||||
theme: theme,
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import { create } from '@storybook/theming/create';
|
||||
|
||||
export default create({
|
||||
base: 'light',
|
||||
|
||||
brandTitle: 'Maputnik',
|
||||
brandUrl: 'https://github.com/maputnik/editor',
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
## main
|
||||
|
||||
### ✨ Features and improvements
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
- _...Add new stuff here..._
|
||||
|
||||
## 2.1.1
|
||||
|
||||
### ✨ 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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Contributor Covenant
|
||||
[](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)
|
||||
+3
-3
@@ -2,12 +2,12 @@ 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 ./
|
||||
RUN npm install
|
||||
COPY package.json package-lock.json .npmrc ./
|
||||
RUN npm ci
|
||||
|
||||
# Build maputnik
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
RUN npx vite build
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Create a clean nginx-alpine slim image with just the build results
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Lukas Martinelli
|
||||
Copyright (c) 2024 MapLibre contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
||||
|
||||
# Maputnik
|
||||
[][github-action-ci]
|
||||
[][github-action-ci]
|
||||
[][license]
|
||||
|
||||
[github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
|
||||
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci
|
||||
[license]: https://tldrlegal.com/license/mit-license
|
||||
|
||||
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
|
||||
@@ -13,33 +13,32 @@ targeted at developers and map designers.
|
||||
|
||||
## Usage
|
||||
|
||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
||||
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage)
|
||||
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/wiki/Maputnik-CLI) for local style development
|
||||
- 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:8888 maputnik/editor
|
||||
docker run -it --rm -p 8888:80 ghcr.io/maplibre/maputnik:main
|
||||
```
|
||||
|
||||
## Donations
|
||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
||||
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
||||
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
||||
|
||||
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
|
||||
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)**
|
||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||
|
||||
[](https://youtu.be/XoDh0gEnBQo)
|
||||
|
||||
## Develop
|
||||
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
|
||||
Maputnik is written in typescript and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
|
||||
|
||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
||||
|
||||
### Getting Involved
|
||||
Join the #maplibre or #maputnik slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/ Read the the below guide in order to get familiar with how we do things around here.
|
||||
|
||||
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
||||
|
||||
```bash
|
||||
@@ -67,7 +66,8 @@ Lint the JavaScript code.
|
||||
```
|
||||
# run linter
|
||||
npm run lint
|
||||
npm run lint-styles
|
||||
npm run lint-css
|
||||
npm run sort-styles
|
||||
```
|
||||
|
||||
|
||||
@@ -94,68 +94,23 @@ You can also see the tests as they run or select which suites to run by executin
|
||||
npm run cy:open
|
||||
```
|
||||
|
||||
## Release process
|
||||
|
||||
## Related Projects
|
||||
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.
|
||||
|
||||
- [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
|
||||
|
||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
||||
|
||||
### Gold
|
||||
|
||||
- [Wemap](https://getwemap.com/)
|
||||
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
||||
- [Terranodo](http://terranodo.io/)
|
||||
|
||||
<a href="https://getwemap.com/">
|
||||
<img width="33%" alt="Wemap" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/wemap.jpg" />
|
||||
</a>
|
||||
<a href="http://terranodo.io/">
|
||||
<img width="33%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/terranodo.png" />
|
||||
</a>
|
||||
<a href="https://www.orbiconinformatik.dk/">
|
||||
<img width="32%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/orbicon_informatik.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Silver
|
||||
|
||||
- [Klokan Technologies](https://www.klokantech.com/)
|
||||
- [Geofabrik](http://www.geofabrik.de/)
|
||||
- [Dreipol](https://www.dreipol.ch/)
|
||||
|
||||
<a href="https://www.klokantech.com/">
|
||||
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/klokantech.png" />
|
||||
</a>
|
||||
<a href="http://www.geofabrik.de/">
|
||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/geofabrik.png" />
|
||||
</a>
|
||||
<a href="https://www.dreipol.ch/">
|
||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/dreipol.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Individuals
|
||||
|
||||
**Influential Stakeholder**
|
||||
|
||||
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
||||
|
||||
**Stakeholder**
|
||||
|
||||
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
|
||||
|
||||
**Supporter**
|
||||
|
||||
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
||||
You can see this file's history for previous sponsors of the original Maputnik repo.
|
||||
Read more about the MapLibre Sponsorship Program at https://maplibre.org/sponsors/.
|
||||
|
||||
## License
|
||||
|
||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
||||
|
||||
**Disclaimer** This is an independent style editor.
|
||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and Maplibre contributors.
|
||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by other map studios and make your own decisions for a good style editor.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
For an up-to-date policy refer to
|
||||
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt
|
||||
@@ -0,0 +1,11 @@
|
||||
# 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
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/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');
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/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());
|
||||
@@ -87,4 +87,39 @@ describe("history", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should not redo after undo and value change", () => {
|
||||
when.setStyle("geojson");
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.typeKeys(undoKeyCombo);
|
||||
when.typeKeys(undoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "step 3",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.typeKeys(redoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "step 3",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
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("スタイル設定");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -266,7 +266,7 @@ describe("layers", () => {
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.click("spec-field:background-color", 0);
|
||||
when.click("spec-field:background-color");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
@@ -280,6 +280,25 @@ 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", () => {
|
||||
|
||||
@@ -23,4 +23,10 @@ describe("map", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("search", () => {
|
||||
it('should exist', () => {
|
||||
then(get.searchControl()).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/// <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";
|
||||
@@ -14,7 +16,7 @@ const styleFromWindow = (win: Window) => {
|
||||
export class MaputnikAssertable<T> extends Assertable<T> {
|
||||
shouldEqualToStoredStyle = () =>
|
||||
then(
|
||||
new CypressHelper().get.window().then((win) => {
|
||||
new CypressHelper().get.window().then((win: Window) => {
|
||||
const style = styleFromWindow(win);
|
||||
then(this.chainable).shouldDeepNestedInclude(style);
|
||||
})
|
||||
@@ -129,7 +131,9 @@ export class MaputnikDriver {
|
||||
this.helper.when.acceptConfirm();
|
||||
}
|
||||
// when methods should not include assertions
|
||||
this.helper.get.elementByTestId("toolbar:link").should("be.visible");
|
||||
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
|
||||
toolbarLink.scrollIntoView();
|
||||
toolbarLink.should("be.visible");
|
||||
},
|
||||
|
||||
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
|
||||
@@ -177,5 +181,6 @@ export class MaputnikDriver {
|
||||
skipTargetLayerEditor: () =>
|
||||
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
||||
canvas: () => this.helper.get.element("canvas"),
|
||||
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,12 +79,12 @@ describe("modals", () => {
|
||||
when.click("nav:settings");
|
||||
});
|
||||
|
||||
describe("when click name", () => {
|
||||
describe("when click name filed spec information", () => {
|
||||
beforeEach(() => {
|
||||
when.click("field-doc-button-Name");
|
||||
});
|
||||
|
||||
it("name", () => {
|
||||
it("should show the spec information", () => {
|
||||
then(get.elementsText("spec-field-doc")).shouldInclude(
|
||||
"name for the style"
|
||||
);
|
||||
@@ -142,7 +142,7 @@ describe("modals", () => {
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().pipe((style) => style.metadata)
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
).shouldInclude({
|
||||
"maputnik:openmaptiles_access_token": apiKey,
|
||||
});
|
||||
@@ -156,7 +156,7 @@ describe("modals", () => {
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().pipe((style) => style.metadata)
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# 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
|
||||
@@ -0,0 +1,21 @@
|
||||
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.
|
||||
@@ -0,0 +1,39 @@
|
||||
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
|
||||
@@ -0,0 +1,72 @@
|
||||
# Maputnik Desktop [][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.
|
||||
@@ -0,0 +1,81 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
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=
|
||||
@@ -0,0 +1,80 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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__';
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Generated
+3323
-10758
File diff suppressed because it is too large
Load Diff
+60
-60
@@ -1,41 +1,49 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "2.0.0-pre.2",
|
||||
"version": "2.1.1",
|
||||
"description": "A MapLibre GL visual style editor",
|
||||
"type": "module",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"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\"",
|
||||
"storybook": "storybook dev -h 0.0.0.0 -p 6006",
|
||||
"build-storybook": "storybook build -o build/storybook"
|
||||
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maputnik/editor"
|
||||
"url": "https://github.com/maplibre/maputnik"
|
||||
},
|
||||
"author": "Lukas Martinelli",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||
"dependencies": {
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
|
||||
"@mdi/js": "^6.6.96",
|
||||
"@mdi/react": "^1.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"@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",
|
||||
"array-move": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"classnames": "^2.5.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",
|
||||
"json-stringify-pretty-compact": "^3.0.0",
|
||||
"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-to-ast": "^2.1.0",
|
||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -45,30 +53,30 @@
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mapbox-gl-inspect": "^1.3.1",
|
||||
"maplibre-gl": "^2.4.0",
|
||||
"maplibre-gl": "^4.1.2",
|
||||
"maputnik-design": "github:maputnik/design#172b06c",
|
||||
"ol": "^6.14.1",
|
||||
"ol-mapbox-style": "^7.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^16.0.0",
|
||||
"react-accessible-accordion": "^4.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-accessible-accordion": "^5.0.0",
|
||||
"react-aria-menubutton": "^7.0.3",
|
||||
"react-aria-modal": "^4.0.1",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"react-autobind": "^1.0.6",
|
||||
"react-autocomplete": "^1.8.1",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-file-reader-input": "^2.0.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-icon-base": "^2.1.2",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"sass": "^1.50.0",
|
||||
"slugify": "^1.6.5",
|
||||
"sass": "^1.72.0",
|
||||
"slugify": "^1.6.6",
|
||||
"string-hash": "^1.1.3",
|
||||
"url": "^0.11.0"
|
||||
"url": "^0.11.3"
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
@@ -88,24 +96,15 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/code-coverage": "^3.12.15",
|
||||
"@cypress/code-coverage": "^3.12.30",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@shellygo/cypress-test-utils": "^2.0.17",
|
||||
"@storybook/addon-a11y": "^7.6.5",
|
||||
"@storybook/addon-actions": "^7.6.5",
|
||||
"@storybook/addon-links": "^7.6.5",
|
||||
"@storybook/addon-storysource": "^7.6.5",
|
||||
"@storybook/addons": "^7.6.5",
|
||||
"@storybook/builder-vite": "^7.6.5",
|
||||
"@storybook/react": "^7.6.5",
|
||||
"@storybook/react-vite": "^7.6.5",
|
||||
"@storybook/theming": "^7.6.5",
|
||||
"@shellygo/cypress-test-utils": "^2.1.9",
|
||||
"@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.13",
|
||||
"@types/geojson": "^7946.0.14",
|
||||
"@types/json-to-ast": "^2.1.4",
|
||||
"@types/lodash.capitalize": "^4.2.9",
|
||||
"@types/lodash.clamp": "^4.0.9",
|
||||
@@ -113,37 +112,38 @@
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@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/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-collapse": "^5.0.4",
|
||||
"@types/react-color": "^3.0.10",
|
||||
"@types/react-dom": "^16.9.24",
|
||||
"@types/react-color": "^3.0.12",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@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.7",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"cypress": "^13.6.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"cypress": "^13.13.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"express": "^4.17.3",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"i18next-parser": "^9.0.1",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^3.2.0",
|
||||
"mocha": "^9.2.2",
|
||||
"postcss": "^8.4.12",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"storybook": "^7.6.5",
|
||||
"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.0",
|
||||
"vite-plugin-istanbul": "^5.0.0"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-16
@@ -9,7 +9,7 @@ import {unset} from 'lodash'
|
||||
import {arrayMoveMutable} from 'array-move'
|
||||
import hash from "string-hash";
|
||||
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
|
||||
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
|
||||
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
|
||||
|
||||
import MapMaplibreGl from './MapMaplibreGl'
|
||||
import MapOpenLayers from './MapOpenLayers'
|
||||
@@ -24,7 +24,6 @@ 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'
|
||||
@@ -128,7 +127,6 @@ type AppState = {
|
||||
open: boolean
|
||||
shortcuts: boolean
|
||||
export: boolean
|
||||
survey: boolean
|
||||
debug: boolean
|
||||
}
|
||||
}
|
||||
@@ -137,7 +135,6 @@ 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)
|
||||
@@ -277,7 +274,6 @@ 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: {
|
||||
@@ -379,8 +375,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
this.getInitialStateFromUrl(newStyle);
|
||||
}
|
||||
|
||||
// This "any" can be removed in latest version of maplibre where maplibre re-exported types from style-spec
|
||||
const errors = validate(newStyle as any, latest) || [];
|
||||
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
|
||||
|
||||
// The validate function doesn't give us errors for duplicate error with
|
||||
// empty string for layer.id, manually deal with that here.
|
||||
@@ -840,10 +835,6 @@ 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,
|
||||
@@ -943,7 +934,6 @@ 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')}
|
||||
/>
|
||||
@@ -971,10 +961,6 @@ 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
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type AppLayoutProps = {
|
||||
type AppLayoutInternalProps = {
|
||||
toolbar: React.ReactElement
|
||||
layerList: React.ReactElement
|
||||
layerEditor?: React.ReactElement
|
||||
map: React.ReactElement
|
||||
bottom?: React.ReactElement
|
||||
modals?: React.ReactNode
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class AppLayout extends React.Component<AppLayoutProps> {
|
||||
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
@@ -23,17 +24,21 @@ class AppLayout extends React.Component<AppLayoutProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
document.body.dir = this.props.i18n.dir();
|
||||
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
<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>
|
||||
<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>
|
||||
@@ -43,4 +48,5 @@ class AppLayout extends React.Component<AppLayoutProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export default AppLayout
|
||||
const AppLayout = withTranslation()(AppLayoutInternal);
|
||||
export default AppLayout;
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import React from 'react'
|
||||
import {formatLayerId} from '../libs/format';
|
||||
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type AppMessagePanelProps = {
|
||||
type AppMessagePanelInternalProps = {
|
||||
errors?: unknown[]
|
||||
infos?: unknown[]
|
||||
infos?: string[]
|
||||
mapStyle?: StyleSpecification
|
||||
onLayerSelect?(...args: unknown[]): unknown
|
||||
currentLayer?: LayerSpecification
|
||||
selectedLayerIndex?: number
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
|
||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {selectedLayerIndex} = this.props;
|
||||
const {t, selectedLayerIndex} = this.props;
|
||||
const errors = this.props.errors?.map((error: any, idx) => {
|
||||
let content;
|
||||
if (error.parsed && error.parsed.type === "layer") {
|
||||
@@ -25,7 +26,9 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
|
||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
||||
content = (
|
||||
<>
|
||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
<Trans t={t}>
|
||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
</Trans>
|
||||
{selectedLayerIndex !== parsed.data.index &&
|
||||
<>
|
||||
—
|
||||
@@ -33,7 +36,7 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
|
||||
className="maputnik-message-panel__switch-button"
|
||||
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
||||
>
|
||||
switch to layer
|
||||
{t("switch to layer")}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
@@ -59,3 +62,5 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
|
||||
}
|
||||
}
|
||||
|
||||
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
|
||||
export default AppMessagePanel;
|
||||
|
||||
@@ -2,9 +2,12 @@ import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {detect} from 'detect-browser';
|
||||
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdLanguage} 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();
|
||||
@@ -43,29 +46,6 @@ class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
||||
}
|
||||
}
|
||||
|
||||
type ToolbarLinkHighlightedProps = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
href?: string
|
||||
onToggleModal?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
class ToolbarLinkHighlighted extends React.Component<ToolbarLinkHighlightedProps> {
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
data-wd-key="toolbar:link-highlighted"
|
||||
>
|
||||
<span className="maputnik-toolbar-link-wrapper">
|
||||
{this.props.children}
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
type ToolbarSelectProps = {
|
||||
children?: React.ReactNode
|
||||
wdKey?: string
|
||||
@@ -102,7 +82,7 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
|
||||
|
||||
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
||||
|
||||
type AppToolbarProps = {
|
||||
type AppToolbarInternalProps = {
|
||||
mapStyle: object
|
||||
inspectModeEnabled: boolean
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
@@ -115,9 +95,9 @@ type AppToolbarProps = {
|
||||
onSetMapState(mapState: MapState): unknown
|
||||
mapState?: MapState
|
||||
renderer?: string
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
state = {
|
||||
isOpen: {
|
||||
settings: false,
|
||||
@@ -132,6 +112,10 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
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();
|
||||
@@ -143,40 +127,41 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const views = [
|
||||
{
|
||||
id: "map",
|
||||
group: "general",
|
||||
title: "Map",
|
||||
title: t("Map"),
|
||||
},
|
||||
{
|
||||
id: "inspect",
|
||||
group: "general",
|
||||
title: "Inspect",
|
||||
title: t("Inspect"),
|
||||
disabled: this.props.renderer === 'ol',
|
||||
},
|
||||
{
|
||||
id: "filter-deuteranopia",
|
||||
group: "color-accessibility",
|
||||
title: "Deuteranopia filter",
|
||||
title: t("Deuteranopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-protanopia",
|
||||
group: "color-accessibility",
|
||||
title: "Protanopia filter",
|
||||
title: t("Protanopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-tritanopia",
|
||||
group: "color-accessibility",
|
||||
title: "Tritanopia filter",
|
||||
title: t("Tritanopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-achromatopsia",
|
||||
group: "color-accessibility",
|
||||
title: "Achromatopsia filter",
|
||||
title: t("Achromatopsia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
];
|
||||
@@ -196,29 +181,29 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-list")}
|
||||
>
|
||||
Layers list
|
||||
{t("Layers list")}
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:layer-editor"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-editor")}
|
||||
>
|
||||
Layer editor
|
||||
{t("Layer editor")}
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:map-view"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("map")}
|
||||
>
|
||||
Map view
|
||||
{t("Map view")}
|
||||
</button>
|
||||
<a
|
||||
className="maputnik-toolbar-logo"
|
||||
target="blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://github.com/maputnik/editor"
|
||||
href="https://github.com/maplibre/maputnik"
|
||||
>
|
||||
<img src="node_modules/maputnik-design/logos/logo-color.svg" />
|
||||
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
|
||||
<h1>
|
||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||
@@ -228,24 +213,24 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
||||
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||
<MdOpenInBrowser />
|
||||
<IconText>Open</IconText>
|
||||
<IconText>{t("Open")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>Export</IconText>
|
||||
<IconText>{t("Export")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||
<MdLayers />
|
||||
<IconText>Data Sources</IconText>
|
||||
<IconText>{t("Data Sources")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<MdSettings />
|
||||
<IconText>Style Settings</IconText>
|
||||
<IconText>{t("Style Settings")}</IconText>
|
||||
</ToolbarAction>
|
||||
|
||||
<ToolbarSelect wdKey="nav:inspect">
|
||||
<MdFindInPage />
|
||||
<label>View
|
||||
<label>{t("View")}
|
||||
<select
|
||||
className="maputnik-select"
|
||||
data-wd-key="maputnik-select"
|
||||
@@ -259,7 +244,7 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
<optgroup label="Color accessibility">
|
||||
<optgroup label={t("Color accessibility")}>
|
||||
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||
@@ -272,16 +257,35 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
</label>
|
||||
</ToolbarSelect>
|
||||
|
||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||
<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>Help</IconText>
|
||||
<IconText>{t("Help")}</IconText>
|
||||
</ToolbarLink>
|
||||
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
||||
<MdAssignmentTurnedIn />
|
||||
<IconText>Take the Maputnik Survey</IconText>
|
||||
</ToolbarLinkHighlighted>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
const AppToolbar = withTranslation()(AppToolbarInternal);
|
||||
export default AppToolbar;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, {SyntheticEvent} from 'react'
|
||||
import React, {PropsWithChildren, SyntheticEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
|
||||
|
||||
type BlockProps = {
|
||||
type BlockProps = PropsWithChildren & {
|
||||
"data-wd-key"?: string
|
||||
label?: string
|
||||
action?: React.ReactElement
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class Doc extends React.Component<DocProps> {
|
||||
const renderValues = (
|
||||
!!values &&
|
||||
// HACK: Currently we merge additional values into the style spec, so this is required
|
||||
// See <https://github.com/maputnik/editor/blob/main/src/components/PropertyGroup.jsx#L16>
|
||||
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16>
|
||||
!Array.isArray(values)
|
||||
);
|
||||
|
||||
|
||||
@@ -2,21 +2,23 @@ import React from 'react'
|
||||
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldCommentProps = {
|
||||
type FieldCommentInternalProps = {
|
||||
value?: string
|
||||
onChange(value: string | undefined): unknown
|
||||
error: {message: string}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldComment extends React.Component<FieldCommentProps> {
|
||||
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const fieldSpec = {
|
||||
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
|
||||
};
|
||||
|
||||
return <Block
|
||||
label={"Comments"}
|
||||
label={t("Comments")}
|
||||
fieldSpec={fieldSpec}
|
||||
data-wd-key="layer-comment"
|
||||
error={this.props.error}
|
||||
@@ -25,9 +27,12 @@ export default class FieldComment extends React.Component<FieldCommentProps> {
|
||||
multi={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
default="Comment..."
|
||||
default={t("Comment...")}
|
||||
data-wd-key="layer-comment.input"
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldComment = withTranslation()(FieldCommentInternal);
|
||||
export default FieldComment;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||
|
||||
type FieldDocLabelProps = {
|
||||
label: object | string | undefined
|
||||
label: JSX.Element | string | undefined
|
||||
fieldSpec?: {
|
||||
doc?: string
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
|
||||
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
|
||||
// Because otherwise when editing values we end up accidentally changing field type.
|
||||
if (state.isEditing) {
|
||||
return {};
|
||||
@@ -399,7 +399,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}>
|
||||
{specField}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
|
||||
@@ -13,7 +13,8 @@ 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}
|
||||
>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import React from 'react'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
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 FieldMaxZoomProps = {
|
||||
type FieldMaxZoomInternalProps = {
|
||||
value?: number
|
||||
onChange(value: number | undefined): unknown
|
||||
error?: {message: string}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
|
||||
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
|
||||
render() {
|
||||
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
|
||||
error={this.props.error}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
@@ -28,3 +30,6 @@ export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
|
||||
export default FieldMaxZoom;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import React from 'react'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
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 FieldMinZoomProps = {
|
||||
type FieldMinZoomInternalProps = {
|
||||
value?: number
|
||||
onChange(...args: unknown[]): unknown
|
||||
error?: {message: string}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
|
||||
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
|
||||
render() {
|
||||
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
|
||||
error={this.props.error}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
@@ -28,3 +30,6 @@ export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
||||
export default FieldMinZoom;
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import React from 'react'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
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 FieldSourceProps = {
|
||||
type FieldSourceInternalProps = {
|
||||
value?: string
|
||||
wdKey?: string
|
||||
onChange?(value: string| undefined): unknown
|
||||
sourceIds?: unknown[]
|
||||
error?: {message: string}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldSource extends React.Component<FieldSourceProps> {
|
||||
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block
|
||||
label={"Source"}
|
||||
label={t("Source")}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={this.props.error}
|
||||
data-wd-key={this.props.wdKey}
|
||||
@@ -33,3 +35,6 @@ export default class FieldSource extends React.Component<FieldSourceProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||
export default FieldSource;
|
||||
|
||||
@@ -3,16 +3,17 @@ 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 FieldSourceLayerProps = {
|
||||
type FieldSourceLayerInternalProps = {
|
||||
value?: string
|
||||
onChange?(...args: unknown[]): unknown
|
||||
sourceLayerIds?: unknown[]
|
||||
isFixed?: boolean
|
||||
error?: {message: string}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
|
||||
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
@@ -20,8 +21,9 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block
|
||||
label={"Source Layer"}
|
||||
label={t("Source Layer")}
|
||||
fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={this.props.error}
|
||||
@@ -35,3 +37,6 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||
export default FieldSourceLayer;
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import React from 'react'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
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 FieldTypeProps = {
|
||||
type FieldTypeInternalProps = {
|
||||
value: string
|
||||
wdKey?: string
|
||||
onChange(value: string): unknown
|
||||
error?: {message: string}
|
||||
disabled?: boolean
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldType extends React.Component<FieldTypeProps> {
|
||||
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block label={"Type"} fieldSpec={latest.layer.type}
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Type")} fieldSpec={latest.layer.type}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
>
|
||||
@@ -50,3 +52,6 @@ export default class FieldType extends React.Component<FieldTypeProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldType = withTranslation()(FieldTypeInternal);
|
||||
export default FieldType;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { PropsWithChildren, ReactElement } from 'react'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
|
||||
type FieldsetProps = {
|
||||
type FieldsetProps = PropsWithChildren & {
|
||||
label?: string,
|
||||
fieldSpec?: { doc?: string },
|
||||
action?: ReactElement,
|
||||
|
||||
@@ -13,9 +13,10 @@ 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: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
const filter = props.filter || ['all'];
|
||||
|
||||
if (!Array.isArray(filter)) {
|
||||
@@ -47,7 +48,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
|
||||
"sources": {
|
||||
"tmp": {
|
||||
"type": "geojson",
|
||||
"data": {}
|
||||
"data": ''
|
||||
}
|
||||
},
|
||||
"sprite": "",
|
||||
@@ -89,13 +90,13 @@ function hasNestedCombiningFilter(filter: LegacyFilterSpecification | Expression
|
||||
return false
|
||||
}
|
||||
|
||||
type FilterEditorProps = {
|
||||
type FilterEditorInternalProps = {
|
||||
/** 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
|
||||
@@ -103,12 +104,12 @@ type FilterEditorState = {
|
||||
valueIsSimpleFilter?: boolean
|
||||
};
|
||||
|
||||
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
|
||||
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
|
||||
static defaultProps = {
|
||||
filter: ["all"],
|
||||
}
|
||||
|
||||
constructor (props: FilterEditorProps) {
|
||||
constructor (props: FilterEditorInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
@@ -155,17 +156,17 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
})
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
|
||||
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
|
||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||
|
||||
// Upgrade but never downgrade
|
||||
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
|
||||
if (!displaySimpleFilter && state.displaySimpleFilter === true) {
|
||||
return {
|
||||
displaySimpleFilter: false,
|
||||
valueIsSimpleFilter: false,
|
||||
};
|
||||
}
|
||||
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
|
||||
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
|
||||
return {
|
||||
valueIsSimpleFilter: true,
|
||||
}
|
||||
@@ -178,7 +179,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
}
|
||||
|
||||
render() {
|
||||
const {errors} = this.props;
|
||||
const {errors, t} = this.props;
|
||||
const {displaySimpleFilter} = this.state;
|
||||
const fieldSpec={
|
||||
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
||||
@@ -190,16 +191,16 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
if (isNestedCombiningFilter) {
|
||||
return <div className="maputnik-filter-editor-unsupported">
|
||||
<p>
|
||||
Nested filters are not supported.
|
||||
{t("Nested filters are not supported.")}
|
||||
</p>
|
||||
<InputButton
|
||||
onClick={this.makeExpression}
|
||||
title="Convert to expression"
|
||||
title={t("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>
|
||||
Upgrade to expression
|
||||
{t("Upgrade to expression")}
|
||||
</InputButton>
|
||||
</div>
|
||||
}
|
||||
@@ -212,7 +213,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
<div>
|
||||
<InputButton
|
||||
onClick={this.makeExpression}
|
||||
title="Convert to expression"
|
||||
title={t("Convert to expression")}
|
||||
className="maputnik-make-zoom-function"
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
@@ -247,13 +248,17 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
<Block
|
||||
key="top"
|
||||
fieldSpec={fieldSpec}
|
||||
label={"Filter"}
|
||||
label={t("Filter")}
|
||||
action={actions}
|
||||
>
|
||||
<InputSelect
|
||||
value={combiningOp}
|
||||
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
|
||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||
options={[
|
||||
["all", t("every filter matches")],
|
||||
["none", t("no filter matches")],
|
||||
["any", t("any filter matches")]
|
||||
]}
|
||||
/>
|
||||
</Block>
|
||||
{editorBlocks}
|
||||
@@ -268,7 +273,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> Add filter
|
||||
</svg> {t("Add filter")}
|
||||
</InputButton>
|
||||
</div>
|
||||
<div
|
||||
@@ -299,12 +304,13 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
/>
|
||||
{this.state.valueIsSimpleFilter &&
|
||||
<div className="maputnik-expr-infobox">
|
||||
You've entered a old style filter,{' '}
|
||||
{t("You've entered an old style filter.")}
|
||||
{' '}
|
||||
<button
|
||||
onClick={this.makeFilter}
|
||||
className="maputnik-expr-infobox__button"
|
||||
>
|
||||
switch to filter editor
|
||||
{t("Switch to filter editor.")}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -313,3 +319,6 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FilterEditor = withTranslation()(FilterEditorInternal);
|
||||
export default FilterEditor;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import React from 'react'
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FilterEditorBlockProps = {
|
||||
type FilterEditorBlockInternalProps = PropsWithChildren & {
|
||||
onDelete(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
|
||||
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> {
|
||||
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="Delete filter block"
|
||||
title={t("Delete filter block")}
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
@@ -25,3 +27,5 @@ export default class FilterEditorBlock extends React.Component<FilterEditorBlock
|
||||
}
|
||||
}
|
||||
|
||||
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
|
||||
export default FilterEditorBlock;
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: FieldArrayProps, state: FieldArrayState) {
|
||||
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) {
|
||||
const value: any[] = [];
|
||||
const initialPropsValue = state.initialPropsValue.slice(0);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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'
|
||||
@@ -21,10 +22,11 @@ export type FieldDynamicArrayProps = {
|
||||
}
|
||||
'aria-label'?: string
|
||||
label: string
|
||||
};
|
||||
}
|
||||
|
||||
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation;
|
||||
|
||||
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> {
|
||||
changeValue(idx: number, newValue: string | number | undefined) {
|
||||
const values = this.values.slice(0)
|
||||
values[idx] = newValue
|
||||
@@ -62,8 +64,13 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
|
||||
}
|
||||
|
||||
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)} />
|
||||
const deleteValueBtn= <DeleteValueInputButton
|
||||
onClick={this.deleteValue.bind(this, i)}
|
||||
{...i18nProps}
|
||||
/>;
|
||||
let input;
|
||||
if(this.props.type === 'url') {
|
||||
input = <InputUrl
|
||||
@@ -117,23 +124,27 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
|
||||
className="maputnik-array-add-value"
|
||||
onClick={this.addValue}
|
||||
>
|
||||
Add value
|
||||
{t("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="Remove array item"
|
||||
title={t("Remove array item")}
|
||||
>
|
||||
<FieldDocLabel
|
||||
label={<MdDelete />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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'
|
||||
@@ -27,6 +28,7 @@ export type InputJsonProps = {
|
||||
mode?: ModeSpec<any>
|
||||
lint?: boolean | object
|
||||
};
|
||||
type InputJsonInternalProps = InputJsonProps & WithTranslation;
|
||||
|
||||
type InputJsonState = {
|
||||
isEditing: boolean
|
||||
@@ -34,7 +36,7 @@ type InputJsonState = {
|
||||
prevValue: string
|
||||
};
|
||||
|
||||
export default class InputJson extends React.Component<InputJsonProps, InputJsonState> {
|
||||
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
|
||||
static defaultProps = {
|
||||
lineNumbers: true,
|
||||
lineWrapping: false,
|
||||
@@ -52,7 +54,7 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
|
||||
_el: HTMLDivElement | null = null;
|
||||
_cancelNextChange: boolean = false;
|
||||
|
||||
constructor(props: InputJsonProps) {
|
||||
constructor(props: InputJsonInternalProps) {
|
||||
super(props);
|
||||
this._keyEvent = "keyboard";
|
||||
this.state = {
|
||||
@@ -156,6 +158,7 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const {showMessage} = this.state;
|
||||
const style = {} as {maxHeight?: number};
|
||||
if (this.props.maxHeight) {
|
||||
@@ -164,7 +167,9 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
|
||||
|
||||
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
|
||||
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
|
||||
Press <kbd>ESC</kbd> to lose focus
|
||||
<Trans t={t}>
|
||||
Press <kbd>ESC</kbd> to lose focus
|
||||
</Trans>
|
||||
</div>
|
||||
<div
|
||||
className={classnames("codemirror-container", this.props.className)}
|
||||
@@ -174,3 +179,6 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const InputJson = withTranslation()(InputJsonInternal);
|
||||
export default InputJson;
|
||||
|
||||
@@ -19,7 +19,10 @@ type InputNumberState = {
|
||||
editing: boolean
|
||||
editingRange?: boolean
|
||||
value?: number
|
||||
dirtyValue?: number
|
||||
/**
|
||||
* This is the value that is currently being edited. It can be an invalid value.
|
||||
*/
|
||||
dirtyValue?: number | string | undefined
|
||||
}
|
||||
|
||||
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
|
||||
@@ -38,7 +41,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: InputNumberProps, state: InputNumberState) {
|
||||
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
|
||||
if (!state.editing && props.value !== state.value) {
|
||||
return {
|
||||
value: props.value,
|
||||
@@ -66,7 +69,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dirtyValue: newValue === "" ? undefined : value,
|
||||
dirtyValue: newValue === "" ? undefined : newValue,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -125,7 +128,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 {
|
||||
|
||||
@@ -49,6 +49,7 @@ 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'],
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class InputString extends React.Component<InputStringProps, Input
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: InputStringProps, state: InputStringState) {
|
||||
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
|
||||
if (!state.editing) {
|
||||
return {
|
||||
value: props.value
|
||||
|
||||
+28
-17
@@ -1,9 +1,10 @@
|
||||
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) {
|
||||
function validate(url: string, t: TFunction): JSX.Element | undefined {
|
||||
if (url === "") {
|
||||
return;
|
||||
}
|
||||
@@ -22,15 +23,19 @@ function validate(url: string) {
|
||||
const isSsl = window.location.protocol === "https:";
|
||||
|
||||
if (!protocol) {
|
||||
error = (
|
||||
<SmallError>
|
||||
Must provide protocol {
|
||||
isSsl
|
||||
? <code>https://</code>
|
||||
: <><code>http://</code> or <code>https://</code></>
|
||||
}
|
||||
</SmallError>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (
|
||||
protocol &&
|
||||
@@ -39,7 +44,9 @@ function validate(url: string) {
|
||||
) {
|
||||
error = (
|
||||
<SmallError>
|
||||
CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain
|
||||
<Trans t={t}>
|
||||
CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain
|
||||
</Trans>
|
||||
</SmallError>
|
||||
);
|
||||
}
|
||||
@@ -61,32 +68,34 @@ export type FieldUrlProps = {
|
||||
className?: string
|
||||
};
|
||||
|
||||
type FieldUrlInternalProps = FieldUrlProps & WithTranslation;
|
||||
|
||||
type FieldUrlState = {
|
||||
error?: React.ReactNode
|
||||
}
|
||||
|
||||
export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlState> {
|
||||
class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlState> {
|
||||
static defaultProps = {
|
||||
onInput: () => {},
|
||||
}
|
||||
|
||||
constructor (props: FieldUrlProps) {
|
||||
constructor (props: FieldUrlInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: validate(props.value)
|
||||
error: validate(props.value, props.t),
|
||||
};
|
||||
}
|
||||
|
||||
onInput = (url: string) => {
|
||||
this.setState({
|
||||
error: validate(url)
|
||||
error: validate(url, this.props.t),
|
||||
});
|
||||
if (this.props.onInput) this.props.onInput(url);
|
||||
}
|
||||
|
||||
onChange = (url: string) => {
|
||||
this.setState({
|
||||
error: validate(url)
|
||||
error: validate(url, this.props.t),
|
||||
});
|
||||
this.props.onChange(url);
|
||||
}
|
||||
@@ -106,3 +115,5 @@ export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlSta
|
||||
}
|
||||
}
|
||||
|
||||
const FieldUrl = withTranslation()(FieldUrlInternal);
|
||||
export default FieldUrl;
|
||||
|
||||
@@ -19,31 +19,45 @@ 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"]) {
|
||||
return layout[type] ? layout[type] : layout.invalid;
|
||||
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 layoutGroups(layerType: LayerSpecification["type"]): {title: string, type: string, fields?: string[]}[] {
|
||||
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
|
||||
const layerGroup = {
|
||||
title: 'Layer',
|
||||
id: 'layer',
|
||||
title: t('Layer'),
|
||||
type: 'layer'
|
||||
}
|
||||
const filterGroup = {
|
||||
title: 'Filter',
|
||||
id: 'filter',
|
||||
title: t('Filter'),
|
||||
type: 'filter'
|
||||
}
|
||||
const editorGroup = {
|
||||
title: 'JSON Editor',
|
||||
id: 'jsoneditor',
|
||||
title: t('JSON Editor'),
|
||||
type: 'jsoneditor'
|
||||
}
|
||||
return [layerGroup, filterGroup]
|
||||
.concat(getLayoutForType(layerType).groups)
|
||||
.concat(getLayoutForType(layerType, t).groups)
|
||||
.concat([editorGroup])
|
||||
}
|
||||
|
||||
type LayerEditorProps = {
|
||||
type LayerEditorInternalProps = {
|
||||
layer: LayerSpecification
|
||||
sources: {[key: string]: SourceSpecification}
|
||||
vectorLayers: {[key: string]: any}
|
||||
@@ -58,14 +72,14 @@ type LayerEditorProps = {
|
||||
isLastLayer?: boolean
|
||||
layerIndex: number
|
||||
errors?: any[]
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type LayerEditorState = {
|
||||
editorGroups: {[keys:string]: boolean}
|
||||
};
|
||||
|
||||
/** Layer editor supporting multiple types of layers. */
|
||||
export default class LayerEditor extends React.Component<LayerEditorProps, LayerEditorState> {
|
||||
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
|
||||
static defaultProps = {
|
||||
onLayerChanged: () => {},
|
||||
onLayerIdChange: () => {},
|
||||
@@ -76,22 +90,22 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props: LayerEditorProps) {
|
||||
constructor(props: LayerEditorInternalProps) {
|
||||
super(props)
|
||||
|
||||
//TODO: Clean this up and refactor into function
|
||||
const editorGroups: {[keys:string]: boolean} = {}
|
||||
layoutGroups(this.props.layer.type).forEach(group => {
|
||||
layoutGroups(this.props.layer.type, props.t).forEach(group => {
|
||||
editorGroups[group.title] = true
|
||||
})
|
||||
|
||||
this.state = { editorGroups }
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: LayerEditorProps, state: LayerEditorState) {
|
||||
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
|
||||
const additionalGroups = { ...state.editorGroups }
|
||||
|
||||
getLayoutForType(props.layer.type).groups.forEach(group => {
|
||||
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
|
||||
if(!(group.title in additionalGroups)) {
|
||||
additionalGroups[group.title] = true
|
||||
}
|
||||
@@ -242,17 +256,19 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const groupIds: string[] = [];
|
||||
const layerType = this.props.layer.type
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
const groups = layoutGroups(layerType, t).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
}).map(group => {
|
||||
const groupId = group.title.replace(/ /g, "_");
|
||||
const groupId = group.id;
|
||||
groupIds.push(groupId);
|
||||
return <LayerEditorGroup
|
||||
data-wd-key={group.title}
|
||||
id={groupId}
|
||||
key={group.title}
|
||||
key={groupId}
|
||||
title={group.title}
|
||||
isActive={this.state.editorGroups[group.title]}
|
||||
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
|
||||
@@ -265,25 +281,25 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
|
||||
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
|
||||
delete: {
|
||||
text: "Delete",
|
||||
text: t("Delete"),
|
||||
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
|
||||
},
|
||||
duplicate: {
|
||||
text: "Duplicate",
|
||||
text: t("Duplicate"),
|
||||
handler: () => this.props.onLayerCopy(this.props.layerIndex)
|
||||
},
|
||||
hide: {
|
||||
text: (layout.visibility === "none") ? "Show" : "Hide",
|
||||
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
|
||||
},
|
||||
moveLayerUp: {
|
||||
text: "Move layer up",
|
||||
text: t("Move layer up"),
|
||||
// Not actually used...
|
||||
disabled: this.props.isFirstLayer,
|
||||
handler: () => this.moveLayer(-1)
|
||||
},
|
||||
moveLayerDown: {
|
||||
text: "Move layer down",
|
||||
text: t("Move layer down"),
|
||||
// Not actually used...
|
||||
disabled: this.props.isLastLayer,
|
||||
handler: () => this.moveLayer(+1)
|
||||
@@ -297,12 +313,12 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
|
||||
return <section className="maputnik-layer-editor"
|
||||
role="main"
|
||||
aria-label="Layer editor"
|
||||
aria-label={t("Layer editor")}
|
||||
>
|
||||
<header>
|
||||
<div className="layer-header">
|
||||
<h2 className="layer-header__title">
|
||||
Layer: {formatLayerId(this.props.layer.id)}
|
||||
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
|
||||
</h2>
|
||||
<div className="layer-header__info">
|
||||
<Wrapper
|
||||
@@ -310,7 +326,11 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
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>
|
||||
@@ -340,3 +360,6 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
const LayerEditor = withTranslation()(LayerEditorInternal);
|
||||
export default LayerEditor;
|
||||
|
||||
@@ -10,6 +10,7 @@ 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[]
|
||||
@@ -22,6 +23,7 @@ type LayerListContainerProps = {
|
||||
sources: object
|
||||
errors: any[]
|
||||
};
|
||||
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
|
||||
|
||||
type LayerListContainerState = {
|
||||
collapsedGroups: {[ket: string]: boolean}
|
||||
@@ -31,14 +33,14 @@ type LayerListContainerState = {
|
||||
};
|
||||
|
||||
// List of collapsible layer editors
|
||||
class LayerListContainer extends React.Component<LayerListContainerProps, LayerListContainerState> {
|
||||
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
selectedItemRef: React.RefObject<any>;
|
||||
scrollContainerRef: React.RefObject<HTMLElement>;
|
||||
|
||||
constructor(props: LayerListContainerProps) {
|
||||
constructor(props: LayerListContainerInternalProps) {
|
||||
super(props);
|
||||
this.selectedItemRef = React.createRef();
|
||||
this.scrollContainerRef = React.createRef();
|
||||
@@ -259,10 +261,12 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
|
||||
})
|
||||
})
|
||||
|
||||
const t = this.props.t;
|
||||
|
||||
return <section
|
||||
className="maputnik-layer-list"
|
||||
role="complementary"
|
||||
aria-label="Layers list"
|
||||
aria-label={t("Layers list")}
|
||||
ref={this.scrollContainerRef}
|
||||
>
|
||||
<ModalAdd
|
||||
@@ -274,7 +278,7 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
|
||||
onLayersChange={this.props.onLayersChange}
|
||||
/>
|
||||
<header className="maputnik-layer-list-header">
|
||||
<span className="maputnik-layer-list-header-title">Layers</span>
|
||||
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
|
||||
<span className="maputnik-space" />
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
@@ -283,7 +287,11 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
|
||||
data-wd-key="skip-target-layer-list"
|
||||
onClick={this.toggleLayers}
|
||||
className="maputnik-button">
|
||||
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
|
||||
{this.state.areAllGroupsExpanded === true ?
|
||||
t("Collapse")
|
||||
:
|
||||
t("Expand")
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,14 +301,14 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
|
||||
onClick={this.toggleModal.bind(this, 'add')}
|
||||
data-wd-key="layer-list:add-layer"
|
||||
className="maputnik-button maputnik-button-selected">
|
||||
Add Layer
|
||||
{t("Add Layer")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
role="navigation"
|
||||
aria-label="Layers list"
|
||||
aria-label={t("Layers list")}
|
||||
>
|
||||
<ul className="maputnik-layer-list-container">
|
||||
{listItems}
|
||||
@@ -310,6 +318,13 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
|
||||
}
|
||||
}
|
||||
|
||||
// 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} />)
|
||||
|
||||
|
||||
@@ -139,6 +139,6 @@ class LayerListItem extends React.Component<LayerListItemProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const LayerListItemSortable = SortableElement((props: LayerListItemProps) => <LayerListItem {...props} />);
|
||||
const LayerListItemSortable = SortableElement<LayerListItemProps>((props: LayerListItemProps) => <LayerListItem {...props} />);
|
||||
|
||||
export default LayerListItemSortable;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React, {type JSX} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
|
||||
// @ts-ignore
|
||||
import MapboxInspect from 'mapbox-gl-inspect'
|
||||
// @ts-ignore
|
||||
import colors from 'mapbox-gl-inspect/lib/colors'
|
||||
import MaplibreInspect from '@maplibre/maplibre-gl-inspect'
|
||||
import colors from '@maplibre/maplibre-gl-inspect/lib/colors'
|
||||
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'
|
||||
import MapMaplibreGlFeaturePropertyPopup, { InspectFeature } from './MapMaplibreGlFeaturePropertyPopup'
|
||||
import Color from 'color'
|
||||
@@ -13,13 +11,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';
|
||||
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
|
||||
import { withTranslation, WithTranslation } from 'react-i18next'
|
||||
|
||||
|
||||
const IS_SUPPORTED = MapLibreGl.supported();
|
||||
|
||||
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container) {
|
||||
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
|
||||
ReactDOM.render(popup, mountNode);
|
||||
return mountNode;
|
||||
return mountNode as HTMLElement;
|
||||
}
|
||||
|
||||
function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) {
|
||||
@@ -37,6 +35,7 @@ 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') {
|
||||
@@ -52,7 +51,7 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
|
||||
return inspectStyle
|
||||
}
|
||||
|
||||
type MapMaplibreGlProps = {
|
||||
type MapMaplibreGlInternalProps = {
|
||||
onDataChange?(event: {map: Map | null}): unknown
|
||||
onLayerSelect(...args: unknown[]): unknown
|
||||
mapStyle: StyleSpecification
|
||||
@@ -65,15 +64,15 @@ type MapMaplibreGlProps = {
|
||||
}
|
||||
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
|
||||
onChange(value: {center: LngLat, zoom: number}): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type MapMaplibreGlState = {
|
||||
map: Map | null
|
||||
inspect: MapboxInspect | null
|
||||
inspect: MaplibreInspect | null
|
||||
zoom?: number
|
||||
};
|
||||
|
||||
export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, MapMaplibreGlState> {
|
||||
class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> {
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
@@ -83,7 +82,7 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
|
||||
}
|
||||
container: HTMLDivElement | null = null
|
||||
|
||||
constructor(props: MapMaplibreGlProps) {
|
||||
constructor(props: MapMaplibreGlInternalProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
map: null,
|
||||
@@ -91,20 +90,8 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
|
||||
}
|
||||
}
|
||||
|
||||
updateMapFromProps(props: MapMaplibreGlProps) {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
if(!this.state.map) return
|
||||
|
||||
//Maplibre GL now does diffing natively so we don't need to calculate
|
||||
//the necessary operations ourselves!
|
||||
this.state.map.setStyle(
|
||||
this.props.replaceAccessTokens(props.mapStyle),
|
||||
{diff: true}
|
||||
)
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: MapMaplibreGlProps, nextState: MapMaplibreGlState) {
|
||||
shouldComponentUpdate(nextProps: MapMaplibreGlInternalProps, nextState: MapMaplibreGlState) {
|
||||
let should = false;
|
||||
try {
|
||||
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
|
||||
@@ -115,46 +102,42 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
const map = this.state.map;
|
||||
|
||||
this.updateMapFromProps(this.props);
|
||||
|
||||
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
|
||||
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
|
||||
// eslint-disable-next-line
|
||||
this.state.inspect._popupBlocked = false;
|
||||
this.state.inspect.toggleInspector()
|
||||
}
|
||||
const styleWithTokens = this.props.replaceAccessTokens(this.props.mapStyle);
|
||||
if (map) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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});
|
||||
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() {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
const mapOpts = {
|
||||
...this.props.options,
|
||||
container: this.container!,
|
||||
style: this.props.mapStyle,
|
||||
hash: true,
|
||||
maxZoom: 24
|
||||
}
|
||||
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;
|
||||
|
||||
const map = new MapLibreGl.Map(mapOpts);
|
||||
|
||||
@@ -169,7 +152,9 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
|
||||
map.showCollisionBoxes = mapOpts.showCollisionBoxes!;
|
||||
map.showOverdrawInspector = mapOpts.showOverdrawInspector!;
|
||||
|
||||
const zoomControl = new ZoomControl;
|
||||
this.initGeocoder(map);
|
||||
|
||||
const zoomControl = new ZoomControl(this.props.t("Zoom:"));
|
||||
map.addControl(zoomControl, 'top-right');
|
||||
|
||||
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
|
||||
@@ -177,7 +162,7 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
|
||||
|
||||
const tmpNode = document.createElement('div');
|
||||
|
||||
const inspect = new MapboxInspect({
|
||||
const inspect = new MaplibreInspect({
|
||||
popup: new MapLibreGl.Popup({
|
||||
closeOnClick: false
|
||||
}),
|
||||
@@ -234,25 +219,61 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
|
||||
this.props.onLayerSelect(index);
|
||||
}
|
||||
|
||||
initGeocoder(map: Map) {
|
||||
const geocoderConfig = {
|
||||
forwardGeocode: async (config: MaplibreGeocoderApiConfig) => {
|
||||
const features = [];
|
||||
try {
|
||||
const request = `https://nominatim.openstreetmap.org/search?q=${config.query}&format=geojson&polygon_geojson=1&addressdetails=1`;
|
||||
const response = await fetch(request);
|
||||
const geojson = await response.json();
|
||||
for (const feature of geojson.features) {
|
||||
const center = [
|
||||
feature.bbox[0] +
|
||||
(feature.bbox[2] - feature.bbox[0]) / 2,
|
||||
feature.bbox[1] +
|
||||
(feature.bbox[3] - feature.bbox[1]) / 2
|
||||
];
|
||||
const point = {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: center
|
||||
},
|
||||
place_name: feature.properties.display_name,
|
||||
properties: feature.properties,
|
||||
text: feature.properties.display_name,
|
||||
place_type: ['place'],
|
||||
center
|
||||
};
|
||||
features.push(point);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to forwardGeocode with error: ${e}`);
|
||||
}
|
||||
return {
|
||||
features
|
||||
};
|
||||
},
|
||||
} as unknown as MaplibreGeocoderApi;
|
||||
const geocoder = new MaplibreGeocoder(geocoderConfig, {
|
||||
placeholder: this.props.t("Search"),
|
||||
maplibregl: MapLibreGl,
|
||||
});
|
||||
map.addControl(geocoder, 'top-left');
|
||||
}
|
||||
|
||||
render() {
|
||||
if(IS_SUPPORTED) {
|
||||
return <div
|
||||
className="maputnik-map__map"
|
||||
role="region"
|
||||
aria-label="Map view"
|
||||
ref={x => this.container = x}
|
||||
data-wd-key="maplibre:map"
|
||||
></div>
|
||||
}
|
||||
else {
|
||||
return <div
|
||||
className="maputnik-map maputnik-map--error"
|
||||
>
|
||||
<div className="maputnik-map__error-message">
|
||||
Error: Cannot load MaplibreGL, WebGL is either unsupported or disabled
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
const t = this.props.t;
|
||||
return <div
|
||||
className="maputnik-map__map"
|
||||
role="region"
|
||||
aria-label={t("Map view")}
|
||||
ref={x => this.container = x}
|
||||
data-wd-key="maplibre:map"
|
||||
></div>
|
||||
}
|
||||
}
|
||||
|
||||
const MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
|
||||
export default MapMaplibreGl;
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import FieldString from './FieldString'
|
||||
import type { GeoJSONFeatureWithSourceLayer } from '@maplibre/maplibre-gl-inspect'
|
||||
|
||||
export type InspectFeature = {
|
||||
id: string
|
||||
properties: {[key:string]: any}
|
||||
layer: {[key:string]: any}
|
||||
geometry: GeoJSON.Geometry
|
||||
sourceLayer: string
|
||||
export type InspectFeature = GeoJSONFeatureWithSourceLayer & {
|
||||
inspectModeCounter?: number
|
||||
counter?: number
|
||||
}
|
||||
|
||||
function displayValue(value: string | number | Date | object) {
|
||||
function displayValue(value: string | number | Date | object | undefined) {
|
||||
if (typeof value === 'undefined' || value === null) return value;
|
||||
if (value instanceof Date) return value.toLocaleString();
|
||||
if (typeof value === 'object' ||
|
||||
@@ -21,30 +15,25 @@ function displayValue(value: string | number | Date | object) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function renderProperties(feature: InspectFeature) {
|
||||
return Object.keys(feature.properties).map(propertyName => {
|
||||
const property = feature.properties[propertyName]
|
||||
return <Block key={propertyName} label={propertyName}>
|
||||
<FieldString value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
|
||||
</Block>
|
||||
})
|
||||
}
|
||||
|
||||
function renderFeatureId(feature: InspectFeature) {
|
||||
return <Block key={"feature-id"} label={"feature_id"}>
|
||||
<FieldString value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} />
|
||||
</Block>
|
||||
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 <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>
|
||||
<Block key={"property-type"} label={"$type"}>
|
||||
<FieldString value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
||||
</Block>
|
||||
{renderFeatureId(feature)}
|
||||
{renderProperties(feature)}
|
||||
</div>
|
||||
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))}
|
||||
{Object.keys(feature.properties).map(propertyName => {
|
||||
const property = feature.properties[propertyName];
|
||||
return renderKeyValueTableRow(propertyName, displayValue(property))
|
||||
})}
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
function removeDuplicatedFeatures(features: InspectFeature[]) {
|
||||
@@ -78,7 +67,11 @@ class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
|
||||
render() {
|
||||
const features = removeDuplicatedFeatures(this.props.features)
|
||||
return <div className="maputnik-feature-property-popup">
|
||||
{features.map(renderFeature)}
|
||||
<table className="maputnik-popup-table">
|
||||
<tbody>
|
||||
{features.map(renderFeature)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,16 @@ 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[feature.layer['source-layer']].find((f: InspectFeature) => f.layer.id === feature.layer.id)
|
||||
const featureObject = sources[sourceKey].find((f: InspectFeature) => f.layer.id === feature.layer.id)
|
||||
|
||||
featureObject!.counter = returnedFeatures[feature.layer.id]
|
||||
} else {
|
||||
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
||||
sources[feature.layer['source-layer']].push(feature)
|
||||
sources[sourceKey] = sources[sourceKey] || []
|
||||
sources[sourceKey].push(feature)
|
||||
|
||||
returnedFeatures[feature.layer.id] = 1
|
||||
}
|
||||
@@ -40,29 +41,21 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
|
||||
|
||||
try {
|
||||
const paintProps = feature.layer.paint;
|
||||
let propName;
|
||||
|
||||
if(Object.prototype.hasOwnProperty.call(paintProps, "text-color") && paintProps["text-color"]) {
|
||||
propName = "text-color";
|
||||
if("text-color" in paintProps && paintProps["text-color"]) {
|
||||
return String(paintProps["text-color"]);
|
||||
}
|
||||
else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-color") && paintProps["fill-color"]) {
|
||||
propName = "fill-color";
|
||||
if ("fill-color" in paintProps && paintProps["fill-color"]) {
|
||||
return String(paintProps["fill-color"]);
|
||||
}
|
||||
else if (Object.prototype.hasOwnProperty.call(paintProps, "line-color") && paintProps["line-color"]) {
|
||||
propName = "line-color";
|
||||
if ("line-color" in paintProps && paintProps["line-color"]) {
|
||||
return String(paintProps["line-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";
|
||||
if ("fill-extrusion-color" in paintProps && paintProps["fill-extrusion-color"]) {
|
||||
return String(paintProps["fill-extrusion-color"]);
|
||||
}
|
||||
// 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.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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';
|
||||
|
||||
@@ -22,7 +24,7 @@ function renderCoords (coords: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
type MapOpenLayersProps = {
|
||||
type MapOpenLayersInternalProps = {
|
||||
onDataChange?(...args: unknown[]): unknown
|
||||
mapStyle: object
|
||||
accessToken?: string
|
||||
@@ -31,7 +33,7 @@ type MapOpenLayersProps = {
|
||||
debugToolbox: boolean
|
||||
replaceAccessTokens(...args: unknown[]): unknown
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type MapOpenLayersState = {
|
||||
zoom: string
|
||||
@@ -41,7 +43,7 @@ type MapOpenLayersState = {
|
||||
selectedFeatures?: any[]
|
||||
};
|
||||
|
||||
export default class MapOpenLayers extends React.Component<MapOpenLayersProps, MapOpenLayersState> {
|
||||
class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps, MapOpenLayersState> {
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
@@ -53,7 +55,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
overlay: Overlay | undefined;
|
||||
popupContainer: HTMLElement | null = null;
|
||||
|
||||
constructor(props: MapOpenLayersProps) {
|
||||
constructor(props: MapOpenLayersInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
zoom: "0",
|
||||
@@ -72,7 +74,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
apply(this.map, newMapStyle);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: MapOpenLayersProps) {
|
||||
componentDidUpdate(prevProps: MapOpenLayersInternalProps) {
|
||||
if (this.props.mapStyle !== prevProps.mapStyle) {
|
||||
this.updateStyle(
|
||||
this.props.replaceAccessTokens(this.props.mapStyle)
|
||||
@@ -150,6 +152,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div className="maputnik-ol-container">
|
||||
<div
|
||||
ref={x => this.popupContainer = x}
|
||||
@@ -159,7 +162,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
<button
|
||||
className="maplibregl-popup-close-button"
|
||||
onClick={this.closeOverlay}
|
||||
aria-label="Close popup"
|
||||
aria-label={t("Close popup")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -169,20 +172,20 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-ol-zoom">
|
||||
Zoom: {this.state.zoom}
|
||||
{t("Zoom:")} {this.state.zoom}
|
||||
</div>
|
||||
{this.props.debugToolbox &&
|
||||
<div className="maputnik-ol-debug">
|
||||
<div>
|
||||
<label>cursor: </label>
|
||||
<label>{t("cursor:")} </label>
|
||||
<span>{renderCoords(this.state.cursor)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>center: </label>
|
||||
<label>{t("center:")} </label>
|
||||
<span>{renderCoords(this.state.center)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>rotation: </label>
|
||||
<label>{t("rotation:")} </label>
|
||||
<span>{this.state.rotation}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -191,7 +194,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
className="maputnik-ol"
|
||||
ref={x => this.container = x}
|
||||
role="region"
|
||||
aria-label="Map view"
|
||||
aria-label={t("Map view")}
|
||||
style={{
|
||||
...this.props.style,
|
||||
}}>
|
||||
@@ -200,3 +203,5 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
}
|
||||
}
|
||||
|
||||
const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
|
||||
export default MapOpenLayers;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import React, { PropsWithChildren } 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 ModalProps = {
|
||||
type ModalInternalProps = PropsWithChildren & {
|
||||
"data-wd-key"?: string
|
||||
isOpen: boolean
|
||||
title: string
|
||||
@@ -12,15 +12,15 @@ type ModalProps = {
|
||||
underlayClickExits?: boolean
|
||||
underlayProps?: any
|
||||
className?: string
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class Modal extends React.Component<ModalProps> {
|
||||
class ModalInternal extends React.Component<ModalInternalProps> {
|
||||
static defaultProps = {
|
||||
underlayClickExits: true
|
||||
}
|
||||
|
||||
// See <https://github.com/maputnik/editor/issues/416>
|
||||
// See <https://github.com/maplibre/maputnik/issues/416>
|
||||
onClose = () => {
|
||||
if (document.activeElement) {
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
@@ -32,6 +32,7 @@ export default class Modal extends React.Component<ModalProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
if(this.props.isOpen) {
|
||||
return <AriaModal
|
||||
titleText={this.props.title}
|
||||
@@ -49,7 +50,7 @@ export default class Modal extends React.Component<ModalProps> {
|
||||
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
||||
<span className="maputnik-modal-header-space"></span>
|
||||
<button className="maputnik-modal-header-toggle"
|
||||
title="Close modal"
|
||||
title={t("Close modal")}
|
||||
onClick={this.onClose}
|
||||
data-wd-key={this.props["data-wd-key"]+".close-modal"}
|
||||
>
|
||||
@@ -68,3 +69,5 @@ export default class Modal extends React.Component<ModalProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const Modal = withTranslation()(ModalInternal);
|
||||
export default Modal;
|
||||
|
||||
@@ -7,15 +7,16 @@ 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 ModalAddProps = {
|
||||
type ModalAddInternalProps = {
|
||||
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"]
|
||||
@@ -24,7 +25,7 @@ type ModalAddState = {
|
||||
'source-layer'?: string
|
||||
};
|
||||
|
||||
export default class ModalAdd extends React.Component<ModalAddProps, ModalAddState> {
|
||||
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
|
||||
addLayer = () => {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const layer: ModalAddState = {
|
||||
@@ -45,7 +46,7 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
|
||||
this.props.onOpenToggle(false)
|
||||
}
|
||||
|
||||
constructor(props: ModalAddProps) {
|
||||
constructor(props: ModalAddInternalProps) {
|
||||
super(props)
|
||||
const state: ModalAddState = {
|
||||
type: 'fill',
|
||||
@@ -54,12 +55,12 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
|
||||
|
||||
if(props.sources.length > 0) {
|
||||
state.source = Object.keys(this.props.sources)[0];
|
||||
state['source-layer'] = this.props.sources[state.source as keyof ModalAddProps["sources"]][0]
|
||||
state['source-layer'] = this.props.sources[state.source as keyof ModalAddInternalProps["sources"]][0]
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: ModalAddProps, prevState: ModalAddState) {
|
||||
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
|
||||
// Check if source is valid for new type
|
||||
const oldType = prevState.type;
|
||||
const newType = this.state.type;
|
||||
@@ -125,13 +126,14 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
|
||||
|
||||
|
||||
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={'Add Layer'}
|
||||
title={t('Add Layer')}
|
||||
data-wd-key="modal:add-layer"
|
||||
className="maputnik-add-modal"
|
||||
>
|
||||
@@ -169,10 +171,12 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
|
||||
onClick={this.addLayer}
|
||||
data-wd-key="add-layer"
|
||||
>
|
||||
Add Layer
|
||||
{t("Add Layer")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalAdd = withTranslation()(ModalAddInternal);
|
||||
export default ModalAdd;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
type ModalDebugProps = {
|
||||
type ModalDebugInternalProps = {
|
||||
isOpen: boolean
|
||||
renderer: string
|
||||
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
|
||||
@@ -18,12 +19,12 @@ type ModalDebugProps = {
|
||||
lat: number
|
||||
}
|
||||
}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class ModalDebug extends React.Component<ModalDebugProps> {
|
||||
class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
|
||||
render() {
|
||||
const {mapView} = this.props;
|
||||
const {t, mapView} = this.props;
|
||||
|
||||
const osmZoom = Math.round(mapView.zoom)+1;
|
||||
const osmLon = +(mapView.center.lng).toFixed(5);
|
||||
@@ -33,10 +34,10 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
|
||||
data-wd-key="modal:debug"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Debug'}
|
||||
title={t('Debug')}
|
||||
>
|
||||
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||
<h1>Options</h1>
|
||||
<h1>{t("Options")}</h1>
|
||||
{this.props.renderer === 'mlgljs' &&
|
||||
<ul>
|
||||
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
|
||||
@@ -63,16 +64,20 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Links</h1>
|
||||
<p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
|
||||
>
|
||||
Open in OSM
|
||||
</a> — Opens the current view on openstreetmap.org
|
||||
<Trans t={t}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
|
||||
>
|
||||
Open in OSM
|
||||
</a> — Opens the current view on openstreetmap.org
|
||||
</Trans>
|
||||
</p>
|
||||
</section>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalDebug = withTranslation()(ModalDebugInternal);
|
||||
export default ModalDebug;
|
||||
|
||||
@@ -5,6 +5,7 @@ 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'
|
||||
@@ -16,15 +17,15 @@ import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||
const MAPLIBRE_GL_VERSION = version;
|
||||
|
||||
|
||||
type ModalExportProps = {
|
||||
type ModalExportInternalProps = {
|
||||
mapStyle: StyleSpecification & { id: string }
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||
|
||||
tokenizedStyle () {
|
||||
return format(
|
||||
@@ -48,7 +49,7 @@ export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
|
||||
downloadHtml() {
|
||||
const tokenStyle = this.tokenizedStyle();
|
||||
const htmlTitle = this.props.mapStyle.name || "Map";
|
||||
const htmlTitle = this.props.mapStyle.name || this.props.t("Map");
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -100,30 +101,32 @@ export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
|
||||
|
||||
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={'Export Style'}
|
||||
title={t('Export Style')}
|
||||
className="maputnik-export-modal"
|
||||
>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Download Style</h1>
|
||||
<h1>{t("Download Style")}</h1>
|
||||
<p>
|
||||
Download a JSON style to your computer.
|
||||
{t("Download a JSON style to your computer.")}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<FieldString
|
||||
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||
label={fsa.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fsa.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={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||
label={fsa.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fsa.maputnik.thunderforest_access_token}
|
||||
value={(this.props.mapStyle.metadata || {} as any)['maputnik:thunderforest_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||
/>
|
||||
@@ -134,14 +137,14 @@ export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
onClick={this.downloadStyle.bind(this)}
|
||||
>
|
||||
<MdFileDownload />
|
||||
Download Style
|
||||
{t("Download Style")}
|
||||
</InputButton>
|
||||
|
||||
<InputButton
|
||||
onClick={this.downloadHtml.bind(this)}
|
||||
>
|
||||
<MdFileDownload />
|
||||
Download HTML
|
||||
{t("Download HTML")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</section>
|
||||
@@ -150,3 +153,5 @@ export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const ModalExport = withTranslation()(ModalExportInternal);
|
||||
export default ModalExport;
|
||||
|
||||
@@ -2,23 +2,25 @@ import React from 'react'
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import Modal from './Modal'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
type ModalLoadingProps = {
|
||||
type ModalLoadingInternalProps = {
|
||||
isOpen: boolean
|
||||
onCancel(...args: unknown[]): unknown
|
||||
title: string
|
||||
message: React.ReactNode
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class ModalLoading extends React.Component<ModalLoadingProps> {
|
||||
class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
|
||||
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}
|
||||
@@ -35,10 +37,12 @@ export default class ModalLoading extends React.Component<ModalLoadingProps> {
|
||||
</p>
|
||||
<p className="maputnik-dialog__buttons">
|
||||
<InputButton onClick={(e) => this.props.onCancel(e)}>
|
||||
Cancel
|
||||
{t("Cancel")}
|
||||
</InputButton>
|
||||
</p>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalLoading = withTranslation()(ModalLoadingInternal);
|
||||
export default ModalLoading;
|
||||
|
||||
@@ -2,6 +2,7 @@ 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'
|
||||
@@ -42,11 +43,11 @@ class PublicStyle extends React.Component<PublicStyleProps> {
|
||||
}
|
||||
}
|
||||
|
||||
type ModalOpenProps = {
|
||||
type ModalOpenInternalProps = {
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onStyleOpen(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type ModalOpenState = {
|
||||
styleUrl: string
|
||||
@@ -55,8 +56,8 @@ type ModalOpenState = {
|
||||
activeRequestUrl?: string | null
|
||||
};
|
||||
|
||||
export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpenState> {
|
||||
constructor(props: ModalOpenProps) {
|
||||
class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> {
|
||||
constructor(props: ModalOpenInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
styleUrl: ""
|
||||
@@ -174,6 +175,7 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const styleOptions = publicStyles.map(style => {
|
||||
return <PublicStyle
|
||||
key={style.id}
|
||||
@@ -200,29 +202,31 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
|
||||
data-wd-key="modal:open"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={() => this.onOpenToggle()}
|
||||
title={'Open Style'}
|
||||
title={t('Open Style')}
|
||||
>
|
||||
{errorElement}
|
||||
<section className="maputnik-modal-section">
|
||||
<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>
|
||||
<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>
|
||||
</FileReaderInput>
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<form onSubmit={this.onSubmitUrl}>
|
||||
<h1>Load from URL</h1>
|
||||
<h1>{t("Load from URL")}</h1>
|
||||
<p>
|
||||
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 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>
|
||||
</p>
|
||||
<InputUrl
|
||||
aria-label="Style URL"
|
||||
aria-label={t("Style URL")}
|
||||
data-wd-key="modal:open.url.input"
|
||||
type="text"
|
||||
className="maputnik-input"
|
||||
default="Enter URL..."
|
||||
default={t("Enter URL...")}
|
||||
value={this.state.styleUrl}
|
||||
onInput={this.onChangeUrl}
|
||||
onChange={this.onChangeUrl}
|
||||
@@ -239,9 +243,9 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section maputnik-modal-section--shrink">
|
||||
<h1>Gallery Styles</h1>
|
||||
<h1>{t("Gallery Styles")}</h1>
|
||||
<p>
|
||||
Open one of the publicly available styles to start from.
|
||||
{t("Open one of the publicly available styles to start from.")}
|
||||
</p>
|
||||
<div className="maputnik-style-gallery-container">
|
||||
{styleOptions}
|
||||
@@ -251,12 +255,14 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
|
||||
|
||||
<ModalLoading
|
||||
isOpen={!!this.state.activeRequest}
|
||||
title={'Loading style'}
|
||||
title={t('Loading style')}
|
||||
onCancel={(e: Event) => this.onCancelActiveRequest(e)}
|
||||
message={"Loading: "+this.state.activeRequestUrl}
|
||||
message={t("Loading: {{requestUrl}}", { requestUrl: this.state.activeRequestUrl })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ModalOpen = withTranslation()(ModalOpenInternal);
|
||||
export default ModalOpen;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import type {LightSpecification, StyleSpecification, TransitionSpecification} from 'maplibre-gl'
|
||||
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'
|
||||
@@ -12,15 +13,15 @@ import FieldColor from './FieldColor'
|
||||
import Modal from './Modal'
|
||||
import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||
|
||||
type ModalSettingsProps = {
|
||||
type ModalSettingsInternalProps = {
|
||||
mapStyle: StyleSpecification
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
onChangeMetadataProperty(...args: unknown[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
|
||||
changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
|
||||
const transition = {
|
||||
...this.props.mapStyle.transition,
|
||||
@@ -58,6 +59,25 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
});
|
||||
}
|
||||
|
||||
changeTerrainProperty(property: keyof TerrainSpecification, value: any) {
|
||||
const terrain = {
|
||||
...this.props.mapStyle.terrain,
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
delete terrain[property];
|
||||
}
|
||||
else {
|
||||
// @ts-ignore
|
||||
terrain[property] = value;
|
||||
}
|
||||
|
||||
this.props.onStyleChanged({
|
||||
...this.props.mapStyle,
|
||||
terrain,
|
||||
});
|
||||
}
|
||||
|
||||
changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
@@ -76,87 +96,86 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
|
||||
render() {
|
||||
const metadata = this.props.mapStyle.metadata || {} as any;
|
||||
const {onChangeMetadataProperty, mapStyle} = this.props;
|
||||
const inputProps = { }
|
||||
const {t, onChangeMetadataProperty, mapStyle} = this.props;
|
||||
const fsa = fieldSpecAdditional(t);
|
||||
|
||||
const light = this.props.mapStyle.light || {};
|
||||
const transition = this.props.mapStyle.transition || {};
|
||||
const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification;
|
||||
|
||||
return <Modal
|
||||
data-wd-key="modal:settings"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Style Settings'}
|
||||
title={t('Style Settings')}
|
||||
>
|
||||
<div className="modal:settings">
|
||||
<FieldString {...inputProps}
|
||||
label={"Name"}
|
||||
<FieldString
|
||||
label={t("Name")}
|
||||
fieldSpec={latest.$root.name}
|
||||
data-wd-key="modal:settings.name"
|
||||
value={this.props.mapStyle.name}
|
||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||
/>
|
||||
<FieldString {...inputProps}
|
||||
label={"Owner"}
|
||||
fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}
|
||||
<FieldString
|
||||
label={t("Owner")}
|
||||
fieldSpec={{doc: t("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 {...inputProps}
|
||||
<FieldUrl
|
||||
fieldSpec={latest.$root.sprite}
|
||||
label="Sprite URL"
|
||||
label={t("Sprite URL")}
|
||||
data-wd-key="modal:settings.sprite"
|
||||
value={this.props.mapStyle.sprite as string}
|
||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||
/>
|
||||
|
||||
<FieldUrl {...inputProps}
|
||||
label="Glyphs URL"
|
||||
<FieldUrl
|
||||
label={t("Glyphs URL")}
|
||||
fieldSpec={latest.$root.glyphs}
|
||||
data-wd-key="modal:settings.glyphs"
|
||||
value={this.props.mapStyle.glyphs as string}
|
||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||
/>
|
||||
|
||||
<FieldString {...inputProps}
|
||||
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||
<FieldString
|
||||
label={fsa.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fsa.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 {...inputProps}
|
||||
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||
<FieldString
|
||||
label={fsa.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fsa.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={"Center"}
|
||||
label={t("Center")}
|
||||
fieldSpec={latest.$root.center}
|
||||
length={2}
|
||||
type="number"
|
||||
value={mapStyle.center || []}
|
||||
default={latest.$root.center.default || [0, 0]}
|
||||
default={[0, 0]}
|
||||
onChange={this.changeStyleProperty.bind(this, "center")}
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
{...inputProps}
|
||||
label={"Zoom"}
|
||||
label={t("Zoom")}
|
||||
fieldSpec={latest.$root.zoom}
|
||||
value={mapStyle.zoom}
|
||||
default={latest.$root.zoom.default || 0}
|
||||
default={0}
|
||||
onChange={this.changeStyleProperty.bind(this, "zoom")}
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
{...inputProps}
|
||||
label={"Bearing"}
|
||||
label={t("Bearing")}
|
||||
fieldSpec={latest.$root.bearing}
|
||||
value={mapStyle.bearing}
|
||||
default={latest.$root.bearing.default}
|
||||
@@ -164,8 +183,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
{...inputProps}
|
||||
label={"Pitch"}
|
||||
label={t("Pitch")}
|
||||
fieldSpec={latest.$root.pitch}
|
||||
value={mapStyle.pitch}
|
||||
default={latest.$root.pitch.default}
|
||||
@@ -173,8 +191,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldEnum
|
||||
{...inputProps}
|
||||
label={"Light anchor"}
|
||||
label={t("Light anchor")}
|
||||
fieldSpec={latest.light.anchor}
|
||||
name="light-anchor"
|
||||
value={light.anchor as string}
|
||||
@@ -184,8 +201,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldColor
|
||||
{...inputProps}
|
||||
label={"Light color"}
|
||||
label={t("Light color")}
|
||||
fieldSpec={latest.light.color}
|
||||
value={light.color as string}
|
||||
default={latest.light.color.default}
|
||||
@@ -193,8 +209,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
{...inputProps}
|
||||
label={"Light intensity"}
|
||||
label={t("Light intensity")}
|
||||
fieldSpec={latest.light.intensity}
|
||||
value={light.intensity as number}
|
||||
default={latest.light.intensity.default}
|
||||
@@ -202,8 +217,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldArray
|
||||
{...inputProps}
|
||||
label={"Light position"}
|
||||
label={t("Light position")}
|
||||
fieldSpec={latest.light.position}
|
||||
type="number"
|
||||
length={latest.light.position.length}
|
||||
@@ -212,9 +226,24 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
onChange={this.changeLightProperty.bind(this, "position")}
|
||||
/>
|
||||
|
||||
<FieldString
|
||||
label={t("Terrain source")}
|
||||
fieldSpec={latest.terrain.source}
|
||||
data-wd-key="modal:settings.maputnik:terrain_source"
|
||||
value={terrain.source}
|
||||
onChange={this.changeTerrainProperty.bind(this, "source")}
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
{...inputProps}
|
||||
label={"Transition delay"}
|
||||
label={t("Terrain exaggeration")}
|
||||
fieldSpec={latest.terrain.exaggeration}
|
||||
value={terrain.exaggeration}
|
||||
default={latest.terrain.exaggeration.default}
|
||||
onChange={this.changeTerrainProperty.bind(this, "exaggeration")}
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={t("Transition delay")}
|
||||
fieldSpec={latest.transition.delay}
|
||||
value={transition.delay}
|
||||
default={latest.transition.delay.default}
|
||||
@@ -222,21 +251,20 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
{...inputProps}
|
||||
label={"Transition duration"}
|
||||
label={t("Transition duration")}
|
||||
fieldSpec={latest.transition.duration}
|
||||
value={transition.duration}
|
||||
default={latest.transition.duration.default}
|
||||
onChange={this.changeTransitionProperty.bind(this, "duration")}
|
||||
/>
|
||||
|
||||
<FieldSelect {...inputProps}
|
||||
label={fieldSpecAdditional.maputnik.style_renderer.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
|
||||
<FieldSelect
|
||||
label={fsa.maputnik.style_renderer.label}
|
||||
fieldSpec={fsa.maputnik.style_renderer}
|
||||
data-wd-key="modal:settings.maputnik:renderer"
|
||||
options={[
|
||||
['mlgljs', 'MapLibreGL JS'],
|
||||
['ol', 'Open Layers (experimental)'],
|
||||
['ol', t('Open Layers (experimental)')],
|
||||
]}
|
||||
value={metadata['maputnik:renderer'] || 'mlgljs'}
|
||||
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')}
|
||||
@@ -246,3 +274,5 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const ModalSettings = withTranslation()(ModalSettingsInternal)
|
||||
export default ModalSettings;
|
||||
|
||||
@@ -1,48 +1,50 @@
|
||||
import React from 'react'
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
type ModalShortcutsProps = {
|
||||
type ModalShortcutsInternalProps = {
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class ModalShortcuts extends React.Component<ModalShortcutsProps> {
|
||||
class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const help = [
|
||||
{
|
||||
key: <kbd>?</kbd>,
|
||||
text: "Shortcuts menu"
|
||||
text: t("Shortcuts menu")
|
||||
},
|
||||
{
|
||||
key: <kbd>o</kbd>,
|
||||
text: "Open modal"
|
||||
text: t("Open modal")
|
||||
},
|
||||
{
|
||||
key: <kbd>e</kbd>,
|
||||
text: "Export modal"
|
||||
text: t("Export modal")
|
||||
},
|
||||
{
|
||||
key: <kbd>d</kbd>,
|
||||
text: "Data Sources modal"
|
||||
text: t("Data Sources modal")
|
||||
},
|
||||
{
|
||||
key: <kbd>s</kbd>,
|
||||
text: "Style Settings modal"
|
||||
text: t("Style Settings modal")
|
||||
},
|
||||
{
|
||||
key: <kbd>i</kbd>,
|
||||
text: "Toggle inspect"
|
||||
text: t("Toggle inspect")
|
||||
},
|
||||
{
|
||||
key: <kbd>m</kbd>,
|
||||
text: "Focus map"
|
||||
text: t("Focus map")
|
||||
},
|
||||
{
|
||||
key: <kbd>!</kbd>,
|
||||
text: "Debug modal"
|
||||
text: t("Debug modal")
|
||||
},
|
||||
]
|
||||
|
||||
@@ -50,51 +52,51 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
|
||||
const mapShortcuts = [
|
||||
{
|
||||
key: <kbd>+</kbd>,
|
||||
text: "Increase the zoom level by 1.",
|
||||
text: t("Increase the zoom level by 1.",)
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>+</kbd></>,
|
||||
text: "Increase the zoom level by 2.",
|
||||
text: t("Increase the zoom level by 2.",)
|
||||
},
|
||||
{
|
||||
key: <kbd>-</kbd>,
|
||||
text: "Decrease the zoom level by 1.",
|
||||
text: t("Decrease the zoom level by 1.",)
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>-</kbd></>,
|
||||
text: "Decrease the zoom level by 2.",
|
||||
text: t("Decrease the zoom level by 2.",)
|
||||
},
|
||||
{
|
||||
key: <kbd>Up</kbd>,
|
||||
text: "Pan up by 100 pixels.",
|
||||
text: t("Pan up by 100 pixels.",)
|
||||
},
|
||||
{
|
||||
key: <kbd>Down</kbd>,
|
||||
text: "Pan down by 100 pixels.",
|
||||
text: t("Pan down by 100 pixels.",)
|
||||
},
|
||||
{
|
||||
key: <kbd>Left</kbd>,
|
||||
text: "Pan left by 100 pixels.",
|
||||
text: t("Pan left by 100 pixels.",)
|
||||
},
|
||||
{
|
||||
key: <kbd>Right</kbd>,
|
||||
text: "Pan right by 100 pixels.",
|
||||
text: t("Pan right by 100 pixels.",)
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>,
|
||||
text: "Increase the rotation by 15 degrees.",
|
||||
text: t("Increase the rotation by 15 degrees.",)
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>,
|
||||
text: "Decrease the rotation by 15 degrees."
|
||||
text: t("Decrease the rotation by 15 degrees.")
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>,
|
||||
text: "Increase the pitch by 10 degrees."
|
||||
text: t("Increase the pitch by 10 degrees.")
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>,
|
||||
text: "Decrease the pitch by 10 degrees."
|
||||
text: t("Decrease the pitch by 10 degrees.")
|
||||
},
|
||||
]
|
||||
|
||||
@@ -103,11 +105,13 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
|
||||
data-wd-key="modal:shortcuts"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Shortcuts'}
|
||||
title={t('Shortcuts')}
|
||||
>
|
||||
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||
<p>
|
||||
Press <code>ESC</code> to lose focus of any active elements, then press one of:
|
||||
<Trans t={t}>
|
||||
Press <code>ESC</code> to lose focus of any active elements, then press one of:
|
||||
</Trans>
|
||||
</p>
|
||||
<dl>
|
||||
{help.map((item, idx) => {
|
||||
@@ -117,7 +121,7 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
|
||||
</div>
|
||||
})}
|
||||
</dl>
|
||||
<p>If the Map is in focused you can use the following shortcuts</p>
|
||||
<p>{t("If the Map is in focused you can use the following shortcuts")}</p>
|
||||
<ul>
|
||||
{mapShortcuts.map((item, idx) => {
|
||||
return <li key={idx}>
|
||||
@@ -130,3 +134,5 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
|
||||
export default ModalShortcuts;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react'
|
||||
import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
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'
|
||||
@@ -74,16 +75,17 @@ 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={`Remove '${this.props.sourceId}' source`}
|
||||
aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})}
|
||||
className="maputnik-active-source-type-editor-header-delete"
|
||||
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
@@ -104,7 +106,7 @@ class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTyp
|
||||
|
||||
type AddSourceProps = {
|
||||
onAdd(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type AddSourceState = {
|
||||
mode: EditorMode
|
||||
@@ -134,7 +136,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
case 'geojson_json': return {
|
||||
type: 'geojson',
|
||||
cluster: (source as GeoJSONSourceSpecification).cluster || false,
|
||||
data: {}
|
||||
data: ''
|
||||
}
|
||||
case 'tilejson_vector': return {
|
||||
type: 'vector',
|
||||
@@ -202,6 +204,7 @@ 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
|
||||
//
|
||||
@@ -215,25 +218,25 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
|
||||
return <div className="maputnik-add-source">
|
||||
<FieldString
|
||||
label={"Source ID"}
|
||||
fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}
|
||||
label={t("Source ID")}
|
||||
fieldSpec={{doc: t("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={"Source Type"}
|
||||
label={t("Source Type")}
|
||||
fieldSpec={sourceTypeFieldSpec}
|
||||
options={[
|
||||
['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'],
|
||||
['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')],
|
||||
]}
|
||||
onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
|
||||
value={this.state.mode as string}
|
||||
@@ -247,20 +250,20 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
className="maputnik-add-source-button"
|
||||
onClick={this.onAdd}
|
||||
>
|
||||
Add Source
|
||||
{t("Add Source")}
|
||||
</InputButton>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
type ModalSourcesProps = {
|
||||
type ModalSourcesInternalProps = {
|
||||
mapStyle: StyleSpecification
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
||||
stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
|
||||
const strippedSource = {...source}
|
||||
delete strippedSource['title']
|
||||
@@ -268,7 +271,8 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const mapStyle = this.props.mapStyle
|
||||
const {t, mapStyle} = this.props;
|
||||
const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady};
|
||||
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
||||
const source = mapStyle.sources[sourceId]
|
||||
return <ActiveModalSourcesTypeEditor
|
||||
@@ -277,6 +281,7 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
source={source}
|
||||
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
||||
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
||||
{...i18nProps}
|
||||
/>
|
||||
})
|
||||
|
||||
@@ -295,17 +300,17 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
data-wd-key="modal:sources"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Sources'}
|
||||
title={t('Sources')}
|
||||
>
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Active Sources</h1>
|
||||
<h1>{t("Active Sources")}</h1>
|
||||
{activeSources}
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Choose Public Source</h1>
|
||||
<h1>{t("Choose Public Source")}</h1>
|
||||
<p>
|
||||
Add one of the publicly available sources to your style.
|
||||
{t("Add one of the publicly available sources to your style.")}
|
||||
</p>
|
||||
<div className="maputnik-public-sources" style={{maxWidth: 500}}>
|
||||
{tilesetOptions}
|
||||
@@ -313,13 +318,16 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<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>
|
||||
<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>
|
||||
<AddSource
|
||||
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
|
||||
{...i18nProps}
|
||||
/>
|
||||
</section>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalSources = withTranslation()(ModalSourcesInternal);
|
||||
export default ModalSources;
|
||||
|
||||
@@ -8,6 +8,8 @@ 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;
|
||||
|
||||
@@ -17,14 +19,15 @@ 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={"TileJSON URL"}
|
||||
label={t("TileJSON URL")}
|
||||
fieldSpec={latest.source_vector.url}
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
@@ -45,7 +48,7 @@ type TileURLSourceEditorProps = {
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
children?: React.ReactNode
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
changeTileUrls(tiles: string[]) {
|
||||
@@ -58,7 +61,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
renderTileUrls() {
|
||||
const tiles = this.props.source.tiles || [];
|
||||
return <FieldDynamicArray
|
||||
label={"Tile URL"}
|
||||
label={this.props.t("Tile URL")}
|
||||
fieldSpec={latest.source_vector.tiles}
|
||||
type="url"
|
||||
value={tiles}
|
||||
@@ -67,10 +70,11 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div>
|
||||
{this.renderTileUrls()}
|
||||
<FieldNumber
|
||||
label={"Min Zoom"}
|
||||
label={t("Min Zoom")}
|
||||
fieldSpec={latest.source_vector.minzoom}
|
||||
value={this.props.source.minzoom || 0}
|
||||
onChange={minzoom => this.props.onChange({
|
||||
@@ -79,7 +83,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
})}
|
||||
/>
|
||||
<FieldNumber
|
||||
label={"Max Zoom"}
|
||||
label={t("Max Zoom")}
|
||||
fieldSpec={latest.source_vector.maxzoom}
|
||||
value={this.props.source.maxzoom || 22}
|
||||
onChange={maxzoom => this.props.onChange({
|
||||
@@ -93,16 +97,24 @@ 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;
|
||||
@@ -115,7 +127,7 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
||||
|
||||
return <div>
|
||||
<FieldUrl
|
||||
label={"Image URL"}
|
||||
label={t("Image URL")}
|
||||
fieldSpec={latest.source_image.url}
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
@@ -123,11 +135,11 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
||||
url,
|
||||
})}
|
||||
/>
|
||||
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
||||
{createCornerLabels(t).map(({label, key}, idx) => {
|
||||
return (
|
||||
<FieldArray
|
||||
label={`Coord ${label}`}
|
||||
key={label}
|
||||
label={label}
|
||||
key={key}
|
||||
length={2}
|
||||
type="number"
|
||||
value={this.props.source.coordinates[idx]}
|
||||
@@ -146,10 +158,11 @@ 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;
|
||||
@@ -169,18 +182,18 @@ class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
|
||||
|
||||
return <div>
|
||||
<FieldDynamicArray
|
||||
label={"Video URL"}
|
||||
label={t("Video URL")}
|
||||
fieldSpec={latest.source_video.urls}
|
||||
type="string"
|
||||
value={this.props.source.urls}
|
||||
default={[]}
|
||||
onChange={changeUrls}
|
||||
/>
|
||||
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
||||
{createCornerLabels(t).map(({label, key}, idx) => {
|
||||
return (
|
||||
<FieldArray
|
||||
label={`Coord ${label}`}
|
||||
key={label}
|
||||
label={label}
|
||||
key={key}
|
||||
length={2}
|
||||
type="number"
|
||||
value={this.props.source.coordinates[idx]}
|
||||
@@ -198,12 +211,13 @@ type GeoJSONSourceUrlEditorProps = {
|
||||
data: string
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class GeoJSONSourceUrlEditor extends React.Component<GeoJSONSourceUrlEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <FieldUrl
|
||||
label={"GeoJSON URL"}
|
||||
label={t("GeoJSON URL")}
|
||||
fieldSpec={latest.source_geojson.data}
|
||||
value={this.props.source.data}
|
||||
onChange={data => this.props.onChange({
|
||||
@@ -220,12 +234,13 @@ 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={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
|
||||
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}>
|
||||
<FieldJson
|
||||
layer={this.props.source.data}
|
||||
maxHeight={200}
|
||||
@@ -243,7 +258,7 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
|
||||
/>
|
||||
</Block>
|
||||
<FieldCheckbox
|
||||
label={'Cluster'}
|
||||
label={t('Cluster')}
|
||||
value={this.props.source.cluster}
|
||||
onChange={cluster => {
|
||||
this.props.onChange({
|
||||
@@ -256,18 +271,22 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
|
||||
}
|
||||
}
|
||||
|
||||
type ModalSourcesTypeEditorProps = {
|
||||
type ModalSourcesTypeEditorInternalProps = {
|
||||
mode: EditorMode
|
||||
source: any
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class ModalSourcesTypeEditor extends React.Component<ModalSourcesTypeEditorProps> {
|
||||
class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEditorInternalProps> {
|
||||
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} />
|
||||
@@ -278,7 +297,7 @@ export default class ModalSourcesTypeEditor extends React.Component<ModalSources
|
||||
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
|
||||
<FieldSelect
|
||||
label={"Encoding"}
|
||||
label={t("Encoding")}
|
||||
fieldSpec={latest.source_raster_dem.encoding}
|
||||
options={Object.keys(latest.source_raster_dem.encoding.values)}
|
||||
onChange={encoding => this.props.onChange({
|
||||
@@ -295,3 +314,5 @@ export default class ModalSourcesTypeEditor extends React.Component<ModalSources
|
||||
}
|
||||
}
|
||||
|
||||
const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
|
||||
export default ModalSourcesTypeEditor;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
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 don’t track you, so we don’t know how you use Maputnik. Help us make Maputnik better for you by completing a 7–minute 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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import './SmallError.scss';
|
||||
|
||||
|
||||
type SmallErrorProps = {
|
||||
type SmallErrorInternalProps = {
|
||||
children?: React.ReactNode
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class SmallError extends React.Component<SmallErrorProps> {
|
||||
class SmallErrorInternal extends React.Component<SmallErrorInternalProps> {
|
||||
render () {
|
||||
const t = this.props.t;
|
||||
return (
|
||||
<div className="SmallError">
|
||||
Error: {this.props.children}
|
||||
{t("Error:")} {this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SmallError = withTranslation()(SmallErrorInternal);
|
||||
export default SmallError;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import InputSpec from './InputSpec'
|
||||
@@ -11,13 +11,14 @@ 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: DataPropertyProps, state: DataPropertyState) {
|
||||
function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState) {
|
||||
// This is initialsed below only if required to improved performance.
|
||||
let newRefs: {[key: number]: string} | undefined;
|
||||
|
||||
@@ -35,7 +36,7 @@ function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
|
||||
return newRefs;
|
||||
}
|
||||
|
||||
type DataPropertyProps = {
|
||||
type DataPropertyInternalProps = {
|
||||
onChange?(fieldName: string, value: any): unknown
|
||||
onDeleteStop?(...args: unknown[]): unknown
|
||||
onAddStop?(...args: unknown[]): unknown
|
||||
@@ -46,7 +47,7 @@ type DataPropertyProps = {
|
||||
fieldSpec?: object
|
||||
value?: DataPropertyValue
|
||||
errors?: object
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type DataPropertyState = {
|
||||
refs: {[key: number]: string}
|
||||
@@ -65,7 +66,7 @@ export type Stop = [{
|
||||
value: number
|
||||
}, number]
|
||||
|
||||
export default class DataProperty extends React.Component<DataPropertyProps, DataPropertyState> {
|
||||
class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> {
|
||||
state = {
|
||||
refs: {} as {[key: number]: string}
|
||||
}
|
||||
@@ -80,7 +81,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: DataPropertyProps, state: DataPropertyState) {
|
||||
static getDerivedStateFromProps(props: Readonly<DataPropertyInternalProps>, state: DataPropertyState) {
|
||||
const newRefs = setStopRefs(props, state);
|
||||
if(newRefs) {
|
||||
return {
|
||||
@@ -213,6 +214,8 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
if (typeof this.props.value?.type === "undefined") {
|
||||
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec)
|
||||
}
|
||||
@@ -227,8 +230,8 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
|
||||
|
||||
const dataProps = {
|
||||
'aria-label': "Input value",
|
||||
label: "Data value",
|
||||
'aria-label': t("Input value"),
|
||||
label: t("Data value"),
|
||||
value: dataLevel as any,
|
||||
onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
|
||||
}
|
||||
@@ -263,7 +266,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
</td>
|
||||
<td>
|
||||
<InputSpec
|
||||
aria-label="Output value"
|
||||
aria-label={t("Output value")}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
@@ -282,27 +285,27 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
|
||||
<div className="maputnik-data-fieldset-inner">
|
||||
<Block
|
||||
label={"Function"}
|
||||
label={t("Function")}
|
||||
key="function"
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSelect
|
||||
value={this.props.value!.type}
|
||||
onChange={(propVal: string) => this.changeDataType(propVal)}
|
||||
title={"Select a type of data scale (default is 'categorical')."}
|
||||
title={t("Select a type of data scale (default is 'categorical').")}
|
||||
options={this.getDataFunctionTypes(this.props.fieldSpec)}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
{this.props.value?.type !== "identity" &&
|
||||
<Block
|
||||
label={"Base"}
|
||||
label={t("Base")}
|
||||
key="base"
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSpec
|
||||
fieldName={"base"}
|
||||
fieldSpec={latest.function.base}
|
||||
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
|
||||
value={this.props.value?.base}
|
||||
onChange={(_, newValue) => this.changeBase(newValue as number)}
|
||||
/>
|
||||
@@ -316,14 +319,14 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputString
|
||||
value={this.props.value?.property}
|
||||
title={"Input a data property to base styles off of."}
|
||||
title={t("Input a data property to base styles off of.")}
|
||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
{dataFields &&
|
||||
<Block
|
||||
label={"Default"}
|
||||
label={t("Default")}
|
||||
key="default"
|
||||
>
|
||||
<InputSpec
|
||||
@@ -337,12 +340,12 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
{dataFields &&
|
||||
<div className="maputnik-function-stop">
|
||||
<table className="maputnik-function-stop-table">
|
||||
<caption>Stops</caption>
|
||||
<caption>{t("Stops")}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zoom</th>
|
||||
<th>Input value</th>
|
||||
<th rowSpan={2}>Output value</th>
|
||||
<th>{t("Zoom")}</th>
|
||||
<th>{t("Input value")}</th>
|
||||
<th rowSpan={2}>{t("Output value")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -359,7 +362,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> Add stop
|
||||
</svg> {t("Add stop")}
|
||||
</InputButton>
|
||||
}
|
||||
<InputButton
|
||||
@@ -368,7 +371,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg> Convert to expression
|
||||
</svg> {t("Convert to expression")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,3 +379,6 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const DataProperty = withTranslation()(DataPropertyInternal);
|
||||
export default DataProperty;
|
||||
|
||||
@@ -2,21 +2,26 @@ import React from 'react'
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
type DeleteStopButtonProps = {
|
||||
type DeleteStopButtonInternalProps = {
|
||||
onClick?(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class DeleteStopButton extends React.Component<DeleteStopButtonProps> {
|
||||
class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <InputButton
|
||||
className="maputnik-delete-stop"
|
||||
onClick={this.props.onClick}
|
||||
title={"Remove zoom level from stop"}
|
||||
title={t("Remove zoom level from stop")}
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
}
|
||||
}
|
||||
|
||||
const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
|
||||
export default DeleteStopButton;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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'
|
||||
@@ -8,7 +9,7 @@ import labelFromFieldName from '../libs/label-from-field-name'
|
||||
import FieldJson from './FieldJson'
|
||||
|
||||
|
||||
type ExpressionPropertyProps = {
|
||||
type ExpressionPropertyInternalProps = {
|
||||
onDelete?(...args: unknown[]): unknown
|
||||
fieldName: string
|
||||
fieldType?: string
|
||||
@@ -20,20 +21,20 @@ type ExpressionPropertyProps = {
|
||||
canUndo?(...args: unknown[]): unknown
|
||||
onFocus?(...args: unknown[]): unknown
|
||||
onBlur?(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type ExpressionPropertyState = {
|
||||
jsonError: boolean
|
||||
};
|
||||
|
||||
export default class ExpressionProperty extends React.Component<ExpressionPropertyProps, ExpressionPropertyState> {
|
||||
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps, ExpressionPropertyState> {
|
||||
static defaultProps = {
|
||||
errors: {},
|
||||
onFocus: () => {},
|
||||
onBlur: () => {},
|
||||
}
|
||||
|
||||
constructor (props:ExpressionPropertyProps) {
|
||||
constructor(props: ExpressionPropertyInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
jsonError: false,
|
||||
@@ -53,7 +54,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
|
||||
}
|
||||
|
||||
render() {
|
||||
const {errors, fieldName, fieldType, value, canUndo} = this.props;
|
||||
const {t, errors, fieldName, fieldType, value, canUndo} = this.props;
|
||||
const {jsonError} = this.state;
|
||||
const undoDisabled = canUndo ? !canUndo() : true;
|
||||
|
||||
@@ -65,7 +66,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
|
||||
onClick={this.props.onUndo}
|
||||
disabled={undoDisabled}
|
||||
className="maputnik-delete-stop"
|
||||
title="Revert from expression"
|
||||
title={t("Revert from expression")}
|
||||
>
|
||||
<MdUndo />
|
||||
</InputButton>
|
||||
@@ -74,7 +75,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
|
||||
key="delete_action"
|
||||
onClick={this.props.onDelete}
|
||||
className="maputnik-delete-stop"
|
||||
title="Delete expression"
|
||||
title={t("Delete expression")}
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
@@ -112,7 +113,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
|
||||
// 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={labelFromFieldName(this.props.fieldName)}
|
||||
label={t(labelFromFieldName(this.props.fieldName))}
|
||||
action={deleteStopBtn}
|
||||
wideMode={true}
|
||||
>
|
||||
@@ -137,3 +138,6 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
|
||||
export default ExpressionProperty;
|
||||
|
||||
@@ -3,16 +3,18 @@ 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 FunctionInputButtonsProps = {
|
||||
type FunctionInputButtonsInternalProps = {
|
||||
fieldSpec?: any
|
||||
onZoomClick?(...args: unknown[]): unknown
|
||||
onDataClick?(...args: unknown[]): unknown
|
||||
onExpressionClick?(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FunctionInputButtons extends React.Component<FunctionInputButtonsProps> {
|
||||
class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
|
||||
|
||||
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
||||
@@ -20,7 +22,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
|
||||
<InputButton
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onExpressionClick}
|
||||
title="Convert to expression"
|
||||
title={t("Convert to expression")}
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
@@ -31,7 +33,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
|
||||
makeZoomInputButton = <InputButton
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onZoomClick}
|
||||
title="Convert property into a zoom function"
|
||||
title={t("Convert property into a zoom function")}
|
||||
>
|
||||
<MdFunctions />
|
||||
</InputButton>
|
||||
@@ -40,7 +42,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
|
||||
makeDataInputButton = <InputButton
|
||||
className="maputnik-make-data-function"
|
||||
onClick={this.props.onDataClick}
|
||||
title="Convert property to data function"
|
||||
title={t("Convert property to data function")}
|
||||
>
|
||||
<MdInsertChart />
|
||||
</InputButton>
|
||||
@@ -56,3 +58,6 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
|
||||
export default FunctionInputButtons;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
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'
|
||||
@@ -20,7 +21,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: ZoomPropertyProps, state: ZoomPropertyState) {
|
||||
function setStopRefs(props: ZoomPropertyInternalProps, state: ZoomPropertyState) {
|
||||
// This is initialsed below only if required to improved performance.
|
||||
let newRefs: {[key: number]: string} = {};
|
||||
|
||||
@@ -45,7 +46,7 @@ type ZoomWithStops = {
|
||||
}
|
||||
|
||||
|
||||
type ZoomPropertyProps = {
|
||||
type ZoomPropertyInternalProps = {
|
||||
onChange?(...args: unknown[]): unknown
|
||||
onChangeToDataFunction?(...args: unknown[]): unknown
|
||||
onDeleteStop?(...args: unknown[]): unknown
|
||||
@@ -59,13 +60,13 @@ type ZoomPropertyProps = {
|
||||
}
|
||||
errors?: object
|
||||
value?: ZoomWithStops
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type ZoomPropertyState = {
|
||||
refs: {[key: number]: string}
|
||||
}
|
||||
|
||||
export default class ZoomProperty extends React.Component<ZoomPropertyProps, ZoomPropertyState> {
|
||||
class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> {
|
||||
static defaultProps = {
|
||||
errors: {},
|
||||
}
|
||||
@@ -84,7 +85,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: ZoomPropertyProps, state: ZoomPropertyState) {
|
||||
static getDerivedStateFromProps(props: Readonly<ZoomPropertyInternalProps>, state: ZoomPropertyState) {
|
||||
const newRefs = setStopRefs(props, state);
|
||||
if(newRefs) {
|
||||
return {
|
||||
@@ -152,17 +153,17 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
}
|
||||
|
||||
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={key}
|
||||
key={`${stop[0]}-${stop[1]}`}
|
||||
>
|
||||
<td>
|
||||
<InputNumber
|
||||
aria-label="Zoom"
|
||||
aria-label={t("Zoom")}
|
||||
value={zoomLevel}
|
||||
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
||||
min={0}
|
||||
@@ -171,7 +172,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
</td>
|
||||
<td>
|
||||
<InputSpec
|
||||
aria-label="Output value"
|
||||
aria-label={t("Output value")}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec as any}
|
||||
value={value}
|
||||
@@ -190,24 +191,24 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
|
||||
<div className="maputnik-data-fieldset-inner">
|
||||
<Block
|
||||
label={"Function"}
|
||||
label={t("Function")}
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSelect
|
||||
value={"interpolate"}
|
||||
onChange={(propVal: string) => this.changeDataType(propVal)}
|
||||
title={"Select a type of data scale (default is 'categorical')."}
|
||||
title={t("Select a type of data scale (default is 'categorical').")}
|
||||
options={this.getDataFunctionTypes(this.props.fieldSpec!)}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
<Block
|
||||
label={"Base"}
|
||||
label={t("Base")}
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSpec
|
||||
fieldName={"base"}
|
||||
fieldSpec={latest.function.base}
|
||||
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
|
||||
value={this.props.value?.base}
|
||||
onChange={(_, newValue) => this.changeBase(newValue as number | undefined)}
|
||||
/>
|
||||
@@ -215,11 +216,11 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
</Block>
|
||||
<div className="maputnik-function-stop">
|
||||
<table className="maputnik-function-stop-table maputnik-function-stop-table--zoom">
|
||||
<caption>Stops</caption>
|
||||
<caption>{t("Stops")}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zoom</th>
|
||||
<th rowSpan={2}>Output value</th>
|
||||
<th>{t("Zoom")}</th>
|
||||
<th rowSpan={2}>{t("Output value")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -234,7 +235,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> Add stop
|
||||
</svg> {t("Add stop")}
|
||||
</InputButton>
|
||||
<InputButton
|
||||
className="maputnik-add-stop"
|
||||
@@ -242,7 +243,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg> Convert to expression
|
||||
</svg> {t("Convert to expression")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,3 +263,6 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ZoomProperty = withTranslation()(ZoomPropertyInternal);
|
||||
export default ZoomProperty;
|
||||
|
||||
+50
-38
@@ -1,52 +1,28 @@
|
||||
[
|
||||
{
|
||||
"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.9/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
|
||||
"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.8/style.json",
|
||||
"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": "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-basic-gl-style",
|
||||
"title": "Maptiler Basic",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.10/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
|
||||
},
|
||||
{
|
||||
"id": "maptiler-toner-gl-style",
|
||||
"title": "Toner",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@339e5b7/style.json",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@v1.0/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-outdoor",
|
||||
"title": "Zoomstack Outdoor",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-outdoor/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-outdoor.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-road",
|
||||
"title": "Zoomstack Road",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-road/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-light",
|
||||
"title": "Zoomstack Light",
|
||||
@@ -60,9 +36,45 @@
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
|
||||
},
|
||||
{
|
||||
"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="
|
||||
"id": "os-zoomstack-outdoor",
|
||||
"title": "Zoomstack Outdoor",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-outdoor/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-outdoor.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-road",
|
||||
"title": "Zoomstack Road",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-road/style.json",
|
||||
"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": "osm-liberty",
|
||||
"title": "OSM Liberty",
|
||||
"url": "https://maputnik.github.io/osm-liberty/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.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¢er=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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"openmaptiles": {
|
||||
"type": "vector",
|
||||
"url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}",
|
||||
"url": "https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key={key}",
|
||||
"title": "OpenMapTiles v3"
|
||||
},
|
||||
"thunderforest_transport": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"openmaptiles": "KDhMfHvorAFkFe64wlZb",
|
||||
"openmaptiles": "get_your_own_OpIi9ZULNHzrESv6T2vL",
|
||||
"thunderforest": "b71f7f0ba4064f5eb9e903859a9cf5c6"
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
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;
|
||||
+5
-4
@@ -1,15 +1,16 @@
|
||||
import { IconContext } from "react-icons";
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import './favicon.ico'
|
||||
import './styles/index.scss'
|
||||
import './i18n';
|
||||
import App from './components/App';
|
||||
|
||||
ReactDOM.render(
|
||||
const root = createRoot(document.querySelector("#app"));
|
||||
root.render(
|
||||
<IconContext.Provider value={{className: 'react-icons'}}>
|
||||
<App/>
|
||||
</IconContext.Provider>,
|
||||
document.querySelector("#app")
|
||||
</IconContext.Provider>
|
||||
);
|
||||
|
||||
// Hide the loader.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import jsonlint from 'jsonlint';
|
||||
import CodeMirror, { MarkerRange } from 'codemirror';
|
||||
import jsonToAst from 'json-to-ast';
|
||||
import {expression, validate} from '@maplibre/maplibre-gl-style-spec';
|
||||
import {expression, validateStyleMin} from '@maplibre/maplibre-gl-style-spec';
|
||||
|
||||
type MarkerRangeWithMessage = MarkerRange & {message: string};
|
||||
|
||||
@@ -102,7 +102,7 @@ CodeMirror.registerHelper("lint", "mgl", (text: string, opts: any, doc: any) =>
|
||||
let out: ReturnType<typeof expression.createExpression> | null = null;
|
||||
if (context === "layer") {
|
||||
// Just an empty style so we can validate a layer.
|
||||
const errors = validate({
|
||||
const errors = validateStyleMin({
|
||||
"version": 8,
|
||||
"name": "Empty Style",
|
||||
"metadata": {},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user