Compare commits

..

38 Commits

Author SHA1 Message Date
shelly_goldblit 13336bf2af fix failing build 2023-12-31 20:49:09 +02:00
shelly_goldblit e74382d260 fix build 2023-12-31 20:48:13 +02:00
shelly_goldblit fa2dfa7480 remove left overs 2023-12-31 20:47:10 +02:00
shelly_goldblit 2213e74e2a I think all tests should be green now 2023-12-31 20:41:17 +02:00
ShellyDCMS a94ee550f8 update test utils version 2023-12-31 16:57:42 +00:00
shelly_goldblit 0ed6bb518f fix more tests 2023-12-31 18:53:46 +02:00
shelly_goldblit c5a7005a3d lint 2023-12-31 17:53:20 +02:00
shelly_goldblit 055d35f4b8 fix more tests 2023-12-31 17:47:27 +02:00
shelly_goldblit d4d37e5930 lint i guess 2023-12-31 17:39:07 +02:00
shelly_goldblit b20c54d417 move then into driver 2023-12-31 17:34:48 +02:00
ShellyDCMS f7f6df70b2 update test utils version 2023-12-31 14:39:57 +00:00
shelly_goldblit 20c121d17a lint 2023-12-31 16:26:34 +02:00
shelly_goldblit 27190f683b restore lock file 2023-12-31 16:18:02 +02:00
shelly_goldblit b4e31cc031 fix test 2023-12-31 16:17:05 +02:00
shelly_goldblit 26bf99d91a fix build 2023-12-31 13:45:34 +02:00
shelly_goldblit f5ab97d3a7 fix failing tests 2023-12-31 13:43:39 +02:00
shelly_goldblit 6aa48d66a2 fix some more tests 2023-12-31 12:05:50 +02:00
shelly_goldblit ff547fc3f7 fix some more tests 2023-12-31 11:55:03 +02:00
shelly_goldblit 138f685293 fix more tests 2023-12-31 11:42:47 +02:00
shelly_goldblit aa8cb8f3d3 fix some more tests 2023-12-31 11:40:40 +02:00
shelly_goldblit c694237703 fix some more tests 2023-12-31 11:31:31 +02:00
shelly_goldblit ba201935a8 fix failing test 2023-12-31 11:08:06 +02:00
HarelM 79b36efbdb Fix lint 2023-12-31 09:25:30 +02:00
HarelM fb41a59bd8 Update package lock after cache npm clean 2023-12-31 09:21:47 +02:00
HarelM 7538e02e47 Try fixing build again... remove cache 2023-12-31 09:21:26 +02:00
HarelM d031fc67f5 Fix build 2023-12-31 09:12:43 +02:00
HarelM 7de266ca43 Attemopt to fix package-lock 2023-12-31 09:09:54 +02:00
ShellyDCMS ecfdd5f1ba update lock file 2023-12-31 04:18:50 +00:00
shelly_goldblit 0bb0ae87af wip 2023-12-31 06:11:35 +02:00
shelly_goldblit 7c8b25e82c restore lock file 2023-12-31 06:11:04 +02:00
shelly_goldblit 666bc648d8 wip 2023-12-30 21:17:35 +02:00
shelly_goldblit f7a945abce wip 2023-12-30 20:36:49 +02:00
shelly_goldblit 085eda76a1 convert more tests 2023-12-30 18:48:17 +02:00
ShellyDCMS 0dc3f4bf5d fix lock file 2023-12-30 16:41:53 +00:00
shelly_goldblit efc507aecd fix failing test 2023-12-30 18:36:27 +02:00
shelly_goldblit ef902910c8 update keyboard tests 2023-12-30 18:32:19 +02:00
shelly_goldblit 6ed08c7416 modify some of the layer tests 2023-12-30 18:26:50 +02:00
shelly_goldblit 9014e12ca6 improve drivers 2023-12-30 11:49:03 +02:00
158 changed files with 13520 additions and 5894 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"packages": [],
"sandboxes": ["/"]
}
+1 -2
View File
@@ -1,2 +1 @@
github: [maplibre] custom: "https://maputnik.github.io/donate"
open_collective: maplibre
-11
View File
@@ -1,11 +0,0 @@
## Launch Checklist
<!-- Thanks for the PR! Feel free to add or remove items from the checklist. -->
- [ ] Briefly describe the changes in this PR.
- [ ] Link to related issues.
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
- [ ] Write tests for all new functionality.
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
+27 -15
View File
@@ -15,7 +15,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: docker build -t test-docker-image-build . - run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
# build the editor # build the editor
build-node: build-node:
@@ -53,40 +53,52 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build
- name: artifacts/maputnik - run: npm run build-storybook
uses: actions/upload-artifact@v4 - name: artifacts/editor
uses: actions/upload-artifact@v1
with: with:
name: maputnik name: editor
path: dist path: dist
- name: artifacts/storybook
uses: actions/upload-artifact@v1
with:
name: storybook
path: build/storybook
# Build and upload desktop CLI artifacts # Build and upload desktop CLI artifacts
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v3
with: with:
go-version: ^1.23.x go-version: ^1.19.x
cache-dependency-path: desktop/go.sum
id: go id: go
- name: Build desktop artifacts - name: Check out code into the Go module directory
run: npm run build-desktop uses: actions/checkout@v4
with:
repository: maputnik/desktop
ref: master
path: ./src/github.com/maputnik/desktop/
- name: Make
run: cd src/github.com/maputnik/desktop/ && make
- name: Artifacts/linux - name: Artifacts/linux
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v1
with: with:
name: maputnik-linux name: maputnik-linux
path: ./desktop/bin/linux/ path: ./src/github.com/maputnik/desktop/bin/linux/
- name: Artifacts/darwin - name: Artifacts/darwin
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v1
with: with:
name: maputnik-darwin name: maputnik-darwin
path: ./desktop/bin/darwin/ path: ./src/github.com/maputnik/desktop/bin/darwin/
- name: Artifacts/windows - name: Artifacts/windows
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v1
with: with:
name: maputnik-windows name: maputnik-windows
path: ./desktop/bin/windows/ path: ./src/github.com/maputnik/desktop/bin/windows/
e2e-tests: e2e-tests:
name: "E2E tests using ${{ matrix.browser }}" name: "E2E tests using ${{ matrix.browser }}"
@@ -1,39 +0,0 @@
name: Create bump version PR
on:
workflow_dispatch:
inputs:
version:
description: Version to change to.
required: true
type: string
jobs:
bump-version-pr:
name: Bump version PR
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Bump version
run: |
npm version --commit-hooks false --git-tag-version false ${{ inputs.version }}
./build/bump-version-changelog.js ${{ inputs.version }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
commit-message: Bump version to ${{ inputs.version }}
branch: bump-version-to-${{ inputs.version }}
title: Bump version to ${{ inputs.version }}
+8 -33
View File
@@ -3,49 +3,24 @@ name: deploy
on: on:
push: push:
branches: [ main ] branches: [ main ]
tags:
- 'v*'
jobs: 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 # publish docker to GitHub registry
deploy-docker: deploy-docker:
name: deploy/docker name: deploy/docker
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' }} if: ${{ github.event_name == 'push' }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix:
os: [ubuntu-latest]
steps: 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 - uses: actions/checkout@v4
- run: docker build -t ghcr.io/maplibre/maputnik:main . - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
- run: docker push ghcr.io/maplibre/maputnik:main - run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
- run: docker push docker.pkg.github.com/maputnik/editor/editor:main
-104
View File
@@ -1,104 +0,0 @@
name: Release
on:
push:
branches: [main]
workflow_dispatch:
jobs:
release-check:
name: Check if version changed
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Check if version has been updated
id: check
uses: EndBug/version-check@v2
outputs:
publish: ${{ steps.check.outputs.changed }}
release:
name: Release
needs: release-check
if: ${{ needs.release-check.outputs.publish == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build
uses: actions/setup-go@v5
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.3.1
- name: Install
run: npm ci
- name: Build
run: |
npm run build
npm run build-desktop
- name: Tag commit and push
id: tag_version
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ steps.package-version.outputs.current-version }}
- name: Create Archives
run: |
zip -r dist dist
zip -r desktop desktop/bin/
- name: Build Release Notes
id: release_notes
run: |
RELEASE_NOTES_PATH="${PWD}/release_notes.txt"
./build/release-notes.js > ${RELEASE_NOTES_PATH}
echo "release_notes=${RELEASE_NOTES_PATH}" >> $GITHUB_OUTPUT
- name: Create GitHub Release
id: create_regular_release
uses: ncipollo/release-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: ${{ steps.tag_version.outputs.new_tag }}
bodyFile: ${{ steps.release_notes.outputs.release_notes }}
artifacts: "dist.zip,desktop.zip"
allowUpdates: true
draft: false
prerelease: false
+1 -1
View File
@@ -33,6 +33,6 @@ node_modules
public public
/errorShots /errorShots
/old /old
/build
/cypress/screenshots /cypress/screenshots
/dist/ /dist/
/desktop/version.go
-1
View File
@@ -1 +0,0 @@
legacy-peer-deps = true
+9
View File
@@ -0,0 +1,9 @@
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;
+7
View File
@@ -0,0 +1,7 @@
import { addons } from '@storybook/addons';
import { themes } from '@storybook/theming';
import theme from './maputnik.theme';
addons.setConfig({
theme: theme,
});
+8
View File
@@ -0,0 +1,8 @@
import { create } from '@storybook/theming/create';
export default create({
base: 'light',
brandTitle: 'Maputnik',
brandUrl: 'https://github.com/maputnik/editor',
});
-24
View File
@@ -1,24 +0,0 @@
## 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
-2
View File
@@ -1,2 +0,0 @@
# Contributor Covenant
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)
+3 -3
View File
@@ -2,12 +2,12 @@ FROM node:18 as builder
WORKDIR /maputnik WORKDIR /maputnik
# Only copy package.json to prevent npm install from running on every build # Only copy package.json to prevent npm install from running on every build
COPY package.json package-lock.json .npmrc ./ COPY package.json package-lock.json ./
RUN npm ci RUN npm install
# Build maputnik # Build maputnik
COPY . . COPY . .
RUN npx vite build RUN npm run build
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Create a clean nginx-alpine slim image with just the build results # Create a clean nginx-alpine slim image with just the build results
-1
View File
@@ -1,7 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015 Lukas Martinelli Copyright (c) 2015 Lukas Martinelli
Copyright (c) 2024 MapLibre contributors
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+69 -24
View File
@@ -1,10 +1,10 @@
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" /> <img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
# Maputnik # Maputnik
[![GitHub CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci] [![GitHub CI status](https://github.com/maputnik/editor/workflows/ci/badge.svg)][github-action-ci]
[![License](https://img.shields.io/badge/license-MIT-blue.svg)][license] [![License](https://img.shields.io/badge/license-MIT-blue.svg)][license]
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci [github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
[license]: https://tldrlegal.com/license/mit-license [license]: https://tldrlegal.com/license/mit-license
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/) A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
@@ -13,32 +13,33 @@ targeted at developers and map designers.
## Usage ## Usage
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage) - :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/wiki/Maputnik-CLI) for local style development - :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/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. - In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
```bash ```bash
docker run -it --rm -p 8888:80 ghcr.io/maplibre/maputnik:main docker run -it --rm -p 8888:8888 maputnik/editor
``` ```
## 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 ## Documentation
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate! The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)** - :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo - :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
[![Design Map from Scratch](https://j.gifs.com/g5XMgl.gif)](https://youtu.be/XoDh0gEnBQo) [![Design Map from Scratch](https://j.gifs.com/g5XMgl.gif)](https://youtu.be/XoDh0gEnBQo)
## Develop ## Develop
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/). 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/).
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule). 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/`. Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
```bash ```bash
@@ -66,8 +67,7 @@ Lint the JavaScript code.
``` ```
# run linter # run linter
npm run lint npm run lint
npm run lint-css npm run lint-styles
npm run sort-styles
``` ```
@@ -94,23 +94,68 @@ You can also see the tests as they run or select which suites to run by executin
npm run cy:open npm run cy:open
``` ```
## Release process
1. Review [`CHANGELOG.md`](/CHANGELOG.md) ## Related Projects
- 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 ## 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. 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.
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/. ### 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
## License ## License
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and Maplibre contributors. Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and 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.
**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.
-2
View File
@@ -1,2 +0,0 @@
For an up-to-date policy refer to
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt
-11
View File
@@ -1,11 +0,0 @@
# Build Scripts
This folder holds common build scripts used by some of the Github workflows.
The scripts are borrowed from [maplibre/maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/tree/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build).
## Generate Release Notes
`bump-version-changelog.js` Used to update the changelog with the current notes, and set up a space for new notes
`release-notes.js` Used to generate release notes when releasing a new version
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env node
/**
* This script updates the changelog.md file with the version given in the arguments
* It replaces ## main with ## <version>
* Removes _...Add new stuff here..._
* And adds on top a ## main with add stuff here.
*
* Copied from maplibre/maplibre-gl-js
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
*/
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
let changelog = fs.readFileSync(changelogPath, 'utf8');
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
changelog = `## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
` + changelog;
fs.writeFileSync(changelogPath, changelog, 'utf8');
-48
View File
@@ -1,48 +0,0 @@
#!/usr/bin/env node
// Copied from maplibre/maplibre-gl-js
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
const changelog = fs.readFileSync(changelogPath, 'utf8');
/*
Parse the raw changelog text and split it into individual releases.
This regular expression:
- Matches lines starting with "## x.x.x".
- Groups the version number.
- Skips the (optional) release date.
- Groups the changelog content.
- Ends when another "## x.x.x" is found.
*/
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
let releaseNotes = [];
let match;
// eslint-disable-next-line no-cond-assign
while (match = regex.exec(changelog)) {
releaseNotes.push({
'version': match[1],
'changelog': match[2].trim(),
});
}
const latest = releaseNotes[0];
const previous = releaseNotes[1];
// Print the release notes template.
let header = 'Changes since previous version'
if (previous) {
header = `https://github.com/maplibre/maputnik
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`
}
const templatedReleaseNotes = `${header}
${latest.changelog}`;
// eslint-disable-next-line eol-last
process.stdout.write(templatedReleaseNotes.trimEnd());
-35
View File
@@ -87,39 +87,4 @@ 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",
},
],
});
});
}); });
-35
View File
@@ -1,35 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("i18n", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("language detector", () => {
it("English", () => {
const url = "?lng=en";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
});
it("Japanese", () => {
const url = "?lng=ja";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
describe("language switcher", () => {
beforeEach(() => {
when.setStyle("layer");
});
it("the language switcher switches to Japanese", () => {
const selector = "maputnik-lang-select";
then(get.elementByTestId(selector)).shouldExist();
when.select(selector, "ja");
then(get.elementByTestId(selector)).shouldHaveValue("ja");
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
});
});
});
+2 -20
View File
@@ -266,7 +266,7 @@ describe("layers", () => {
beforeEach(() => { beforeEach(() => {
bgId = createBackground(); bgId = createBackground();
when.click("layer-list-item:background:" + bgId); when.click("layer-list-item:background:" + bgId);
when.click("spec-field:background-color"); when.click("spec-field:background-color", 0);
}); });
it("should update style in local storage", () => { it("should update style in local storage", () => {
@@ -280,25 +280,6 @@ describe("layers", () => {
}); });
}); });
}); });
describe("opacity", () => {
let bgId: string;
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.type("spec-field-input:background-opacity", "0.");
});
it("should keep '.' in the input field", () => {
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
});
it("should revert to a valid value when focus out", () => {
when.click("layer-list-item:background:" + bgId);
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue('0');
});
});
}); });
describe("filter", () => { describe("filter", () => {
@@ -325,6 +306,7 @@ describe("layers", () => {
when.click("layer-list-item:background:" + bgId); when.click("layer-list-item:background:" + bgId);
let errorSelector = ".CodeMirror-lint-marker-error"; let errorSelector = ".CodeMirror-lint-marker-error";
// Not a good idea to check that something doesn't exist. infinite things don't exist.
then(get.elementByTestId(errorSelector)).shouldNotExist(); then(get.elementByTestId(errorSelector)).shouldNotExist();
when.click(".CodeMirror"); when.click(".CodeMirror");
-6
View File
@@ -23,10 +23,4 @@ describe("map", () => {
); );
}); });
}); });
describe("search", () => {
it('should exist', () => {
then(get.searchControl()).shouldBeVisible();
});
});
}); });
+10 -8
View File
@@ -1,8 +1,6 @@
/// <reference types="cypress-plugin-tab" />
import { CypressHelper } from "@shellygo/cypress-test-utils"; import { CypressHelper } from "@shellygo/cypress-test-utils";
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable"; import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
import MaputnikCypressHelper from "./maputnik-cypress-helper"; import MaputnikCypressHelper from "./cypress-wrapper-driver";
import ModalDriver from "./modal-driver"; import ModalDriver from "./modal-driver";
const baseUrl = "http://localhost:8888/"; const baseUrl = "http://localhost:8888/";
@@ -16,7 +14,7 @@ const styleFromWindow = (win: Window) => {
export class MaputnikAssertable<T> extends Assertable<T> { export class MaputnikAssertable<T> extends Assertable<T> {
shouldEqualToStoredStyle = () => shouldEqualToStoredStyle = () =>
then( then(
new CypressHelper().get.window().then((win: Window) => { new CypressHelper().get.window().then((win) => {
const style = styleFromWindow(win); const style = styleFromWindow(win);
then(this.chainable).shouldDeepNestedInclude(style); then(this.chainable).shouldDeepNestedInclude(style);
}) })
@@ -131,9 +129,7 @@ export class MaputnikDriver {
this.helper.when.acceptConfirm(); this.helper.when.acceptConfirm();
} }
// when methods should not include assertions // when methods should not include assertions
const toolbarLink = this.helper.get.elementByTestId("toolbar:link") this.helper.get.elementByTestId("toolbar:link").should("be.visible");
toolbarLink.scrollIntoView();
toolbarLink.should("be.visible");
}, },
typeKeys: (keys: string) => this.helper.get.element("body").type(keys), typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
@@ -173,6 +169,13 @@ export class MaputnikDriver {
styleFromLocalStorage: () => styleFromLocalStorage: () =>
this.helper.get.window().then((win) => styleFromWindow(win)), this.helper.get.window().then((win) => styleFromWindow(win)),
maputnikStyleFromLocalStorageObj: () => {
const styleId = localStorage.getItem("maputnik:latest_style");
const styleItem = localStorage.getItem(`maputnik:style:${styleId}`);
const obj = JSON.parse(styleItem || "");
return obj;
},
exampleFileUrl: () => { exampleFileUrl: () => {
return baseUrl + "example-style.json"; return baseUrl + "example-style.json";
}, },
@@ -181,6 +184,5 @@ export class MaputnikDriver {
skipTargetLayerEditor: () => skipTargetLayerEditor: () =>
this.helper.get.elementByTestId("skip-target-layer-editor"), this.helper.get.elementByTestId("skip-target-layer-editor"),
canvas: () => this.helper.get.element("canvas"), canvas: () => this.helper.get.element("canvas"),
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
}; };
} }
+1 -1
View File
@@ -1,5 +1,5 @@
import { v1 as uuid } from "uuid"; import { v1 as uuid } from "uuid";
import MaputnikCypressHelper from "./maputnik-cypress-helper"; import MaputnikCypressHelper from "./cypress-wrapper-driver";
export default class ModalDriver { export default class ModalDriver {
private helper = new MaputnikCypressHelper(); private helper = new MaputnikCypressHelper();
+6 -6
View File
@@ -5,7 +5,7 @@ describe("modals", () => {
beforeAndAfter(); beforeAndAfter();
beforeEach(() => { beforeEach(() => {
when.setStyle(""); when.visit("/");
}); });
describe("open", () => { describe("open", () => {
beforeEach(() => { beforeEach(() => {
@@ -51,7 +51,7 @@ describe("modals", () => {
when.click("nav:export"); when.click("nav:export");
}); });
it("close", () => { it.skip("close", () => {
when.modal.close("modal:export"); when.modal.close("modal:export");
then(get.elementByTestId("modal:export")).shouldNotExist(); then(get.elementByTestId("modal:export")).shouldNotExist();
}); });
@@ -79,12 +79,12 @@ describe("modals", () => {
when.click("nav:settings"); when.click("nav:settings");
}); });
describe("when click name filed spec information", () => { describe("when click name", () => {
beforeEach(() => { beforeEach(() => {
when.click("field-doc-button-Name"); when.click("field-doc-button-Name");
}); });
it("should show the spec information", () => { it("name", () => {
then(get.elementsText("spec-field-doc")).shouldInclude( then(get.elementsText("spec-field-doc")).shouldInclude(
"name for the style" "name for the style"
); );
@@ -142,7 +142,7 @@ describe("modals", () => {
); );
when.click("modal:settings.name"); when.click("modal:settings.name");
then( then(
get.styleFromLocalStorage().then((style) => style.metadata) get.styleFromLocalStorage().pipe((style) => style.metadata)
).shouldInclude({ ).shouldInclude({
"maputnik:openmaptiles_access_token": apiKey, "maputnik:openmaptiles_access_token": apiKey,
}); });
@@ -156,7 +156,7 @@ describe("modals", () => {
); );
when.click("modal:settings.name"); when.click("modal:settings.name");
then( then(
get.styleFromLocalStorage().then((style) => style.metadata) get.styleFromLocalStorage().pipe((style) => style.metadata)
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey }); ).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
}); });
-31
View File
@@ -1,31 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
editor
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Binary version of pubilic/editor
rice-box.go
# Built binary
maputnik
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2016 Maputnik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-39
View File
@@ -1,39 +0,0 @@
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
BINARY=maputnik
DESKTOP_VERSION := 1.1.1
EDITOR_VERSION := $(shell node -p "require('../package.json').version")
GOPATH := $(if $(GOPATH),$(GOPATH),$(HOME)/go)
GOBIN := $(if $(GOBIN),$(GOBIN),$(HOME)/go/bin)
all: $(BINARY)
$(BINARY): $(GOBIN)/gox $(SOURCES) version.go rice-box.go
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
# Copy the current release into ./editor/maputnik so it can be
# embedded in the binary
editor/pull_release:
mkdir -p editor
cp -r ../dist/* editor
$(GOBIN)/gox:
go install github.com/mitchellh/gox@v1.0.1
$(GOBIN)/rice:
go install github.com/GeertJohan/go.rice/rice@v1.0.3
# Embed the current version numbers in the executable by writing version.go
.PHONY: version.go
version.go:
@echo "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
@echo "package main\n" >> version.go
@echo "const DesktopVersion = \"$(DESKTOP_VERSION)\"" >> version.go
@echo "const EditorVersion = \"$(EDITOR_VERSION)\"" >> version.go
rice-box.go: $(GOBIN)/rice editor/pull_release
$(GOBIN)/rice embed-go
.PHONY: clean
clean:
rm -rf editor && rm -f rice-box.go && rm -rf bin
-72
View File
@@ -1,72 +0,0 @@
# Maputnik Desktop [![GitHub CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci]
---
A Golang based cross platform executable for integrating Maputnik locally.
This binary packages up the JavaScript and CSS bundle produced by maputnik
and embeds it in the program for easy distribution. It also allows
exposing a local style file and work on it both in Maputnik and with your favorite
editor.
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
## Install
You can download a single binary for Linux, OSX or Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/editor/releases/latest).
### Usage
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
```bash
maputnik
```
Expose a local style file to Maputnik allowing the web based editor
to save to the local filesystem.
```bash
maputnik --file basic-v9.json
```
Watch the local style for changes and inform the editor via web socket.
This makes it possible to edit the style with a local text editor and still
use Maputnik.
```bash
maputnik --watch --file basic-v9.json
```
Choose a local port to listen on, instead of using the default port 8000.
```bash
maputnik --port 8001
```
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
Could be used to serve sprites and glyphs.
```bash
maputnik --static ./localFolder
```
### API
`maputnik` exposes the configured styles via a HTTP API.
| Method | Description
|---------------------------------|---------------------------------------
| `GET /styles` | List the ID of all configured style files
| `GET /styles/{filename}` | Get contents of a single style file
| `PUT /styles/{filename}` | Update contents of a style file
| `WEBSOCKET /ws` | Listen to change events for the configured style files
### Build
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
```
npm install
npm run build-desktop
```
You should now find the `maputnik` binary in your `bin` directory.
-81
View File
@@ -1,81 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/gorilla/mux"
)
func StyleFileAccessor(filename string) styleFileAccessor {
return styleFileAccessor{filename, styleId(filename)}
}
func styleId(filename string) string {
raw, err := ioutil.ReadFile(filename)
if err != nil {
log.Panicln(err)
}
var spec styleSpec
err = json.Unmarshal(raw, &spec)
if err != nil {
log.Panicln(err)
}
if spec.Id == "" {
fmt.Println("No id in style")
}
return spec.Id
}
type styleSpec struct {
Id string `json:"id"`
}
// Allows access to a single style file
type styleFileAccessor struct {
filename string
id string
}
func (fa styleFileAccessor) ListFiles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.Encode([]string{fa.id})
}
func (fa styleFileAccessor) ReadFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Choose right file
// right now we just return the single file we know of
w.Header().Set("Content-Type", "application/json")
raw, err := ioutil.ReadFile(fa.filename)
if err != nil {
log.Panicln(err)
}
w.Write(raw)
}
func (fa styleFileAccessor) SaveFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Save to right file
w.Header().Set("Content-Type", "application/json")
body, _ := ioutil.ReadAll(r.Body)
var out bytes.Buffer
json.Indent(&out, body, "", " ")
if err := ioutil.WriteFile(fa.filename, out.Bytes(), 0666); err != nil {
log.Fatalf("Can not copy from request to file: %s", err.Error())
}
}
-69
View File
@@ -1,69 +0,0 @@
package filewatch
import (
"io/ioutil"
"log"
"net/http"
"github.com/fsnotify/fsnotify"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func writer(ws *websocket.Conn, filename string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("Modified file:", event.Name)
var p []byte
var err error
p, err = ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
if p != nil {
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
return
}
}
}
case err := <-watcher.Errors:
log.Println("Watch error:", err)
}
}
}()
if err = watcher.Add(filename); err != nil {
log.Fatal(err)
}
<-done
}
func ServeWebsocketFileWatcher(filename string, w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Println(err)
}
return
}
writer(ws, filename)
defer ws.Close()
}
-27
View File
@@ -1,27 +0,0 @@
module maputnik/desktop
go 1.19
require (
github.com/GeertJohan/go.rice v1.0.3
github.com/fsnotify/fsnotify v1.6.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/maputnik/desktop v1.0.7
github.com/urfave/cli v1.22.12
)
require (
github.com/GeertJohan/go.incremental v1.0.0 // indirect
github.com/akavel/rsrc v0.8.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/daaku/go.zipexe v1.0.2 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/nkovacs/streamquote v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.0.1 // indirect
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
)
-54
View File
@@ -1,54 +0,0 @@
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4=
github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ=
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-80
View File
@@ -1,80 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/GeertJohan/go.rice"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/maputnik/desktop/filewatch"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "maputnik"
app.Usage = "Server for integrating Maputnik locally"
app.Version = "Editor: " + EditorVersion + "; Desktop: " + DesktopVersion
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "file, f",
Usage: "Allow access to JSON style from web client",
},
&cli.BoolFlag{
Name: "watch",
Usage: "Notify web client about JSON style file changes",
},
&cli.IntFlag{
Name: "port",
Value: 8000,
Usage: "TCP port to listen on",
},
&cli.StringFlag{
Name: "static",
Usage: "Serve directory under /static/",
},
}
app.Action = func(c *cli.Context) error {
gui := http.FileServer(rice.MustFindBox("editor").HTTPBox())
router := mux.NewRouter().StrictSlash(true)
filename := c.String("file")
if filename != "" {
fmt.Printf("%s is accessible via Maputnik\n", filename)
// Allow access to reading and writing file on the local system
path, _ := filepath.Abs(filename)
accessor := StyleFileAccessor(path)
router.Path("/styles").Methods("GET").HandlerFunc(accessor.ListFiles)
router.Path("/styles/{styleId}").Methods("GET").HandlerFunc(accessor.ReadFile)
router.Path("/styles/{styleId}").Methods("PUT").HandlerFunc(accessor.SaveFile)
// Register websocket to notify we clients about file changes
if c.Bool("watch") {
router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
filewatch.ServeWebsocketFileWatcher(filename, w, r)
})
}
}
staticDir := c.String("static")
if staticDir != "" {
h := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
router.PathPrefix("/static/").Handler(h)
}
router.PathPrefix("/").Handler(http.StripPrefix("/", gui))
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter)
fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port"))
return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter)
}
app.Run(os.Args)
}
-17
View File
@@ -1,17 +0,0 @@
export default {
output: 'src/locales/$LOCALE/$NAMESPACE.json',
locales: [ 'ja', 'he','zh' ],
// Because some keys are dynamically generated, i18next-parser can't detect them.
// We add these keys manually, so we don't want to remove them.
keepRemoved: true,
// We use plain English keys, so we disable key and namespace separators.
keySeparator: false,
namespaceSeparator: false,
defaultValue: (locale, ns, key) => {
// The default value is a string that indicates that the string is not translated.
return '__STRING_NOT_TRANSLATED__';
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+10757 -3322
View File
File diff suppressed because it is too large Load Diff
+60 -60
View File
@@ -1,49 +1,41 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "2.1.1", "version": "2.0.0-pre.2",
"description": "A MapLibre GL visual style editor", "description": "A MapLibre GL visual style editor",
"type": "module", "type": "module",
"main": "''", "main": "''",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"build": "tsc && vite build --base=/maputnik/", "build": "tsc && vite build",
"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", "lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "cypress run", "test": "cypress run",
"cy:open": "cypress open", "cy:open": "cypress open",
"lint-css": "stylelint \"src/styles/*.scss\"", "lint-css": "stylelint \"src/styles/*.scss\"",
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json" "storybook": "storybook dev -h 0.0.0.0 -p 6006",
"build-storybook": "storybook build -o build/storybook"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/maplibre/maputnik" "url": "https://github.com/maputnik/editor"
}, },
"author": "Lukas Martinelli", "author": "Lukas Martinelli",
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/maplibre/maputnik#readme", "homepage": "https://github.com/maputnik/editor#readme",
"dependencies": { "dependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@maplibre/maplibre-gl-geocoder": "^1.6.0", "@maplibre/maplibre-gl-style-spec": "^17.0.1",
"@maplibre/maplibre-gl-inspect": "^1.6.3", "@mdi/js": "^6.6.96",
"@maplibre/maplibre-gl-style-spec": "^20.1.1", "@mdi/react": "^1.5.0",
"@mdi/js": "^7.4.47", "@typescript-eslint/eslint-plugin": "^6.16.0",
"@mdi/react": "^1.6.1", "@typescript-eslint/parser": "^6.16.0",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"array-move": "^4.0.0", "array-move": "^4.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"classnames": "^2.5.1", "classnames": "^2.3.1",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"color": "^4.2.3", "color": "^4.2.3",
"cypress-plugin-tab": "^1.0.5",
"detect-browser": "^5.3.0", "detect-browser": "^5.3.0",
"events": "^3.3.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^23.12.2", "json-stringify-pretty-compact": "^3.0.0",
"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", "json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7", "jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -53,30 +45,30 @@
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"maplibre-gl": "^4.1.2", "mapbox-gl-inspect": "^1.3.1",
"maplibre-gl": "^2.4.0",
"maputnik-design": "github:maputnik/design#172b06c", "maputnik-design": "github:maputnik/design#172b06c",
"ol": "^6.14.1", "ol": "^6.14.1",
"ol-mapbox-style": "^7.1.1", "ol-mapbox-style": "^7.1.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^16.0.0",
"react-accessible-accordion": "^5.0.0", "react-accessible-accordion": "^4.0.0",
"react-aria-menubutton": "^7.0.3", "react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^5.0.2", "react-aria-modal": "^4.0.1",
"react-autobind": "^1.0.6", "react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1", "react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1", "react-collapse": "^5.1.1",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^18.2.0", "react-dom": "^16.0.0",
"react-file-reader-input": "^2.0.0", "react-file-reader-input": "^2.0.0",
"react-i18next": "^15.0.1",
"react-icon-base": "^2.1.2", "react-icon-base": "^2.1.2",
"react-icons": "^5.0.1", "react-icons": "^4.3.1",
"react-sortable-hoc": "^2.0.0", "react-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"sass": "^1.72.0", "sass": "^1.50.0",
"slugify": "^1.6.6", "slugify": "^1.6.5",
"string-hash": "^1.1.3", "string-hash": "^1.1.3",
"url": "^0.11.3" "url": "^0.11.0"
}, },
"jshintConfig": { "jshintConfig": {
"esversion": 6 "esversion": 6
@@ -96,15 +88,24 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@cypress/code-coverage": "^3.12.30", "@cypress/code-coverage": "^3.12.15",
"@istanbuljs/nyc-config-typescript": "^1.0.2", "@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-replace": "^5.0.5",
"@shellygo/cypress-test-utils": "^2.1.9", "@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",
"@types/codemirror": "^5.60.15", "@types/codemirror": "^5.60.15",
"@types/color": "^3.0.6", "@types/color": "^3.0.6",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/geojson": "^7946.0.14", "@types/geojson": "^7946.0.13",
"@types/json-to-ast": "^2.1.4", "@types/json-to-ast": "^2.1.4",
"@types/lodash.capitalize": "^4.2.9", "@types/lodash.capitalize": "^4.2.9",
"@types/lodash.clamp": "^4.0.9", "@types/lodash.clamp": "^4.0.9",
@@ -112,38 +113,37 @@
"@types/lodash.get": "^4.4.9", "@types/lodash.get": "^4.4.9",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9", "@types/lodash.throttle": "^4.1.9",
"@types/mocha": "^10.0.6", "@types/react": "^16.14.52",
"@types/randomcolor": "^0.5.9", "@types/react-aria-menubutton": "^6.2.13",
"@types/react": "^18.2.67", "@types/react-aria-modal": "^4.0.9",
"@types/react-aria-menubutton": "^6.2.14", "@types/react-autocomplete": "^1.8.9",
"@types/react-aria-modal": "^4.0.10",
"@types/react-autocomplete": "^1.8.10",
"@types/react-collapse": "^5.0.4", "@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.12", "@types/react-color": "^3.0.10",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^16.9.24",
"@types/react-file-reader-input": "^2.0.4", "@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6", "@types/react-icon-base": "^2.1.6",
"@types/string-hash": "^1.1.3", "@types/string-hash": "^1.1.3",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"cypress": "^13.13.0", "cypress": "^13.6.1",
"eslint": "^8.57.0", "eslint": "^8.56.0",
"eslint-plugin-react": "^7.34.1", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-react-refresh": "^0.4.5",
"i18next-parser": "^9.0.1", "express": "^4.17.3",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2", "istanbul-lib-coverage": "^3.2.0",
"mocha": "^10.3.0", "mocha": "^9.2.2",
"postcss": "^8.4.38", "postcss": "^8.4.12",
"react-hot-loader": "^4.13.1", "react-hot-loader": "^4.13.0",
"stylelint": "^16.2.1", "storybook": "^7.6.5",
"stylelint-config-recommended-scss": "^14.0.0", "stylelint": "^14.6.1",
"stylelint-scss": "^6.2.1", "stylelint-config-recommended-scss": "^6.0.0",
"typescript": "^5.4.3", "stylelint-scss": "^4.2.0",
"uuid": "^9.0.1", "typescript": "^5.3.3",
"vite": "^5.2.6", "uuid": "^8.3.2",
"vite-plugin-istanbul": "^6.0.0" "vite": "^5.0.0",
"vite-plugin-istanbul": "^5.0.0"
} }
} }
+16 -2
View File
@@ -9,7 +9,7 @@ import {unset} from 'lodash'
import {arrayMoveMutable} from 'array-move' import {arrayMoveMutable} from 'array-move'
import hash from "string-hash"; import hash from "string-hash";
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl' import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec' import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
import MapMaplibreGl from './MapMaplibreGl' import MapMaplibreGl from './MapMaplibreGl'
import MapOpenLayers from './MapOpenLayers' import MapOpenLayers from './MapOpenLayers'
@@ -24,6 +24,7 @@ import ModalExport from './ModalExport'
import ModalSources from './ModalSources' import ModalSources from './ModalSources'
import ModalOpen from './ModalOpen' import ModalOpen from './ModalOpen'
import ModalShortcuts from './ModalShortcuts' import ModalShortcuts from './ModalShortcuts'
import ModalSurvey from './ModalSurvey'
import ModalDebug from './ModalDebug' import ModalDebug from './ModalDebug'
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata' import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
@@ -127,6 +128,7 @@ type AppState = {
open: boolean open: boolean
shortcuts: boolean shortcuts: boolean
export: boolean export: boolean
survey: boolean
debug: boolean debug: boolean
} }
} }
@@ -135,6 +137,7 @@ export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore; revisionStore: RevisionStore;
styleStore: StyleStore | ApiStyleStore; styleStore: StyleStore | ApiStyleStore;
layerWatcher: LayerWatcher; layerWatcher: LayerWatcher;
shortcutEl: ModalShortcuts | null = null;
constructor(props: any) { constructor(props: any) {
super(props) super(props)
@@ -274,6 +277,7 @@ export default class App extends React.Component<any, AppState> {
shortcuts: false, shortcuts: false,
export: false, export: false,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor // TODO: Disabled for now, this should be opened on the Nth visit to the editor
survey: false,
debug: false, debug: false,
}, },
maplibreGlDebugOptions: { maplibreGlDebugOptions: {
@@ -375,7 +379,8 @@ export default class App extends React.Component<any, AppState> {
this.getInitialStateFromUrl(newStyle); this.getInitialStateFromUrl(newStyle);
} }
const errors: ValidationError[] = validateStyleMin(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) || [];
// The validate function doesn't give us errors for duplicate error with // The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here. // empty string for layer.id, manually deal with that here.
@@ -835,6 +840,10 @@ export default class App extends React.Component<any, AppState> {
} }
setModal(modalName: keyof AppState["isOpen"], value: boolean) { setModal(modalName: keyof AppState["isOpen"], value: boolean) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
this.setState({ this.setState({
isOpen: { isOpen: {
...this.state.isOpen, ...this.state.isOpen,
@@ -934,6 +943,7 @@ export default class App extends React.Component<any, AppState> {
mapView={this.state.mapView} mapView={this.state.mapView}
/> />
<ModalShortcuts <ModalShortcuts
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts} isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')} onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/> />
@@ -961,6 +971,10 @@ export default class App extends React.Component<any, AppState> {
isOpen={this.state.isOpen.sources} isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')} onOpenToggle={this.toggleModal.bind(this, 'sources')}
/> />
<ModalSurvey
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div> </div>
return <AppLayout return <AppLayout
+12 -18
View File
@@ -1,18 +1,17 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer' import ScrollContainer from './ScrollContainer'
import { WithTranslation, withTranslation } from 'react-i18next';
type AppLayoutInternalProps = { type AppLayoutProps = {
toolbar: React.ReactElement toolbar: React.ReactElement
layerList: React.ReactElement layerList: React.ReactElement
layerEditor?: React.ReactElement layerEditor?: React.ReactElement
map: React.ReactElement map: React.ReactElement
bottom?: React.ReactElement bottom?: React.ReactElement
modals?: React.ReactNode modals?: React.ReactNode
} & WithTranslation; };
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> { class AppLayout extends React.Component<AppLayoutProps> {
static childContextTypes = { static childContextTypes = {
reactIconBase: PropTypes.object reactIconBase: PropTypes.object
} }
@@ -24,21 +23,17 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
} }
render() { render() {
document.body.dir = this.props.i18n.dir();
return <div className="maputnik-layout"> return <div className="maputnik-layout">
{this.props.toolbar} {this.props.toolbar}
<div className="maputnik-layout-main"> <div className="maputnik-layout-list">
<div className="maputnik-layout-list"> {this.props.layerList}
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
</div> </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 className="maputnik-layout-bottom">
{this.props.bottom} {this.props.bottom}
</div> </div>
@@ -48,5 +43,4 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
} }
} }
const AppLayout = withTranslation()(AppLayoutInternal); export default AppLayout
export default AppLayout;
+7 -12
View File
@@ -1,24 +1,23 @@
import React from 'react' import React from 'react'
import {formatLayerId} from '../libs/format'; import {formatLayerId} from '../libs/format';
import {LayerSpecification, StyleSpecification} from 'maplibre-gl'; import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
type AppMessagePanelInternalProps = { type AppMessagePanelProps = {
errors?: unknown[] errors?: unknown[]
infos?: string[] infos?: unknown[]
mapStyle?: StyleSpecification mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown onLayerSelect?(...args: unknown[]): unknown
currentLayer?: LayerSpecification currentLayer?: LayerSpecification
selectedLayerIndex?: number selectedLayerIndex?: number
} & WithTranslation; };
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> { export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
static defaultProps = { static defaultProps = {
onLayerSelect: () => {}, onLayerSelect: () => {},
} }
render() { render() {
const {t, selectedLayerIndex} = this.props; const {selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => { const errors = this.props.errors?.map((error: any, idx) => {
let content; let content;
if (error.parsed && error.parsed.type === "layer") { if (error.parsed && error.parsed.type === "layer") {
@@ -26,9 +25,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
const layerId = this.props.mapStyle?.layers[parsed.data.index].id; const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
content = ( content = (
<> <>
<Trans t={t}> Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
</Trans>
{selectedLayerIndex !== parsed.data.index && {selectedLayerIndex !== parsed.data.index &&
<> <>
&nbsp;&mdash;&nbsp; &nbsp;&mdash;&nbsp;
@@ -36,7 +33,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
className="maputnik-message-panel__switch-button" className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect!(parsed.data.index)} onClick={() => this.props.onLayerSelect!(parsed.data.index)}
> >
{t("switch to layer")} switch to layer
</button> </button>
</> </>
} }
@@ -62,5 +59,3 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
} }
} }
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
export default AppMessagePanel;
+51 -55
View File
@@ -2,12 +2,9 @@ import React from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import {detect} from 'detect-browser'; import {detect} from 'detect-browser';
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdLanguage} from 'react-icons/md' import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
import pkgJson from '../../package.json' 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. // 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(); const browser = detect();
@@ -46,6 +43,29 @@ 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 = { type ToolbarSelectProps = {
children?: React.ReactNode children?: React.ReactNode
wdKey?: string wdKey?: string
@@ -82,7 +102,7 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia"; export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
type AppToolbarInternalProps = { type AppToolbarProps = {
mapStyle: object mapStyle: object
inspectModeEnabled: boolean inspectModeEnabled: boolean
onStyleChanged(...args: unknown[]): unknown onStyleChanged(...args: unknown[]): unknown
@@ -95,9 +115,9 @@ type AppToolbarInternalProps = {
onSetMapState(mapState: MapState): unknown onSetMapState(mapState: MapState): unknown
mapState?: MapState mapState?: MapState
renderer?: string renderer?: string
} & WithTranslation; };
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> { export default class AppToolbar extends React.Component<AppToolbarProps> {
state = { state = {
isOpen: { isOpen: {
settings: false, settings: false,
@@ -112,10 +132,6 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
this.props.onSetMapState(val); this.props.onSetMapState(val);
} }
handleLanguageChange(val: string) {
this.props.i18n.changeLanguage(val);
}
onSkip = (target: string) => { onSkip = (target: string) => {
if (target === "map") { if (target === "map") {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus(); (document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
@@ -127,41 +143,40 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
} }
render() { render() {
const t = this.props.t;
const views = [ const views = [
{ {
id: "map", id: "map",
group: "general", group: "general",
title: t("Map"), title: "Map",
}, },
{ {
id: "inspect", id: "inspect",
group: "general", group: "general",
title: t("Inspect"), title: "Inspect",
disabled: this.props.renderer === 'ol', disabled: this.props.renderer === 'ol',
}, },
{ {
id: "filter-deuteranopia", id: "filter-deuteranopia",
group: "color-accessibility", group: "color-accessibility",
title: t("Deuteranopia filter"), title: "Deuteranopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-protanopia", id: "filter-protanopia",
group: "color-accessibility", group: "color-accessibility",
title: t("Protanopia filter"), title: "Protanopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-tritanopia", id: "filter-tritanopia",
group: "color-accessibility", group: "color-accessibility",
title: t("Tritanopia filter"), title: "Tritanopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-achromatopsia", id: "filter-achromatopsia",
group: "color-accessibility", group: "color-accessibility",
title: t("Achromatopsia filter"), title: "Achromatopsia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
]; ];
@@ -181,29 +196,29 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
className="maputnik-toolbar-skip" className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-list")} onClick={_e => this.onSkip("layer-list")}
> >
{t("Layers list")} Layers list
</button> </button>
<button <button
data-wd-key="root:skip:layer-editor" data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip" className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-editor")} onClick={_e => this.onSkip("layer-editor")}
> >
{t("Layer editor")} Layer editor
</button> </button>
<button <button
data-wd-key="root:skip:map-view" data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip" className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("map")} onClick={_e => this.onSkip("map")}
> >
{t("Map view")} Map view
</button> </button>
<a <a
className="maputnik-toolbar-logo" className="maputnik-toolbar-logo"
target="blank" target="blank"
rel="noreferrer noopener" rel="noreferrer noopener"
href="https://github.com/maplibre/maputnik" href="https://github.com/maputnik/editor"
> >
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} /> <img src="node_modules/maputnik-design/logos/logo-color.svg" />
<h1> <h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span> <span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span> <span className="maputnik-toolbar-version">v{pkgJson.version}</span>
@@ -213,24 +228,24 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar"> <div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}> <ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser /> <MdOpenInBrowser />
<IconText>{t("Open")}</IconText> <IconText>Open</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}> <ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdFileDownload /> <MdFileDownload />
<IconText>{t("Export")}</IconText> <IconText>Export</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}> <ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers /> <MdLayers />
<IconText>{t("Data Sources")}</IconText> <IconText>Data Sources</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}> <ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings /> <MdSettings />
<IconText>{t("Style Settings")}</IconText> <IconText>Style Settings</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarSelect wdKey="nav:inspect"> <ToolbarSelect wdKey="nav:inspect">
<MdFindInPage /> <MdFindInPage />
<label>{t("View")} <label>View
<select <select
className="maputnik-select" className="maputnik-select"
data-wd-key="maputnik-select" data-wd-key="maputnik-select"
@@ -244,7 +259,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</option> </option>
); );
})} })}
<optgroup label={t("Color accessibility")}> <optgroup label="Color accessibility">
{views.filter(v => v.group === "color-accessibility").map((item) => { {views.filter(v => v.group === "color-accessibility").map((item) => {
return ( return (
<option key={item.id} value={item.id} disabled={item.disabled}> <option key={item.id} value={item.id} disabled={item.disabled}>
@@ -257,35 +272,16 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</label> </label>
</ToolbarSelect> </ToolbarSelect>
<ToolbarSelect wdKey="nav:language"> <ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<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 /> <MdHelpOutline />
<IconText>{t("Help")}</IconText> <IconText>Help</IconText>
</ToolbarLink> </ToolbarLink>
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
<MdAssignmentTurnedIn />
<IconText>Take the Maputnik Survey</IconText>
</ToolbarLinkHighlighted>
</div> </div>
</div> </div>
</nav> </nav>
} }
} }
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;
+2 -2
View File
@@ -1,10 +1,10 @@
import React, {PropsWithChildren, SyntheticEvent} from 'react' import React, {SyntheticEvent} from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import FieldDocLabel from './FieldDocLabel' import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc' import Doc from './Doc'
type BlockProps = PropsWithChildren & { type BlockProps = {
"data-wd-key"?: string "data-wd-key"?: string
label?: string label?: string
action?: React.ReactElement action?: React.ReactElement
+1 -1
View File
@@ -31,7 +31,7 @@ export default class Doc extends React.Component<DocProps> {
const renderValues = ( const renderValues = (
!!values && !!values &&
// HACK: Currently we merge additional values into the style spec, so this is required // HACK: Currently we merge additional values into the style spec, so this is required
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16> // See <https://github.com/maputnik/editor/blob/main/src/components/PropertyGroup.jsx#L16>
!Array.isArray(values) !Array.isArray(values)
); );
+6 -11
View File
@@ -2,23 +2,21 @@ import React from 'react'
import Block from './Block' import Block from './Block'
import InputString from './InputString' import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldCommentInternalProps = { type FieldCommentProps = {
value?: string value?: string
onChange(value: string | undefined): unknown onChange(value: string | undefined): unknown
error: {message: string} error: {message: string}
} & WithTranslation; };
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> { export default class FieldComment extends React.Component<FieldCommentProps> {
render() { render() {
const t = this.props.t;
const fieldSpec = { const fieldSpec = {
doc: t("Comments for the current layer. This is non-standard and not in the spec."), doc: "Comments for the current layer. This is non-standard and not in the spec."
}; };
return <Block return <Block
label={t("Comments")} label={"Comments"}
fieldSpec={fieldSpec} fieldSpec={fieldSpec}
data-wd-key="layer-comment" data-wd-key="layer-comment"
error={this.props.error} error={this.props.error}
@@ -27,12 +25,9 @@ class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
multi={true} multi={true}
value={this.props.value} value={this.props.value}
onChange={this.props.onChange} onChange={this.props.onChange}
default={t("Comment...")} default="Comment..."
data-wd-key="layer-comment.input" data-wd-key="layer-comment.input"
/> />
</Block> </Block>
} }
} }
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md' import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
type FieldDocLabelProps = { type FieldDocLabelProps = {
label: JSX.Element | string | undefined label: object | string | undefined
fieldSpec?: { fieldSpec?: {
doc?: string doc?: string
} }
+2 -2
View File
@@ -128,7 +128,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
} }
} }
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) { static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
// Because otherwise when editing values we end up accidentally changing field type. // Because otherwise when editing values we end up accidentally changing field type.
if (state.isEditing) { if (state.isEditing) {
return {}; return {};
@@ -399,7 +399,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
/> />
) )
} }
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}> return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
{specField} {specField}
</div> </div>
} }
+2 -3
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block' import Block from './Block'
import InputString from './InputString' import InputString from './InputString'
@@ -13,8 +13,7 @@ type FieldIdProps = {
export default class FieldId extends React.Component<FieldIdProps> { export default class FieldId extends React.Component<FieldIdProps> {
render() { render() {
return <Block label="ID" fieldSpec={latest.layer.id} return <Block label={"ID"} fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey} data-wd-key={this.props.wdKey}
error={this.props.error} error={this.props.error}
> >
+5 -10
View File
@@ -1,20 +1,18 @@
import React from 'react' import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block' import Block from './Block'
import InputNumber from './InputNumber' import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMaxZoomInternalProps = { type FieldMaxZoomProps = {
value?: number value?: number
onChange(value: number | undefined): unknown onChange(value: number | undefined): unknown
error?: {message: string} error?: {message: string}
} & WithTranslation; };
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> { export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
render() { render() {
const t = this.props.t; return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
error={this.props.error} error={this.props.error}
data-wd-key="max-zoom" data-wd-key="max-zoom"
> >
@@ -30,6 +28,3 @@ class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
</Block> </Block>
} }
} }
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
export default FieldMaxZoom;
+5 -10
View File
@@ -1,20 +1,18 @@
import React from 'react' import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block' import Block from './Block'
import InputNumber from './InputNumber' import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMinZoomInternalProps = { type FieldMinZoomProps = {
value?: number value?: number
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
error?: {message: string} error?: {message: string}
} & WithTranslation; };
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> { export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
render() { render() {
const t = this.props.t; return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
error={this.props.error} error={this.props.error}
data-wd-key="min-zoom" data-wd-key="min-zoom"
> >
@@ -30,6 +28,3 @@ class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
</Block> </Block>
} }
} }
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
export default FieldMinZoom;
+5 -10
View File
@@ -1,28 +1,26 @@
import React from 'react' import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block' import Block from './Block'
import InputAutocomplete from './InputAutocomplete' import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceInternalProps = { type FieldSourceProps = {
value?: string value?: string
wdKey?: string wdKey?: string
onChange?(value: string| undefined): unknown onChange?(value: string| undefined): unknown
sourceIds?: unknown[] sourceIds?: unknown[]
error?: {message: string} error?: {message: string}
} & WithTranslation; };
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> { export default class FieldSource extends React.Component<FieldSourceProps> {
static defaultProps = { static defaultProps = {
onChange: () => {}, onChange: () => {},
sourceIds: [], sourceIds: [],
} }
render() { render() {
const t = this.props.t;
return <Block return <Block
label={t("Source")} label={"Source"}
fieldSpec={latest.layer.source} fieldSpec={latest.layer.source}
error={this.props.error} error={this.props.error}
data-wd-key={this.props.wdKey} data-wd-key={this.props.wdKey}
@@ -35,6 +33,3 @@ class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
</Block> </Block>
} }
} }
const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource;
+4 -9
View File
@@ -3,17 +3,16 @@ import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec' import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block' import Block from './Block'
import InputAutocomplete from './InputAutocomplete' import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceLayerInternalProps = { type FieldSourceLayerProps = {
value?: string value?: string
onChange?(...args: unknown[]): unknown onChange?(...args: unknown[]): unknown
sourceLayerIds?: unknown[] sourceLayerIds?: unknown[]
isFixed?: boolean isFixed?: boolean
error?: {message: string} error?: {message: string}
} & WithTranslation; };
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> { export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
static defaultProps = { static defaultProps = {
onChange: () => {}, onChange: () => {},
sourceLayerIds: [], sourceLayerIds: [],
@@ -21,9 +20,8 @@ class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalP
} }
render() { render() {
const t = this.props.t;
return <Block return <Block
label={t("Source Layer")} label={"Source Layer"}
fieldSpec={latest.layer['source-layer']} fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer" data-wd-key="layer-source-layer"
error={this.props.error} error={this.props.error}
@@ -37,6 +35,3 @@ class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalP
</Block> </Block>
} }
} }
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
export default FieldSourceLayer;
+5 -10
View File
@@ -1,27 +1,25 @@
import React from 'react' import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block' import Block from './Block'
import InputSelect from './InputSelect' import InputSelect from './InputSelect'
import InputString from './InputString' import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldTypeInternalProps = { type FieldTypeProps = {
value: string value: string
wdKey?: string wdKey?: string
onChange(value: string): unknown onChange(value: string): unknown
error?: {message: string} error?: {message: string}
disabled?: boolean disabled?: boolean
} & WithTranslation; };
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> { export default class FieldType extends React.Component<FieldTypeProps> {
static defaultProps = { static defaultProps = {
disabled: false, disabled: false,
} }
render() { render() {
const t = this.props.t; return <Block label={"Type"} fieldSpec={latest.layer.type}
return <Block label={t("Type")} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey} data-wd-key={this.props.wdKey}
error={this.props.error} error={this.props.error}
> >
@@ -52,6 +50,3 @@ class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
</Block> </Block>
} }
} }
const FieldType = withTranslation()(FieldTypeInternal);
export default FieldType;
+2 -2
View File
@@ -1,9 +1,9 @@
import React, { PropsWithChildren, ReactElement } from 'react' import React, { ReactElement } from 'react'
import FieldDocLabel from './FieldDocLabel' import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc' import Doc from './Doc'
import generateUniqueId from '../libs/document-uid'; import generateUniqueId from '../libs/document-uid';
type FieldsetProps = PropsWithChildren & { type FieldsetProps = {
label?: string, label?: string,
fieldSpec?: { doc?: string }, fieldSpec?: { doc?: string },
action?: ReactElement, action?: ReactElement,
+19 -28
View File
@@ -13,10 +13,9 @@ import FilterEditorBlock from './FilterEditorBlock'
import InputButton from './InputButton' import InputButton from './InputButton'
import Doc from './Doc' import Doc from './Doc'
import ExpressionProperty from './_ExpressionProperty'; import ExpressionProperty from './_ExpressionProperty';
import { WithTranslation, withTranslation } from 'react-i18next';
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification { function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
const filter = props.filter || ['all']; const filter = props.filter || ['all'];
if (!Array.isArray(filter)) { if (!Array.isArray(filter)) {
@@ -48,7 +47,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
"sources": { "sources": {
"tmp": { "tmp": {
"type": "geojson", "type": "geojson",
"data": '' "data": {}
} }
}, },
"sprite": "", "sprite": "",
@@ -90,13 +89,13 @@ function hasNestedCombiningFilter(filter: LegacyFilterSpecification | Expression
return false return false
} }
type FilterEditorInternalProps = { type FilterEditorProps = {
/** Properties of the vector layer and the available fields */ /** Properties of the vector layer and the available fields */
properties?: {[key:string]: any} properties?: {[key:string]: any}
filter?: any[] filter?: any[]
errors?: {[key:string]: any} errors?: {[key:string]: any}
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
} & WithTranslation; };
type FilterEditorState = { type FilterEditorState = {
showDoc: boolean showDoc: boolean
@@ -104,12 +103,12 @@ type FilterEditorState = {
valueIsSimpleFilter?: boolean valueIsSimpleFilter?: boolean
}; };
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> { export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
static defaultProps = { static defaultProps = {
filter: ["all"], filter: ["all"],
} }
constructor (props: FilterEditorInternalProps) { constructor (props: FilterEditorProps) {
super(props); super(props);
this.state = { this.state = {
showDoc: false, showDoc: false,
@@ -156,17 +155,17 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
}) })
} }
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) { static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props)); const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
// Upgrade but never downgrade // Upgrade but never downgrade
if (!displaySimpleFilter && state.displaySimpleFilter === true) { if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
return { return {
displaySimpleFilter: false, displaySimpleFilter: false,
valueIsSimpleFilter: false, valueIsSimpleFilter: false,
}; };
} }
else if (displaySimpleFilter && state.displaySimpleFilter === false) { else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
return { return {
valueIsSimpleFilter: true, valueIsSimpleFilter: true,
} }
@@ -179,7 +178,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
} }
render() { render() {
const {errors, t} = this.props; const {errors} = this.props;
const {displaySimpleFilter} = this.state; const {displaySimpleFilter} = this.state;
const fieldSpec={ const fieldSpec={
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter." doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
@@ -191,16 +190,16 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
if (isNestedCombiningFilter) { if (isNestedCombiningFilter) {
return <div className="maputnik-filter-editor-unsupported"> return <div className="maputnik-filter-editor-unsupported">
<p> <p>
{t("Nested filters are not supported.")} Nested filters are not supported.
</p> </p>
<InputButton <InputButton
onClick={this.makeExpression} onClick={this.makeExpression}
title={t("Convert to expression")} title="Convert to expression"
> >
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24"> <svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} /> <path fill="currentColor" d={mdiFunctionVariant} />
</svg> </svg>
{t("Upgrade to expression")} Upgrade to expression
</InputButton> </InputButton>
</div> </div>
} }
@@ -213,7 +212,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
<div> <div>
<InputButton <InputButton
onClick={this.makeExpression} onClick={this.makeExpression}
title={t("Convert to expression")} title="Convert to expression"
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
@@ -248,17 +247,13 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
<Block <Block
key="top" key="top"
fieldSpec={fieldSpec} fieldSpec={fieldSpec}
label={t("Filter")} label={"Filter"}
action={actions} action={actions}
> >
<InputSelect <InputSelect
value={combiningOp} value={combiningOp}
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)} onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
options={[ options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
["all", t("every filter matches")],
["none", t("no filter matches")],
["any", t("any filter matches")]
]}
/> />
</Block> </Block>
{editorBlocks} {editorBlocks}
@@ -273,7 +268,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} /> <path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> {t("Add filter")} </svg> Add filter
</InputButton> </InputButton>
</div> </div>
<div <div
@@ -304,13 +299,12 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
/> />
{this.state.valueIsSimpleFilter && {this.state.valueIsSimpleFilter &&
<div className="maputnik-expr-infobox"> <div className="maputnik-expr-infobox">
{t("You've entered an old style filter.")} You&apos;ve entered a old style filter,{' '}
{' '}
<button <button
onClick={this.makeFilter} onClick={this.makeFilter}
className="maputnik-expr-infobox__button" className="maputnik-expr-infobox__button"
> >
{t("Switch to filter editor.")} switch to filter editor
</button> </button>
</div> </div>
} }
@@ -319,6 +313,3 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
} }
} }
} }
const FilterEditor = withTranslation()(FilterEditorInternal);
export default FilterEditor;
+5 -9
View File
@@ -1,21 +1,19 @@
import React, { PropsWithChildren } from 'react' import React from 'react'
import InputButton from './InputButton' import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md' import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
type FilterEditorBlockInternalProps = PropsWithChildren & { type FilterEditorBlockProps = {
onDelete(...args: unknown[]): unknown onDelete(...args: unknown[]): unknown
} & WithTranslation; };
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> { export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
render() { render() {
const t = this.props.t;
return <div className="maputnik-filter-editor-block"> return <div className="maputnik-filter-editor-block">
<div className="maputnik-filter-editor-block-action"> <div className="maputnik-filter-editor-block-action">
<InputButton <InputButton
className="maputnik-delete-filter" className="maputnik-delete-filter"
onClick={this.props.onDelete} onClick={this.props.onDelete}
title={t("Delete filter block")} title="Delete filter block"
> >
<MdDelete /> <MdDelete />
</InputButton> </InputButton>
@@ -27,5 +25,3 @@ class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInterna
} }
} }
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
export default FilterEditorBlock;
+1 -1
View File
@@ -32,7 +32,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
}; };
} }
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) { static getDerivedStateFromProps(props: FieldArrayProps, state: FieldArrayState) {
const value: any[] = []; const value: any[] = [];
const initialPropsValue = state.initialPropsValue.slice(0); const initialPropsValue = state.initialPropsValue.slice(0);
+6 -17
View File
@@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import capitalize from 'lodash.capitalize' import capitalize from 'lodash.capitalize'
import {MdDelete} from 'react-icons/md' import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
import InputString from './InputString' import InputString from './InputString'
import InputNumber from './InputNumber' import InputNumber from './InputNumber'
@@ -22,11 +21,10 @@ export type FieldDynamicArrayProps = {
} }
'aria-label'?: string 'aria-label'?: string
label: string label: string
} };
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation;
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> { export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
changeValue(idx: number, newValue: string | number | undefined) { changeValue(idx: number, newValue: string | number | undefined) {
const values = this.values.slice(0) const values = this.values.slice(0)
values[idx] = newValue values[idx] = newValue
@@ -64,13 +62,8 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
} }
render() { 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 inputs = this.values.map((v, i) => {
const deleteValueBtn= <DeleteValueInputButton const deleteValueBtn= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
onClick={this.deleteValue.bind(this, i)}
{...i18nProps}
/>;
let input; let input;
if(this.props.type === 'url') { if(this.props.type === 'url') {
input = <InputUrl input = <InputUrl
@@ -124,27 +117,23 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
className="maputnik-array-add-value" className="maputnik-array-add-value"
onClick={this.addValue} onClick={this.addValue}
> >
{t("Add value")} Add value
</InputButton> </InputButton>
</div> </div>
); );
} }
} }
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
export default FieldDynamicArray;
type DeleteValueInputButtonProps = { type DeleteValueInputButtonProps = {
onClick?(...args: unknown[]): unknown onClick?(...args: unknown[]): unknown
} & WithTranslation; };
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> { class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
render() { render() {
const t = this.props.t;
return <InputButton return <InputButton
className="maputnik-delete-stop" className="maputnik-delete-stop"
onClick={this.props.onClick} onClick={this.props.onClick}
title={t("Remove array item")} title="Remove array item"
> >
<FieldDocLabel <FieldDocLabel
label={<MdDelete />} label={<MdDelete />}
+3 -11
View File
@@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import classnames from 'classnames'; import classnames from 'classnames';
import CodeMirror, { ModeSpec } from 'codemirror'; import CodeMirror, { ModeSpec } from 'codemirror';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import 'codemirror/mode/javascript/javascript' import 'codemirror/mode/javascript/javascript'
import 'codemirror/addon/lint/lint' import 'codemirror/addon/lint/lint'
@@ -28,7 +27,6 @@ export type InputJsonProps = {
mode?: ModeSpec<any> mode?: ModeSpec<any>
lint?: boolean | object lint?: boolean | object
}; };
type InputJsonInternalProps = InputJsonProps & WithTranslation;
type InputJsonState = { type InputJsonState = {
isEditing: boolean isEditing: boolean
@@ -36,7 +34,7 @@ type InputJsonState = {
prevValue: string prevValue: string
}; };
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> { export default class InputJson extends React.Component<InputJsonProps, InputJsonState> {
static defaultProps = { static defaultProps = {
lineNumbers: true, lineNumbers: true,
lineWrapping: false, lineWrapping: false,
@@ -54,7 +52,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
_el: HTMLDivElement | null = null; _el: HTMLDivElement | null = null;
_cancelNextChange: boolean = false; _cancelNextChange: boolean = false;
constructor(props: InputJsonInternalProps) { constructor(props: InputJsonProps) {
super(props); super(props);
this._keyEvent = "keyboard"; this._keyEvent = "keyboard";
this.state = { this.state = {
@@ -158,7 +156,6 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
} }
render() { render() {
const t = this.props.t;
const {showMessage} = this.state; const {showMessage} = this.state;
const style = {} as {maxHeight?: number}; const style = {} as {maxHeight?: number};
if (this.props.maxHeight) { if (this.props.maxHeight) {
@@ -167,9 +164,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true"> return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}> <div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
<Trans t={t}> Press <kbd>ESC</kbd> to lose focus
Press <kbd>ESC</kbd> to lose focus
</Trans>
</div> </div>
<div <div
className={classnames("codemirror-container", this.props.className)} className={classnames("codemirror-container", this.props.className)}
@@ -179,6 +174,3 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
</div> </div>
} }
} }
const InputJson = withTranslation()(InputJsonInternal);
export default InputJson;
+4 -7
View File
@@ -19,10 +19,7 @@ type InputNumberState = {
editing: boolean editing: boolean
editingRange?: boolean editingRange?: boolean
value?: number 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> { export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
@@ -41,7 +38,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
} }
} }
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) { static getDerivedStateFromProps(props: InputNumberProps, state: InputNumberState) {
if (!state.editing && props.value !== state.value) { if (!state.editing && props.value !== state.value) {
return { return {
value: props.value, value: props.value,
@@ -69,7 +66,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
} }
this.setState({ this.setState({
dirtyValue: newValue === "" ? undefined : newValue, dirtyValue: newValue === "" ? undefined : value,
}) })
} }
@@ -128,7 +125,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
// for example we might go from 13 to 13.23, however because we know // 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 // that came from a keyboard event we always want to increase by a
// single step value. // single step value.
if (value < +this.state.dirtyValue!) { if (value < this.state.dirtyValue!) {
value = this.state.value! - step; value = this.state.value! - step;
} }
else { else {
-1
View File
@@ -49,7 +49,6 @@ export default class SpecField extends React.Component<SpecFieldProps> {
value: this.props.value, value: this.props.value,
default: this.props.fieldSpec?.default, default: this.props.fieldSpec?.default,
name: this.props.fieldName, 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), onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
'aria-label': this.props['aria-label'], 'aria-label': this.props['aria-label'],
} }
+1 -1
View File
@@ -33,7 +33,7 @@ export default class InputString extends React.Component<InputStringProps, Input
} }
} }
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) { static getDerivedStateFromProps(props: InputStringProps, state: InputStringState) {
if (!state.editing) { if (!state.editing) {
return { return {
value: props.value value: props.value
+17 -28
View File
@@ -1,10 +1,9 @@
import React from 'react' import React from 'react'
import InputString from './InputString' import InputString from './InputString'
import SmallError from './SmallError' import SmallError from './SmallError'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
function validate(url: string, t: TFunction): JSX.Element | undefined {
function validate(url: string) {
if (url === "") { if (url === "") {
return; return;
} }
@@ -23,19 +22,15 @@ function validate(url: string, t: TFunction): JSX.Element | undefined {
const isSsl = window.location.protocol === "https:"; const isSsl = window.location.protocol === "https:";
if (!protocol) { if (!protocol) {
if (isSsl) { error = (
error = ( <SmallError>
<SmallError> Must provide protocol {
<Trans t={t}>Must provide protocol: <code>https://</code></Trans> isSsl
</SmallError> ? <code>https://</code>
); : <><code>http://</code> or <code>https://</code></>
} else { }
error = ( </SmallError>
<SmallError> );
<Trans t={t}>Must provide protocol: <code>http://</code> or <code>https://</code></Trans>
</SmallError>
);
}
} }
else if ( else if (
protocol && protocol &&
@@ -44,9 +39,7 @@ function validate(url: string, t: TFunction): JSX.Element | undefined {
) { ) {
error = ( error = (
<SmallError> <SmallError>
<Trans t={t}> CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain
CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain
</Trans>
</SmallError> </SmallError>
); );
} }
@@ -68,34 +61,32 @@ export type FieldUrlProps = {
className?: string className?: string
}; };
type FieldUrlInternalProps = FieldUrlProps & WithTranslation;
type FieldUrlState = { type FieldUrlState = {
error?: React.ReactNode error?: React.ReactNode
} }
class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlState> { export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlState> {
static defaultProps = { static defaultProps = {
onInput: () => {}, onInput: () => {},
} }
constructor (props: FieldUrlInternalProps) { constructor (props: FieldUrlProps) {
super(props); super(props);
this.state = { this.state = {
error: validate(props.value, props.t), error: validate(props.value)
}; };
} }
onInput = (url: string) => { onInput = (url: string) => {
this.setState({ this.setState({
error: validate(url, this.props.t), error: validate(url)
}); });
if (this.props.onInput) this.props.onInput(url); if (this.props.onInput) this.props.onInput(url);
} }
onChange = (url: string) => { onChange = (url: string) => {
this.setState({ this.setState({
error: validate(url, this.props.t), error: validate(url)
}); });
this.props.onChange(url); this.props.onChange(url);
} }
@@ -115,5 +106,3 @@ class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlSt
} }
} }
const FieldUrl = withTranslation()(FieldUrlInternal);
export default FieldUrl;
+25 -48
View File
@@ -19,45 +19,31 @@ import FieldSourceLayer from './FieldSourceLayer'
import { changeType, changeProperty } from '../libs/layer' import { changeType, changeProperty } from '../libs/layer'
import layout from '../config/layout.json' import layout from '../config/layout.json'
import {formatLayerId} from '../libs/format'; import {formatLayerId} from '../libs/format';
import { WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
function getLayoutForType(type: LayerSpecification["type"], t: TFunction) { function getLayoutForType(type: LayerSpecification["type"]) {
return layout[type] ? { return layout[type] ? layout[type] : layout.invalid;
...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"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] { function layoutGroups(layerType: LayerSpecification["type"]): {title: string, type: string, fields?: string[]}[] {
const layerGroup = { const layerGroup = {
id: 'layer', title: 'Layer',
title: t('Layer'),
type: 'layer' type: 'layer'
} }
const filterGroup = { const filterGroup = {
id: 'filter', title: 'Filter',
title: t('Filter'),
type: 'filter' type: 'filter'
} }
const editorGroup = { const editorGroup = {
id: 'jsoneditor', title: 'JSON Editor',
title: t('JSON Editor'),
type: 'jsoneditor' type: 'jsoneditor'
} }
return [layerGroup, filterGroup] return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType, t).groups) .concat(getLayoutForType(layerType).groups)
.concat([editorGroup]) .concat([editorGroup])
} }
type LayerEditorInternalProps = { type LayerEditorProps = {
layer: LayerSpecification layer: LayerSpecification
sources: {[key: string]: SourceSpecification} sources: {[key: string]: SourceSpecification}
vectorLayers: {[key: string]: any} vectorLayers: {[key: string]: any}
@@ -72,14 +58,14 @@ type LayerEditorInternalProps = {
isLastLayer?: boolean isLastLayer?: boolean
layerIndex: number layerIndex: number
errors?: any[] errors?: any[]
} & WithTranslation; };
type LayerEditorState = { type LayerEditorState = {
editorGroups: {[keys:string]: boolean} editorGroups: {[keys:string]: boolean}
}; };
/** Layer editor supporting multiple types of layers. */ /** Layer editor supporting multiple types of layers. */
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> { export default class LayerEditor extends React.Component<LayerEditorProps, LayerEditorState> {
static defaultProps = { static defaultProps = {
onLayerChanged: () => {}, onLayerChanged: () => {},
onLayerIdChange: () => {}, onLayerIdChange: () => {},
@@ -90,22 +76,22 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
reactIconBase: PropTypes.object reactIconBase: PropTypes.object
} }
constructor(props: LayerEditorInternalProps) { constructor(props: LayerEditorProps) {
super(props) super(props)
//TODO: Clean this up and refactor into function //TODO: Clean this up and refactor into function
const editorGroups: {[keys:string]: boolean} = {} const editorGroups: {[keys:string]: boolean} = {}
layoutGroups(this.props.layer.type, props.t).forEach(group => { layoutGroups(this.props.layer.type).forEach(group => {
editorGroups[group.title] = true editorGroups[group.title] = true
}) })
this.state = { editorGroups } this.state = { editorGroups }
} }
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) { static getDerivedStateFromProps(props: LayerEditorProps, state: LayerEditorState) {
const additionalGroups = { ...state.editorGroups } const additionalGroups = { ...state.editorGroups }
getLayoutForType(props.layer.type, props.t).groups.forEach(group => { getLayoutForType(props.layer.type).groups.forEach(group => {
if(!(group.title in additionalGroups)) { if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true additionalGroups[group.title] = true
} }
@@ -256,19 +242,17 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
} }
render() { render() {
const t = this.props.t;
const groupIds: string[] = []; const groupIds: string[] = [];
const layerType = this.props.layer.type const layerType = this.props.layer.type
const groups = layoutGroups(layerType, t).filter(group => { const groups = layoutGroups(layerType).filter(group => {
return !(layerType === 'background' && group.type === 'source') return !(layerType === 'background' && group.type === 'source')
}).map(group => { }).map(group => {
const groupId = group.id; const groupId = group.title.replace(/ /g, "_");
groupIds.push(groupId); groupIds.push(groupId);
return <LayerEditorGroup return <LayerEditorGroup
data-wd-key={group.title} data-wd-key={group.title}
id={groupId} id={groupId}
key={groupId} key={group.title}
title={group.title} title={group.title}
isActive={this.state.editorGroups[group.title]} isActive={this.state.editorGroups[group.title]}
onActiveToggle={this.onGroupToggle.bind(this, group.title)} onActiveToggle={this.onGroupToggle.bind(this, group.title)}
@@ -281,25 +265,25 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = { const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
delete: { delete: {
text: t("Delete"), text: "Delete",
handler: () => this.props.onLayerDestroy(this.props.layerIndex) handler: () => this.props.onLayerDestroy(this.props.layerIndex)
}, },
duplicate: { duplicate: {
text: t("Duplicate"), text: "Duplicate",
handler: () => this.props.onLayerCopy(this.props.layerIndex) handler: () => this.props.onLayerCopy(this.props.layerIndex)
}, },
hide: { hide: {
text: (layout.visibility === "none") ? t("Show") : t("Hide"), text: (layout.visibility === "none") ? "Show" : "Hide",
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex) handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
}, },
moveLayerUp: { moveLayerUp: {
text: t("Move layer up"), text: "Move layer up",
// Not actually used... // Not actually used...
disabled: this.props.isFirstLayer, disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1) handler: () => this.moveLayer(-1)
}, },
moveLayerDown: { moveLayerDown: {
text: t("Move layer down"), text: "Move layer down",
// Not actually used... // Not actually used...
disabled: this.props.isLastLayer, disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1) handler: () => this.moveLayer(+1)
@@ -313,12 +297,12 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
return <section className="maputnik-layer-editor" return <section className="maputnik-layer-editor"
role="main" role="main"
aria-label={t("Layer editor")} aria-label="Layer editor"
> >
<header> <header>
<div className="layer-header"> <div className="layer-header">
<h2 className="layer-header__title"> <h2 className="layer-header__title">
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })} Layer: {formatLayerId(this.props.layer.id)}
</h2> </h2>
<div className="layer-header__info"> <div className="layer-header__info">
<Wrapper <Wrapper
@@ -326,11 +310,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
onSelection={handleSelection} onSelection={handleSelection}
closeOnSelection={false} closeOnSelection={false}
> >
<Button <Button id="skip-target-layer-editor" data-wd-key="skip-target-layer-editor" className='more-menu__button' title="Layer options">
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" /> <MdMoreVert className="more-menu__button__svg" />
</Button> </Button>
<Menu> <Menu>
@@ -360,6 +340,3 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
</section> </section>
} }
} }
const LayerEditor = withTranslation()(LayerEditorInternal);
export default LayerEditor;
+7 -22
View File
@@ -10,7 +10,6 @@ import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
import type {LayerSpecification} from 'maplibre-gl'; import type {LayerSpecification} from 'maplibre-gl';
import generateUniqueId from '../libs/document-uid'; import generateUniqueId from '../libs/document-uid';
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer'; import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
import { WithTranslation, withTranslation } from 'react-i18next';
type LayerListContainerProps = { type LayerListContainerProps = {
layers: LayerSpecification[] layers: LayerSpecification[]
@@ -23,7 +22,6 @@ type LayerListContainerProps = {
sources: object sources: object
errors: any[] errors: any[]
}; };
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
type LayerListContainerState = { type LayerListContainerState = {
collapsedGroups: {[ket: string]: boolean} collapsedGroups: {[ket: string]: boolean}
@@ -33,14 +31,14 @@ type LayerListContainerState = {
}; };
// List of collapsible layer editors // List of collapsible layer editors
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> { class LayerListContainer extends React.Component<LayerListContainerProps, LayerListContainerState> {
static defaultProps = { static defaultProps = {
onLayerSelect: () => {}, onLayerSelect: () => {},
} }
selectedItemRef: React.RefObject<any>; selectedItemRef: React.RefObject<any>;
scrollContainerRef: React.RefObject<HTMLElement>; scrollContainerRef: React.RefObject<HTMLElement>;
constructor(props: LayerListContainerInternalProps) { constructor(props: LayerListContainerProps) {
super(props); super(props);
this.selectedItemRef = React.createRef(); this.selectedItemRef = React.createRef();
this.scrollContainerRef = React.createRef(); this.scrollContainerRef = React.createRef();
@@ -261,12 +259,10 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
}) })
}) })
const t = this.props.t;
return <section return <section
className="maputnik-layer-list" className="maputnik-layer-list"
role="complementary" role="complementary"
aria-label={t("Layers list")} aria-label="Layers list"
ref={this.scrollContainerRef} ref={this.scrollContainerRef}
> >
<ModalAdd <ModalAdd
@@ -278,7 +274,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
onLayersChange={this.props.onLayersChange} onLayersChange={this.props.onLayersChange}
/> />
<header className="maputnik-layer-list-header"> <header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">{t("Layers")}</span> <span className="maputnik-layer-list-header-title">Layers</span>
<span className="maputnik-space" /> <span className="maputnik-space" />
<div className="maputnik-default-property"> <div className="maputnik-default-property">
<div className="maputnik-multibutton"> <div className="maputnik-multibutton">
@@ -287,11 +283,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
data-wd-key="skip-target-layer-list" data-wd-key="skip-target-layer-list"
onClick={this.toggleLayers} onClick={this.toggleLayers}
className="maputnik-button"> className="maputnik-button">
{this.state.areAllGroupsExpanded === true ? {this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
t("Collapse")
:
t("Expand")
}
</button> </button>
</div> </div>
</div> </div>
@@ -301,14 +293,14 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
onClick={this.toggleModal.bind(this, 'add')} onClick={this.toggleModal.bind(this, 'add')}
data-wd-key="layer-list:add-layer" data-wd-key="layer-list:add-layer"
className="maputnik-button maputnik-button-selected"> className="maputnik-button maputnik-button-selected">
{t("Add Layer")} Add Layer
</button> </button>
</div> </div>
</div> </div>
</header> </header>
<div <div
role="navigation" role="navigation"
aria-label={t("Layers list")} aria-label="Layers list"
> >
<ul className="maputnik-layer-list-container"> <ul className="maputnik-layer-list-container">
{listItems} {listItems}
@@ -318,13 +310,6 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
} }
} }
// The next two lines have react-refresh/only-export-components disabled because they are
// internal components that are not intended to be used outside of this file.
// For some reason, the linter is not recognizing these components correctly.
// When these components are migrated to functional components, the HOCs will no longer be needed
// and the comments can be removed.
// eslint-disable-next-line react-refresh/only-export-components
const LayerListContainer = withTranslation()(LayerListContainerInternal);
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />) const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
+1 -1
View File
@@ -139,6 +139,6 @@ class LayerListItem extends React.Component<LayerListItemProps> {
} }
} }
const LayerListItemSortable = SortableElement<LayerListItemProps>((props: LayerListItemProps) => <LayerListItem {...props} />); const LayerListItemSortable = SortableElement((props: LayerListItemProps) => <LayerListItem {...props} />);
export default LayerListItemSortable; export default LayerListItemSortable;
+72 -93
View File
@@ -1,8 +1,10 @@
import React, {type JSX} from 'react' import React, {type JSX} from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl' import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
import MaplibreInspect from '@maplibre/maplibre-gl-inspect' // @ts-ignore
import colors from '@maplibre/maplibre-gl-inspect/lib/colors' import MapboxInspect from 'mapbox-gl-inspect'
// @ts-ignore
import colors from 'mapbox-gl-inspect/lib/colors'
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup' import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'
import MapMaplibreGlFeaturePropertyPopup, { InspectFeature } from './MapMaplibreGlFeaturePropertyPopup' import MapMaplibreGlFeaturePropertyPopup, { InspectFeature } from './MapMaplibreGlFeaturePropertyPopup'
import Color from 'color' import Color from 'color'
@@ -11,13 +13,13 @@ import { HighlightedLayer, colorHighlightedLayer } from '../libs/highlight'
import 'maplibre-gl/dist/maplibre-gl.css' import 'maplibre-gl/dist/maplibre-gl.css'
import '../maplibregl.css' import '../maplibregl.css'
import '../libs/maplibre-rtl' 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'
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
const IS_SUPPORTED = MapLibreGl.supported();
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container) {
ReactDOM.render(popup, mountNode); ReactDOM.render(popup, mountNode);
return mountNode as HTMLElement; return mountNode;
} }
function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) { function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) {
@@ -35,7 +37,6 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
} }
const sources: {[key:string]: SourceSpecification} = {} const sources: {[key:string]: SourceSpecification} = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => { Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId] const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster' && source.type !== 'raster-dem') { if(source.type !== 'raster' && source.type !== 'raster-dem') {
@@ -51,7 +52,7 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
return inspectStyle return inspectStyle
} }
type MapMaplibreGlInternalProps = { type MapMaplibreGlProps = {
onDataChange?(event: {map: Map | null}): unknown onDataChange?(event: {map: Map | null}): unknown
onLayerSelect(...args: unknown[]): unknown onLayerSelect(...args: unknown[]): unknown
mapStyle: StyleSpecification mapStyle: StyleSpecification
@@ -64,15 +65,15 @@ type MapMaplibreGlInternalProps = {
} }
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
onChange(value: {center: LngLat, zoom: number}): unknown onChange(value: {center: LngLat, zoom: number}): unknown
} & WithTranslation; };
type MapMaplibreGlState = { type MapMaplibreGlState = {
map: Map | null map: Map | null
inspect: MaplibreInspect | null inspect: MapboxInspect | null
zoom?: number zoom?: number
}; };
class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> { export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, MapMaplibreGlState> {
static defaultProps = { static defaultProps = {
onMapLoaded: () => {}, onMapLoaded: () => {},
onDataChange: () => {}, onDataChange: () => {},
@@ -82,7 +83,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
} }
container: HTMLDivElement | null = null container: HTMLDivElement | null = null
constructor(props: MapMaplibreGlInternalProps) { constructor(props: MapMaplibreGlProps) {
super(props) super(props)
this.state = { this.state = {
map: null, map: null,
@@ -90,8 +91,20 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
} }
} }
updateMapFromProps(props: MapMaplibreGlProps) {
if(!IS_SUPPORTED) return;
shouldComponentUpdate(nextProps: MapMaplibreGlInternalProps, nextState: MapMaplibreGlState) { 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) {
let should = false; let should = false;
try { try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState); should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
@@ -102,42 +115,46 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
} }
componentDidUpdate() { componentDidUpdate() {
if(!IS_SUPPORTED) return;
const map = this.state.map; const map = this.state.map;
const styleWithTokens = this.props.replaceAccessTokens(this.props.mapStyle); this.updateMapFromProps(this.props);
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
// eslint-disable-next-line
this.state.inspect._popupBlocked = false;
this.state.inspect.toggleInspector()
}
if (map) { if (map) {
// Maplibre GL now does diffing natively so we don't need to calculate if (this.props.inspectModeEnabled) {
// the necessary operations ourselves! // HACK: We need to work out why we need to do this and what's causing
// We also need to update the style for inspect to work properly // this error. I'm assuming an issue with maplibre-gl update and
map.setStyle(styleWithTokens, {diff: true}); // mapbox-gl-inspect.
try {
this.state.inspect.render();
} catch(err) {
console.error("FIXME: Caught error", err);
}
}
map.showTileBoundaries = this.props.options?.showTileBoundaries!; map.showTileBoundaries = this.props.options?.showTileBoundaries!;
map.showCollisionBoxes = this.props.options?.showCollisionBoxes!; map.showCollisionBoxes = this.props.options?.showCollisionBoxes!;
map.showOverdrawInspector = this.props.options?.showOverdrawInspector!; 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() { componentDidMount() {
if(!IS_SUPPORTED) return;
const mapOpts = { const mapOpts = {
...this.props.options, ...this.props.options,
container: this.container!, container: this.container!,
style: this.props.mapStyle, style: this.props.mapStyle,
hash: true, 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); const map = new MapLibreGl.Map(mapOpts);
@@ -152,9 +169,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
map.showCollisionBoxes = mapOpts.showCollisionBoxes!; map.showCollisionBoxes = mapOpts.showCollisionBoxes!;
map.showOverdrawInspector = mapOpts.showOverdrawInspector!; map.showOverdrawInspector = mapOpts.showOverdrawInspector!;
this.initGeocoder(map); const zoomControl = new ZoomControl;
const zoomControl = new ZoomControl(this.props.t("Zoom:"));
map.addControl(zoomControl, 'top-right'); map.addControl(zoomControl, 'top-right');
const nav = new MapLibreGl.NavigationControl({visualizePitch:true}); const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
@@ -162,7 +177,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
const tmpNode = document.createElement('div'); const tmpNode = document.createElement('div');
const inspect = new MaplibreInspect({ const inspect = new MapboxInspect({
popup: new MapLibreGl.Popup({ popup: new MapLibreGl.Popup({
closeOnClick: false closeOnClick: false
}), }),
@@ -219,61 +234,25 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
this.props.onLayerSelect(index); 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() { render() {
const t = this.props.t; if(IS_SUPPORTED) {
return <div return <div
className="maputnik-map__map" className="maputnik-map__map"
role="region" role="region"
aria-label={t("Map view")} aria-label="Map view"
ref={x => this.container = x} ref={x => this.container = x}
data-wd-key="maplibre:map" data-wd-key="maplibre:map"
></div> ></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 MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
export default MapMaplibreGl;
@@ -1,12 +1,18 @@
import React from 'react' import React from 'react'
import type { GeoJSONFeatureWithSourceLayer } from '@maplibre/maplibre-gl-inspect' import Block from './Block'
import FieldString from './FieldString'
export type InspectFeature = GeoJSONFeatureWithSourceLayer & { export type InspectFeature = {
id: string
properties: {[key:string]: any}
layer: {[key:string]: any}
geometry: GeoJSON.Geometry
sourceLayer: string
inspectModeCounter?: number inspectModeCounter?: number
counter?: number counter?: number
} }
function displayValue(value: string | number | Date | object | undefined) { function displayValue(value: string | number | Date | object) {
if (typeof value === 'undefined' || value === null) return value; if (typeof value === 'undefined' || value === null) return value;
if (value instanceof Date) return value.toLocaleString(); if (value instanceof Date) return value.toLocaleString();
if (typeof value === 'object' || if (typeof value === 'object' ||
@@ -15,25 +21,30 @@ function displayValue(value: string | number | Date | object | undefined) {
return value; return value;
} }
function renderKeyValueTableRow(key: string, value: string | undefined) { function renderProperties(feature: InspectFeature) {
return <tr key={key}> return Object.keys(feature.properties).map(propertyName => {
<td className="maputnik-popup-table-cell">{key}</td> const property = feature.properties[propertyName]
<td className="maputnik-popup-table-cell">{value}</td> return <Block key={propertyName} label={propertyName}>
</tr> <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 renderFeature(feature: InspectFeature, idx: number) { function renderFeature(feature: InspectFeature, idx: number) {
return <React.Fragment key={idx}> return <div key={`${feature.sourceLayer}-${idx}`}>
<tr> <div className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
<td colSpan={2} className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</td> <Block key={"property-type"} label={"$type"}>
</tr> <FieldString value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
{renderKeyValueTableRow("$type", feature.geometry.type)} </Block>
{renderKeyValueTableRow("$id", displayValue(feature.id))} {renderFeatureId(feature)}
{Object.keys(feature.properties).map(propertyName => { {renderProperties(feature)}
const property = feature.properties[propertyName]; </div>
return renderKeyValueTableRow(propertyName, displayValue(property))
})}
</React.Fragment>
} }
function removeDuplicatedFeatures(features: InspectFeature[]) { function removeDuplicatedFeatures(features: InspectFeature[]) {
@@ -67,11 +78,7 @@ class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
render() { render() {
const features = removeDuplicatedFeatures(this.props.features) const features = removeDuplicatedFeatures(this.props.features)
return <div className="maputnik-feature-property-popup"> return <div className="maputnik-feature-property-popup">
<table className="maputnik-popup-table"> {features.map(renderFeature)}
<tbody>
{features.map(renderFeature)}
</tbody>
</table>
</div> </div>
} }
} }
+21 -14
View File
@@ -8,16 +8,15 @@ function groupFeaturesBySourceLayer(features: InspectFeature[]) {
const returnedFeatures: {[key: string]: number} = {} const returnedFeatures: {[key: string]: number} = {}
features.forEach(feature => { features.forEach(feature => {
const sourceKey = feature.layer['source-layer'] as string;
if(Object.prototype.hasOwnProperty.call(returnedFeatures, feature.layer.id)) { if(Object.prototype.hasOwnProperty.call(returnedFeatures, feature.layer.id)) {
returnedFeatures[feature.layer.id]++ returnedFeatures[feature.layer.id]++
const featureObject = sources[sourceKey].find((f: InspectFeature) => f.layer.id === feature.layer.id) const featureObject = sources[feature.layer['source-layer']].find((f: InspectFeature) => f.layer.id === feature.layer.id)
featureObject!.counter = returnedFeatures[feature.layer.id] featureObject!.counter = returnedFeatures[feature.layer.id]
} else { } else {
sources[sourceKey] = sources[sourceKey] || [] sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
sources[sourceKey].push(feature) sources[feature.layer['source-layer']].push(feature)
returnedFeatures[feature.layer.id] = 1 returnedFeatures[feature.layer.id] = 1
} }
@@ -41,21 +40,29 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
try { try {
const paintProps = feature.layer.paint; const paintProps = feature.layer.paint;
let propName;
if("text-color" in paintProps && paintProps["text-color"]) { if(Object.prototype.hasOwnProperty.call(paintProps, "text-color") && paintProps["text-color"]) {
return String(paintProps["text-color"]); propName = "text-color";
} }
if ("fill-color" in paintProps && paintProps["fill-color"]) { else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-color") && paintProps["fill-color"]) {
return String(paintProps["fill-color"]); propName = "fill-color";
} }
if ("line-color" in paintProps && paintProps["line-color"]) { else if (Object.prototype.hasOwnProperty.call(paintProps, "line-color") && paintProps["line-color"]) {
return String(paintProps["line-color"]); propName = "line-color";
} }
if ("fill-extrusion-color" in paintProps && paintProps["fill-extrusion-color"]) { else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
return String(paintProps["fill-extrusion-color"]); propName = "fill-extrusion-color";
}
if(propName) {
const color = feature.layer.paint[propName];
return String(color);
}
else {
// Default color
return "black";
} }
// Default color
return "black";
} }
// This is quite complex, just incase there's an edgecase we're missing // This is quite complex, just incase there's an edgecase we're missing
// always return black if we get an unexpected error. // always return black if we get an unexpected error.
+11 -16
View File
@@ -1,11 +1,9 @@
import React from 'react' import React from 'react'
import {throttle} from 'lodash'; import {throttle} from 'lodash';
import { WithTranslation, withTranslation } from 'react-i18next';
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'; import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup';
import 'ol/ol.css' import 'ol/ol.css'
//@ts-ignore
import {apply} from 'ol-mapbox-style'; import {apply} from 'ol-mapbox-style';
import {Map, View, Overlay} from 'ol'; import {Map, View, Overlay} from 'ol';
@@ -24,7 +22,7 @@ function renderCoords (coords: string[]) {
} }
} }
type MapOpenLayersInternalProps = { type MapOpenLayersProps = {
onDataChange?(...args: unknown[]): unknown onDataChange?(...args: unknown[]): unknown
mapStyle: object mapStyle: object
accessToken?: string accessToken?: string
@@ -33,7 +31,7 @@ type MapOpenLayersInternalProps = {
debugToolbox: boolean debugToolbox: boolean
replaceAccessTokens(...args: unknown[]): unknown replaceAccessTokens(...args: unknown[]): unknown
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; };
type MapOpenLayersState = { type MapOpenLayersState = {
zoom: string zoom: string
@@ -43,7 +41,7 @@ type MapOpenLayersState = {
selectedFeatures?: any[] selectedFeatures?: any[]
}; };
class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps, MapOpenLayersState> { export default class MapOpenLayers extends React.Component<MapOpenLayersProps, MapOpenLayersState> {
static defaultProps = { static defaultProps = {
onMapLoaded: () => {}, onMapLoaded: () => {},
onDataChange: () => {}, onDataChange: () => {},
@@ -55,7 +53,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
overlay: Overlay | undefined; overlay: Overlay | undefined;
popupContainer: HTMLElement | null = null; popupContainer: HTMLElement | null = null;
constructor(props: MapOpenLayersInternalProps) { constructor(props: MapOpenLayersProps) {
super(props); super(props);
this.state = { this.state = {
zoom: "0", zoom: "0",
@@ -74,7 +72,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
apply(this.map, newMapStyle); apply(this.map, newMapStyle);
} }
componentDidUpdate(prevProps: MapOpenLayersInternalProps) { componentDidUpdate(prevProps: MapOpenLayersProps) {
if (this.props.mapStyle !== prevProps.mapStyle) { if (this.props.mapStyle !== prevProps.mapStyle) {
this.updateStyle( this.updateStyle(
this.props.replaceAccessTokens(this.props.mapStyle) this.props.replaceAccessTokens(this.props.mapStyle)
@@ -152,7 +150,6 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
} }
render() { render() {
const t = this.props.t;
return <div className="maputnik-ol-container"> return <div className="maputnik-ol-container">
<div <div
ref={x => this.popupContainer = x} ref={x => this.popupContainer = x}
@@ -162,7 +159,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
<button <button
className="maplibregl-popup-close-button" className="maplibregl-popup-close-button"
onClick={this.closeOverlay} onClick={this.closeOverlay}
aria-label={t("Close popup")} aria-label="Close popup"
> >
× ×
</button> </button>
@@ -172,20 +169,20 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
/> />
</div> </div>
<div className="maputnik-ol-zoom"> <div className="maputnik-ol-zoom">
{t("Zoom:")} {this.state.zoom} Zoom: {this.state.zoom}
</div> </div>
{this.props.debugToolbox && {this.props.debugToolbox &&
<div className="maputnik-ol-debug"> <div className="maputnik-ol-debug">
<div> <div>
<label>{t("cursor:")} </label> <label>cursor: </label>
<span>{renderCoords(this.state.cursor)}</span> <span>{renderCoords(this.state.cursor)}</span>
</div> </div>
<div> <div>
<label>{t("center:")} </label> <label>center: </label>
<span>{renderCoords(this.state.center)}</span> <span>{renderCoords(this.state.center)}</span>
</div> </div>
<div> <div>
<label>{t("rotation:")} </label> <label>rotation: </label>
<span>{this.state.rotation}</span> <span>{this.state.rotation}</span>
</div> </div>
</div> </div>
@@ -194,7 +191,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
className="maputnik-ol" className="maputnik-ol"
ref={x => this.container = x} ref={x => this.container = x}
role="region" role="region"
aria-label={t("Map view")} aria-label="Map view"
style={{ style={{
...this.props.style, ...this.props.style,
}}> }}>
@@ -203,5 +200,3 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
} }
} }
const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
export default MapOpenLayers;
+7 -10
View File
@@ -1,10 +1,10 @@
import React, { PropsWithChildren } from 'react' import React from 'react'
import {MdClose} from 'react-icons/md' import {MdClose} from 'react-icons/md'
import AriaModal from 'react-aria-modal' import AriaModal from 'react-aria-modal'
import classnames from 'classnames'; import classnames from 'classnames';
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalInternalProps = PropsWithChildren & {
type ModalProps = {
"data-wd-key"?: string "data-wd-key"?: string
isOpen: boolean isOpen: boolean
title: string title: string
@@ -12,15 +12,15 @@ type ModalInternalProps = PropsWithChildren & {
underlayClickExits?: boolean underlayClickExits?: boolean
underlayProps?: any underlayProps?: any
className?: string className?: string
} & WithTranslation; };
class ModalInternal extends React.Component<ModalInternalProps> { export default class Modal extends React.Component<ModalProps> {
static defaultProps = { static defaultProps = {
underlayClickExits: true underlayClickExits: true
} }
// See <https://github.com/maplibre/maputnik/issues/416> // See <https://github.com/maputnik/editor/issues/416>
onClose = () => { onClose = () => {
if (document.activeElement) { if (document.activeElement) {
(document.activeElement as HTMLElement).blur(); (document.activeElement as HTMLElement).blur();
@@ -32,7 +32,6 @@ class ModalInternal extends React.Component<ModalInternalProps> {
} }
render() { render() {
const t = this.props.t;
if(this.props.isOpen) { if(this.props.isOpen) {
return <AriaModal return <AriaModal
titleText={this.props.title} titleText={this.props.title}
@@ -50,7 +49,7 @@ class ModalInternal extends React.Component<ModalInternalProps> {
<h1 className="maputnik-modal-header-title">{this.props.title}</h1> <h1 className="maputnik-modal-header-title">{this.props.title}</h1>
<span className="maputnik-modal-header-space"></span> <span className="maputnik-modal-header-space"></span>
<button className="maputnik-modal-header-toggle" <button className="maputnik-modal-header-toggle"
title={t("Close modal")} title="Close modal"
onClick={this.onClose} onClick={this.onClose}
data-wd-key={this.props["data-wd-key"]+".close-modal"} data-wd-key={this.props["data-wd-key"]+".close-modal"}
> >
@@ -69,5 +68,3 @@ class ModalInternal extends React.Component<ModalInternalProps> {
} }
} }
const Modal = withTranslation()(ModalInternal);
export default Modal;
+8 -12
View File
@@ -7,16 +7,15 @@ import FieldId from './FieldId'
import FieldSource from './FieldSource' import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer' import FieldSourceLayer from './FieldSourceLayer'
import type {LayerSpecification} from 'maplibre-gl' import type {LayerSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalAddInternalProps = { type ModalAddProps = {
layers: LayerSpecification[] layers: LayerSpecification[]
onLayersChange(layers: LayerSpecification[]): unknown onLayersChange(layers: LayerSpecification[]): unknown
isOpen: boolean isOpen: boolean
onOpenToggle(open: boolean): unknown onOpenToggle(open: boolean): unknown
// A dict of source id's and the available source layers // A dict of source id's and the available source layers
sources: any sources: any
} & WithTranslation; };
type ModalAddState = { type ModalAddState = {
type: LayerSpecification["type"] type: LayerSpecification["type"]
@@ -25,7 +24,7 @@ type ModalAddState = {
'source-layer'?: string 'source-layer'?: string
}; };
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> { export default class ModalAdd extends React.Component<ModalAddProps, ModalAddState> {
addLayer = () => { addLayer = () => {
const changedLayers = this.props.layers.slice(0) const changedLayers = this.props.layers.slice(0)
const layer: ModalAddState = { const layer: ModalAddState = {
@@ -46,7 +45,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
this.props.onOpenToggle(false) this.props.onOpenToggle(false)
} }
constructor(props: ModalAddInternalProps) { constructor(props: ModalAddProps) {
super(props) super(props)
const state: ModalAddState = { const state: ModalAddState = {
type: 'fill', type: 'fill',
@@ -55,12 +54,12 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
if(props.sources.length > 0) { if(props.sources.length > 0) {
state.source = Object.keys(this.props.sources)[0]; state.source = Object.keys(this.props.sources)[0];
state['source-layer'] = this.props.sources[state.source as keyof ModalAddInternalProps["sources"]][0] state['source-layer'] = this.props.sources[state.source as keyof ModalAddProps["sources"]][0]
} }
this.state = state; this.state = state;
} }
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) { componentDidUpdate(_prevProps: ModalAddProps, prevState: ModalAddState) {
// Check if source is valid for new type // Check if source is valid for new type
const oldType = prevState.type; const oldType = prevState.type;
const newType = this.state.type; const newType = this.state.type;
@@ -126,14 +125,13 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
render() { render() {
const t = this.props.t;
const sources = this.getSources(this.state.type); const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source!); const layers = this.getLayersForSource(this.state.source!);
return <Modal return <Modal
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Add Layer')} title={'Add Layer'}
data-wd-key="modal:add-layer" data-wd-key="modal:add-layer"
className="maputnik-add-modal" className="maputnik-add-modal"
> >
@@ -171,12 +169,10 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
onClick={this.addLayer} onClick={this.addLayer}
data-wd-key="add-layer" data-wd-key="add-layer"
> >
{t("Add Layer")} Add Layer
</InputButton> </InputButton>
</div> </div>
</Modal> </Modal>
} }
} }
const ModalAdd = withTranslation()(ModalAddInternal);
export default ModalAdd;
+13 -18
View File
@@ -1,10 +1,9 @@
import React from 'react' import React from 'react'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal' import Modal from './Modal'
type ModalDebugInternalProps = { type ModalDebugProps = {
isOpen: boolean isOpen: boolean
renderer: string renderer: string
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
@@ -19,12 +18,12 @@ type ModalDebugInternalProps = {
lat: number lat: number
} }
} }
} & WithTranslation; };
class ModalDebugInternal extends React.Component<ModalDebugInternalProps> { export default class ModalDebug extends React.Component<ModalDebugProps> {
render() { render() {
const {t, mapView} = this.props; const {mapView} = this.props;
const osmZoom = Math.round(mapView.zoom)+1; const osmZoom = Math.round(mapView.zoom)+1;
const osmLon = +(mapView.center.lng).toFixed(5); const osmLon = +(mapView.center.lng).toFixed(5);
@@ -34,10 +33,10 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
data-wd-key="modal:debug" data-wd-key="modal:debug"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Debug')} title={'Debug'}
> >
<section className="maputnik-modal-section maputnik-modal-shortcuts"> <section className="maputnik-modal-section maputnik-modal-shortcuts">
<h1>{t("Options")}</h1> <h1>Options</h1>
{this.props.renderer === 'mlgljs' && {this.props.renderer === 'mlgljs' &&
<ul> <ul>
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => { {Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
@@ -64,20 +63,16 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h1>Links</h1> <h1>Links</h1>
<p> <p>
<Trans t={t}> <a
<a target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`} >
> Open in OSM
Open in OSM </a> &mdash; Opens the current view on openstreetmap.org
</a> &mdash; Opens the current view on openstreetmap.org
</Trans>
</p> </p>
</section> </section>
</Modal> </Modal>
} }
} }
const ModalDebug = withTranslation()(ModalDebugInternal);
export default ModalDebug;
+13 -18
View File
@@ -5,7 +5,6 @@ import {version} from 'maplibre-gl/package.json'
import {format} from '@maplibre/maplibre-gl-style-spec' import {format} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification} from 'maplibre-gl' import type {StyleSpecification} from 'maplibre-gl'
import {MdFileDownload} from 'react-icons/md' import {MdFileDownload} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
import FieldString from './FieldString' import FieldString from './FieldString'
import InputButton from './InputButton' import InputButton from './InputButton'
@@ -17,15 +16,15 @@ import fieldSpecAdditional from '../libs/field-spec-additional'
const MAPLIBRE_GL_VERSION = version; const MAPLIBRE_GL_VERSION = version;
type ModalExportInternalProps = { type ModalExportProps = {
mapStyle: StyleSpecification & { id: string } mapStyle: StyleSpecification & { id: string }
onStyleChanged(...args: unknown[]): unknown onStyleChanged(...args: unknown[]): unknown
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(...args: unknown[]): unknown
} & WithTranslation; };
class ModalExportInternal extends React.Component<ModalExportInternalProps> { export default class ModalExport extends React.Component<ModalExportProps> {
tokenizedStyle () { tokenizedStyle () {
return format( return format(
@@ -49,7 +48,7 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
downloadHtml() { downloadHtml() {
const tokenStyle = this.tokenizedStyle(); const tokenStyle = this.tokenizedStyle();
const htmlTitle = this.props.mapStyle.name || this.props.t("Map"); const htmlTitle = this.props.mapStyle.name || "Map";
const html = `<!DOCTYPE html> const html = `<!DOCTYPE html>
<html> <html>
<head> <head>
@@ -101,32 +100,30 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
render() { render() {
const t = this.props.t;
const fsa = fieldSpecAdditional(t);
return <Modal return <Modal
data-wd-key="modal:export" data-wd-key="modal:export"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Export Style')} title={'Export Style'}
className="maputnik-export-modal" className="maputnik-export-modal"
> >
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h1>{t("Download Style")}</h1> <h1>Download Style</h1>
<p> <p>
{t("Download a JSON style to your computer.")} Download a JSON style to your computer.
</p> </p>
<div> <div>
<FieldString <FieldString
label={fsa.maputnik.maptiler_access_token.label} label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fsa.maputnik.maptiler_access_token} fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:openmaptiles_access_token']} value={(this.props.mapStyle.metadata || {} as any)['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")} onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/> />
<FieldString <FieldString
label={fsa.maputnik.thunderforest_access_token.label} label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token} fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:thunderforest_access_token']} value={(this.props.mapStyle.metadata || {} as any)['maputnik:thunderforest_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")} onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/> />
@@ -137,14 +134,14 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
onClick={this.downloadStyle.bind(this)} onClick={this.downloadStyle.bind(this)}
> >
<MdFileDownload /> <MdFileDownload />
{t("Download Style")} Download Style
</InputButton> </InputButton>
<InputButton <InputButton
onClick={this.downloadHtml.bind(this)} onClick={this.downloadHtml.bind(this)}
> >
<MdFileDownload /> <MdFileDownload />
{t("Download HTML")} Download HTML
</InputButton> </InputButton>
</div> </div>
</section> </section>
@@ -153,5 +150,3 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
} }
} }
const ModalExport = withTranslation()(ModalExportInternal);
export default ModalExport;
+4 -8
View File
@@ -2,25 +2,23 @@ import React from 'react'
import InputButton from './InputButton' import InputButton from './InputButton'
import Modal from './Modal' import Modal from './Modal'
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalLoadingInternalProps = { type ModalLoadingProps = {
isOpen: boolean isOpen: boolean
onCancel(...args: unknown[]): unknown onCancel(...args: unknown[]): unknown
title: string title: string
message: React.ReactNode message: React.ReactNode
} & WithTranslation; };
class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> { export default class ModalLoading extends React.Component<ModalLoadingProps> {
underlayOnClick(e: Event) { underlayOnClick(e: Event) {
// This stops click events falling through to underlying modals. // This stops click events falling through to underlying modals.
e.stopPropagation(); e.stopPropagation();
} }
render() { render() {
const t = this.props.t;
return <Modal return <Modal
data-wd-key="modal:loading" data-wd-key="modal:loading"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
@@ -37,12 +35,10 @@ class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
</p> </p>
<p className="maputnik-dialog__buttons"> <p className="maputnik-dialog__buttons">
<InputButton onClick={(e) => this.props.onCancel(e)}> <InputButton onClick={(e) => this.props.onCancel(e)}>
{t("Cancel")} Cancel
</InputButton> </InputButton>
</p> </p>
</Modal> </Modal>
} }
} }
const ModalLoading = withTranslation()(ModalLoadingInternal);
export default ModalLoading;
+17 -23
View File
@@ -2,7 +2,6 @@ import React, { FormEvent } from 'react'
import {MdFileUpload} from 'react-icons/md' import {MdFileUpload} from 'react-icons/md'
import {MdAddCircleOutline} from 'react-icons/md' import {MdAddCircleOutline} from 'react-icons/md'
import FileReaderInput, { Result } from 'react-file-reader-input' import FileReaderInput, { Result } from 'react-file-reader-input'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import ModalLoading from './ModalLoading' import ModalLoading from './ModalLoading'
import Modal from './Modal' import Modal from './Modal'
@@ -43,11 +42,11 @@ class PublicStyle extends React.Component<PublicStyleProps> {
} }
} }
type ModalOpenInternalProps = { type ModalOpenProps = {
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(...args: unknown[]): unknown
onStyleOpen(...args: unknown[]): unknown onStyleOpen(...args: unknown[]): unknown
} & WithTranslation; };
type ModalOpenState = { type ModalOpenState = {
styleUrl: string styleUrl: string
@@ -56,8 +55,8 @@ type ModalOpenState = {
activeRequestUrl?: string | null activeRequestUrl?: string | null
}; };
class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> { export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpenState> {
constructor(props: ModalOpenInternalProps) { constructor(props: ModalOpenProps) {
super(props); super(props);
this.state = { this.state = {
styleUrl: "" styleUrl: ""
@@ -175,7 +174,6 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
} }
render() { render() {
const t = this.props.t;
const styleOptions = publicStyles.map(style => { const styleOptions = publicStyles.map(style => {
return <PublicStyle return <PublicStyle
key={style.id} key={style.id}
@@ -202,31 +200,29 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
data-wd-key="modal:open" data-wd-key="modal:open"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={() => this.onOpenToggle()} onOpenToggle={() => this.onOpenToggle()}
title={t('Open Style')} title={'Open Style'}
> >
{errorElement} {errorElement}
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h1>{t("Upload Style")}</h1> <h1>Upload Style</h1>
<p>{t("Upload a JSON style from your computer.")}</p> <p>Upload a JSON style from your computer.</p>
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label={t("Style file")}> <FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label="Style file">
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Upload")}</InputButton> <InputButton className="maputnik-upload-button"><MdFileUpload /> Upload</InputButton>
</FileReaderInput> </FileReaderInput>
</section> </section>
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<form onSubmit={this.onSubmitUrl}> <form onSubmit={this.onSubmitUrl}>
<h1>{t("Load from URL")}</h1> <h1>Load from URL</h1>
<p> <p>
<Trans t={t}> Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
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> </p>
<InputUrl <InputUrl
aria-label={t("Style URL")} aria-label="Style URL"
data-wd-key="modal:open.url.input" data-wd-key="modal:open.url.input"
type="text" type="text"
className="maputnik-input" className="maputnik-input"
default={t("Enter URL...")} default="Enter URL..."
value={this.state.styleUrl} value={this.state.styleUrl}
onInput={this.onChangeUrl} onInput={this.onChangeUrl}
onChange={this.onChangeUrl} onChange={this.onChangeUrl}
@@ -243,9 +239,9 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
</section> </section>
<section className="maputnik-modal-section maputnik-modal-section--shrink"> <section className="maputnik-modal-section maputnik-modal-section--shrink">
<h1>{t("Gallery Styles")}</h1> <h1>Gallery Styles</h1>
<p> <p>
{t("Open one of the publicly available styles to start from.")} Open one of the publicly available styles to start from.
</p> </p>
<div className="maputnik-style-gallery-container"> <div className="maputnik-style-gallery-container">
{styleOptions} {styleOptions}
@@ -255,14 +251,12 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
<ModalLoading <ModalLoading
isOpen={!!this.state.activeRequest} isOpen={!!this.state.activeRequest}
title={t('Loading style')} title={'Loading style'}
onCancel={(e: Event) => this.onCancelActiveRequest(e)} onCancel={(e: Event) => this.onCancelActiveRequest(e)}
message={t("Loading: {{requestUrl}}", { requestUrl: this.state.activeRequestUrl })} message={"Loading: "+this.state.activeRequestUrl}
/> />
</div> </div>
) )
} }
} }
const ModalOpen = withTranslation()(ModalOpenInternal);
export default ModalOpen;
+48 -78
View File
@@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import {latest} from '@maplibre/maplibre-gl-style-spec'
import type {LightSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from 'maplibre-gl' import type {LightSpecification, StyleSpecification, TransitionSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
import FieldArray from './FieldArray' import FieldArray from './FieldArray'
import FieldNumber from './FieldNumber' import FieldNumber from './FieldNumber'
@@ -13,15 +12,15 @@ import FieldColor from './FieldColor'
import Modal from './Modal' import Modal from './Modal'
import fieldSpecAdditional from '../libs/field-spec-additional' import fieldSpecAdditional from '../libs/field-spec-additional'
type ModalSettingsInternalProps = { type ModalSettingsProps = {
mapStyle: StyleSpecification mapStyle: StyleSpecification
onStyleChanged(...args: unknown[]): unknown onStyleChanged(...args: unknown[]): unknown
onChangeMetadataProperty(...args: unknown[]): unknown onChangeMetadataProperty(...args: unknown[]): unknown
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(...args: unknown[]): unknown
} & WithTranslation; };
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> { export default class ModalSettings extends React.Component<ModalSettingsProps> {
changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) { changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
const transition = { const transition = {
...this.props.mapStyle.transition, ...this.props.mapStyle.transition,
@@ -59,25 +58,6 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
}); });
} }
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) { changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) {
const changedStyle = { const changedStyle = {
...this.props.mapStyle, ...this.props.mapStyle,
@@ -96,86 +76,87 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
render() { render() {
const metadata = this.props.mapStyle.metadata || {} as any; const metadata = this.props.mapStyle.metadata || {} as any;
const {t, onChangeMetadataProperty, mapStyle} = this.props; const {onChangeMetadataProperty, mapStyle} = this.props;
const fsa = fieldSpecAdditional(t); const inputProps = { }
const light = this.props.mapStyle.light || {}; const light = this.props.mapStyle.light || {};
const transition = this.props.mapStyle.transition || {}; const transition = this.props.mapStyle.transition || {};
const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification;
return <Modal return <Modal
data-wd-key="modal:settings" data-wd-key="modal:settings"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Style Settings')} title={'Style Settings'}
> >
<div className="modal:settings"> <div className="modal:settings">
<FieldString <FieldString {...inputProps}
label={t("Name")} label={"Name"}
fieldSpec={latest.$root.name} fieldSpec={latest.$root.name}
data-wd-key="modal:settings.name" data-wd-key="modal:settings.name"
value={this.props.mapStyle.name} value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")} onChange={this.changeStyleProperty.bind(this, "name")}
/> />
<FieldString <FieldString {...inputProps}
label={t("Owner")} label={"Owner"}
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}} fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}
data-wd-key="modal:settings.owner" data-wd-key="modal:settings.owner"
value={(this.props.mapStyle as any).owner} value={(this.props.mapStyle as any).owner}
onChange={this.changeStyleProperty.bind(this, "owner")} onChange={this.changeStyleProperty.bind(this, "owner")}
/> />
<FieldUrl <FieldUrl {...inputProps}
fieldSpec={latest.$root.sprite} fieldSpec={latest.$root.sprite}
label={t("Sprite URL")} label="Sprite URL"
data-wd-key="modal:settings.sprite" data-wd-key="modal:settings.sprite"
value={this.props.mapStyle.sprite as string} value={this.props.mapStyle.sprite as string}
onChange={this.changeStyleProperty.bind(this, "sprite")} onChange={this.changeStyleProperty.bind(this, "sprite")}
/> />
<FieldUrl <FieldUrl {...inputProps}
label={t("Glyphs URL")} label="Glyphs URL"
fieldSpec={latest.$root.glyphs} fieldSpec={latest.$root.glyphs}
data-wd-key="modal:settings.glyphs" data-wd-key="modal:settings.glyphs"
value={this.props.mapStyle.glyphs as string} value={this.props.mapStyle.glyphs as string}
onChange={this.changeStyleProperty.bind(this, "glyphs")} onChange={this.changeStyleProperty.bind(this, "glyphs")}
/> />
<FieldString <FieldString {...inputProps}
label={fsa.maputnik.maptiler_access_token.label} label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fsa.maputnik.maptiler_access_token} fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
data-wd-key="modal:settings.maputnik:openmaptiles_access_token" data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
value={metadata['maputnik:openmaptiles_access_token']} value={metadata['maputnik:openmaptiles_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")} onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/> />
<FieldString <FieldString {...inputProps}
label={fsa.maputnik.thunderforest_access_token.label} label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token} fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
data-wd-key="modal:settings.maputnik:thunderforest_access_token" data-wd-key="modal:settings.maputnik:thunderforest_access_token"
value={metadata['maputnik:thunderforest_access_token']} value={metadata['maputnik:thunderforest_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")} onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/> />
<FieldArray <FieldArray
label={t("Center")} label={"Center"}
fieldSpec={latest.$root.center} fieldSpec={latest.$root.center}
length={2} length={2}
type="number" type="number"
value={mapStyle.center || []} value={mapStyle.center || []}
default={[0, 0]} default={latest.$root.center.default || [0, 0]}
onChange={this.changeStyleProperty.bind(this, "center")} onChange={this.changeStyleProperty.bind(this, "center")}
/> />
<FieldNumber <FieldNumber
label={t("Zoom")} {...inputProps}
label={"Zoom"}
fieldSpec={latest.$root.zoom} fieldSpec={latest.$root.zoom}
value={mapStyle.zoom} value={mapStyle.zoom}
default={0} default={latest.$root.zoom.default || 0}
onChange={this.changeStyleProperty.bind(this, "zoom")} onChange={this.changeStyleProperty.bind(this, "zoom")}
/> />
<FieldNumber <FieldNumber
label={t("Bearing")} {...inputProps}
label={"Bearing"}
fieldSpec={latest.$root.bearing} fieldSpec={latest.$root.bearing}
value={mapStyle.bearing} value={mapStyle.bearing}
default={latest.$root.bearing.default} default={latest.$root.bearing.default}
@@ -183,7 +164,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/> />
<FieldNumber <FieldNumber
label={t("Pitch")} {...inputProps}
label={"Pitch"}
fieldSpec={latest.$root.pitch} fieldSpec={latest.$root.pitch}
value={mapStyle.pitch} value={mapStyle.pitch}
default={latest.$root.pitch.default} default={latest.$root.pitch.default}
@@ -191,7 +173,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/> />
<FieldEnum <FieldEnum
label={t("Light anchor")} {...inputProps}
label={"Light anchor"}
fieldSpec={latest.light.anchor} fieldSpec={latest.light.anchor}
name="light-anchor" name="light-anchor"
value={light.anchor as string} value={light.anchor as string}
@@ -201,7 +184,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/> />
<FieldColor <FieldColor
label={t("Light color")} {...inputProps}
label={"Light color"}
fieldSpec={latest.light.color} fieldSpec={latest.light.color}
value={light.color as string} value={light.color as string}
default={latest.light.color.default} default={latest.light.color.default}
@@ -209,7 +193,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/> />
<FieldNumber <FieldNumber
label={t("Light intensity")} {...inputProps}
label={"Light intensity"}
fieldSpec={latest.light.intensity} fieldSpec={latest.light.intensity}
value={light.intensity as number} value={light.intensity as number}
default={latest.light.intensity.default} default={latest.light.intensity.default}
@@ -217,7 +202,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/> />
<FieldArray <FieldArray
label={t("Light position")} {...inputProps}
label={"Light position"}
fieldSpec={latest.light.position} fieldSpec={latest.light.position}
type="number" type="number"
length={latest.light.position.length} length={latest.light.position.length}
@@ -226,24 +212,9 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
onChange={this.changeLightProperty.bind(this, "position")} 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 <FieldNumber
label={t("Terrain exaggeration")} {...inputProps}
fieldSpec={latest.terrain.exaggeration} label={"Transition delay"}
value={terrain.exaggeration}
default={latest.terrain.exaggeration.default}
onChange={this.changeTerrainProperty.bind(this, "exaggeration")}
/>
<FieldNumber
label={t("Transition delay")}
fieldSpec={latest.transition.delay} fieldSpec={latest.transition.delay}
value={transition.delay} value={transition.delay}
default={latest.transition.delay.default} default={latest.transition.delay.default}
@@ -251,20 +222,21 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
/> />
<FieldNumber <FieldNumber
label={t("Transition duration")} {...inputProps}
label={"Transition duration"}
fieldSpec={latest.transition.duration} fieldSpec={latest.transition.duration}
value={transition.duration} value={transition.duration}
default={latest.transition.duration.default} default={latest.transition.duration.default}
onChange={this.changeTransitionProperty.bind(this, "duration")} onChange={this.changeTransitionProperty.bind(this, "duration")}
/> />
<FieldSelect <FieldSelect {...inputProps}
label={fsa.maputnik.style_renderer.label} label={fieldSpecAdditional.maputnik.style_renderer.label}
fieldSpec={fsa.maputnik.style_renderer} fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
data-wd-key="modal:settings.maputnik:renderer" data-wd-key="modal:settings.maputnik:renderer"
options={[ options={[
['mlgljs', 'MapLibreGL JS'], ['mlgljs', 'MapLibreGL JS'],
['ol', t('Open Layers (experimental)')], ['ol', 'Open Layers (experimental)'],
]} ]}
value={metadata['maputnik:renderer'] || 'mlgljs'} value={metadata['maputnik:renderer'] || 'mlgljs'}
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')} onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')}
@@ -274,5 +246,3 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
} }
} }
const ModalSettings = withTranslation()(ModalSettingsInternal)
export default ModalSettings;
+26 -32
View File
@@ -1,50 +1,48 @@
import React from 'react' import React from 'react'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal' import Modal from './Modal'
type ModalShortcutsInternalProps = { type ModalShortcutsProps = {
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(...args: unknown[]): unknown
} & WithTranslation; };
class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps> { export default class ModalShortcuts extends React.Component<ModalShortcutsProps> {
render() { render() {
const t = this.props.t;
const help = [ const help = [
{ {
key: <kbd>?</kbd>, key: <kbd>?</kbd>,
text: t("Shortcuts menu") text: "Shortcuts menu"
}, },
{ {
key: <kbd>o</kbd>, key: <kbd>o</kbd>,
text: t("Open modal") text: "Open modal"
}, },
{ {
key: <kbd>e</kbd>, key: <kbd>e</kbd>,
text: t("Export modal") text: "Export modal"
}, },
{ {
key: <kbd>d</kbd>, key: <kbd>d</kbd>,
text: t("Data Sources modal") text: "Data Sources modal"
}, },
{ {
key: <kbd>s</kbd>, key: <kbd>s</kbd>,
text: t("Style Settings modal") text: "Style Settings modal"
}, },
{ {
key: <kbd>i</kbd>, key: <kbd>i</kbd>,
text: t("Toggle inspect") text: "Toggle inspect"
}, },
{ {
key: <kbd>m</kbd>, key: <kbd>m</kbd>,
text: t("Focus map") text: "Focus map"
}, },
{ {
key: <kbd>!</kbd>, key: <kbd>!</kbd>,
text: t("Debug modal") text: "Debug modal"
}, },
] ]
@@ -52,51 +50,51 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
const mapShortcuts = [ const mapShortcuts = [
{ {
key: <kbd>+</kbd>, key: <kbd>+</kbd>,
text: t("Increase the zoom level by 1.",) text: "Increase the zoom level by 1.",
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>+</kbd></>, key: <><kbd>Shift</kbd> + <kbd>+</kbd></>,
text: t("Increase the zoom level by 2.",) text: "Increase the zoom level by 2.",
}, },
{ {
key: <kbd>-</kbd>, key: <kbd>-</kbd>,
text: t("Decrease the zoom level by 1.",) text: "Decrease the zoom level by 1.",
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>-</kbd></>, key: <><kbd>Shift</kbd> + <kbd>-</kbd></>,
text: t("Decrease the zoom level by 2.",) text: "Decrease the zoom level by 2.",
}, },
{ {
key: <kbd>Up</kbd>, key: <kbd>Up</kbd>,
text: t("Pan up by 100 pixels.",) text: "Pan up by 100 pixels.",
}, },
{ {
key: <kbd>Down</kbd>, key: <kbd>Down</kbd>,
text: t("Pan down by 100 pixels.",) text: "Pan down by 100 pixels.",
}, },
{ {
key: <kbd>Left</kbd>, key: <kbd>Left</kbd>,
text: t("Pan left by 100 pixels.",) text: "Pan left by 100 pixels.",
}, },
{ {
key: <kbd>Right</kbd>, key: <kbd>Right</kbd>,
text: t("Pan right by 100 pixels.",) text: "Pan right by 100 pixels.",
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>, key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>,
text: t("Increase the rotation by 15 degrees.",) text: "Increase the rotation by 15 degrees.",
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>, key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>,
text: t("Decrease the rotation by 15 degrees.") text: "Decrease the rotation by 15 degrees."
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>, key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>,
text: t("Increase the pitch by 10 degrees.") text: "Increase the pitch by 10 degrees."
}, },
{ {
key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>, key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>,
text: t("Decrease the pitch by 10 degrees.") text: "Decrease the pitch by 10 degrees."
}, },
] ]
@@ -105,13 +103,11 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
data-wd-key="modal:shortcuts" data-wd-key="modal:shortcuts"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Shortcuts')} title={'Shortcuts'}
> >
<section className="maputnik-modal-section maputnik-modal-shortcuts"> <section className="maputnik-modal-section maputnik-modal-shortcuts">
<p> <p>
<Trans t={t}> Press <code>ESC</code> to lose focus of any active elements, then press one of:
Press <code>ESC</code> to lose focus of any active elements, then press one of:
</Trans>
</p> </p>
<dl> <dl>
{help.map((item, idx) => { {help.map((item, idx) => {
@@ -121,7 +117,7 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
</div> </div>
})} })}
</dl> </dl>
<p>{t("If the Map is in focused you can use the following shortcuts")}</p> <p>If the Map is in focused you can use the following shortcuts</p>
<ul> <ul>
{mapShortcuts.map((item, idx) => { {mapShortcuts.map((item, idx) => {
return <li key={idx}> return <li key={idx}>
@@ -134,5 +130,3 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
} }
} }
const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
export default ModalShortcuts;
+29 -37
View File
@@ -1,8 +1,7 @@
import React from 'react' import React from 'react'
import {MdAddCircleOutline, MdDelete} from 'react-icons/md' import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import {latest} from '@maplibre/maplibre-gl-style-spec'
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl' import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal' import Modal from './Modal'
import InputButton from './InputButton' import InputButton from './InputButton'
@@ -75,17 +74,16 @@ type ActiveModalSourcesTypeEditorProps = {
source: SourceSpecification source: SourceSpecification
onDelete(...args: unknown[]): unknown onDelete(...args: unknown[]): unknown
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; };
class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTypeEditorProps> { class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTypeEditorProps> {
render() { render() {
const t = this.props.t;
return <div className="maputnik-active-source-type-editor"> return <div className="maputnik-active-source-type-editor">
<div className="maputnik-active-source-type-editor-header"> <div className="maputnik-active-source-type-editor-header">
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span> <span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
<span className="maputnik-space" /> <span className="maputnik-space" />
<InputButton <InputButton
aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})} aria-label={`Remove '${this.props.sourceId}' source`}
className="maputnik-active-source-type-editor-header-delete" className="maputnik-active-source-type-editor-header-delete"
onClick={()=> this.props.onDelete(this.props.sourceId)} onClick={()=> this.props.onDelete(this.props.sourceId)}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
@@ -106,7 +104,7 @@ class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTyp
type AddSourceProps = { type AddSourceProps = {
onAdd(...args: unknown[]): unknown onAdd(...args: unknown[]): unknown
} & WithTranslation; };
type AddSourceState = { type AddSourceState = {
mode: EditorMode mode: EditorMode
@@ -136,7 +134,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
case 'geojson_json': return { case 'geojson_json': return {
type: 'geojson', type: 'geojson',
cluster: (source as GeoJSONSourceSpecification).cluster || false, cluster: (source as GeoJSONSourceSpecification).cluster || false,
data: '' data: {}
} }
case 'tilejson_vector': return { case 'tilejson_vector': return {
type: 'vector', type: 'vector',
@@ -204,7 +202,6 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
} }
render() { render() {
const t = this.props.t;
// Kind of a hack because the type changes, however maputnik has 1..n // Kind of a hack because the type changes, however maputnik has 1..n
// options per type, for example // options per type, for example
// //
@@ -218,25 +215,25 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
return <div className="maputnik-add-source"> return <div className="maputnik-add-source">
<FieldString <FieldString
label={t("Source ID")} label={"Source ID"}
fieldSpec={{doc: t("Unique ID that identifies the source and is used in the layer to reference the source.")}} fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}
value={this.state.sourceId} value={this.state.sourceId}
onChange={(v: string) => this.setState({ sourceId: v})} onChange={(v: string) => this.setState({ sourceId: v})}
/> />
<FieldSelect <FieldSelect
label={t("Source Type")} label={"Source Type"}
fieldSpec={sourceTypeFieldSpec} fieldSpec={sourceTypeFieldSpec}
options={[ options={[
['geojson_json', t('GeoJSON (JSON)')], ['geojson_json', 'GeoJSON (JSON)'],
['geojson_url', t('GeoJSON (URL)')], ['geojson_url', 'GeoJSON (URL)'],
['tilejson_vector', t('Vector (TileJSON URL)')], ['tilejson_vector', 'Vector (TileJSON URL)'],
['tilexyz_vector', t('Vector (XYZ URLs)')], ['tilexyz_vector', 'Vector (XYZ URLs)'],
['tilejson_raster', t('Raster (TileJSON URL)')], ['tilejson_raster', 'Raster (TileJSON URL)'],
['tilexyz_raster', t('Raster (XYZ URL)')], ['tilexyz_raster', 'Raster (XYZ URL)'],
['tilejson_raster-dem', t('Raster DEM (TileJSON URL)')], ['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
['tilexyz_raster-dem', t('Raster DEM (XYZ URLs)')], ['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
['image', t('Image')], ['image', 'Image'],
['video', t('Video')], ['video', 'Video'],
]} ]}
onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})} onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
value={this.state.mode as string} value={this.state.mode as string}
@@ -250,20 +247,20 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
className="maputnik-add-source-button" className="maputnik-add-source-button"
onClick={this.onAdd} onClick={this.onAdd}
> >
{t("Add Source")} Add Source
</InputButton> </InputButton>
</div> </div>
} }
} }
type ModalSourcesInternalProps = { type ModalSourcesProps = {
mapStyle: StyleSpecification mapStyle: StyleSpecification
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(...args: unknown[]): unknown
onStyleChanged(...args: unknown[]): unknown onStyleChanged(...args: unknown[]): unknown
} & WithTranslation; };
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> { export default class ModalSources extends React.Component<ModalSourcesProps> {
stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification { stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
const strippedSource = {...source} const strippedSource = {...source}
delete strippedSource['title'] delete strippedSource['title']
@@ -271,8 +268,7 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
} }
render() { render() {
const {t, mapStyle} = this.props; const mapStyle = this.props.mapStyle
const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady};
const activeSources = Object.keys(mapStyle.sources).map(sourceId => { const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
const source = mapStyle.sources[sourceId] const source = mapStyle.sources[sourceId]
return <ActiveModalSourcesTypeEditor return <ActiveModalSourcesTypeEditor
@@ -281,7 +277,6 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
source={source} source={source}
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))} onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))} onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
{...i18nProps}
/> />
}) })
@@ -300,17 +295,17 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
data-wd-key="modal:sources" data-wd-key="modal:sources"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={t('Sources')} title={'Sources'}
> >
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h1>{t("Active Sources")}</h1> <h1>Active Sources</h1>
{activeSources} {activeSources}
</section> </section>
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h1>{t("Choose Public Source")}</h1> <h1>Choose Public Source</h1>
<p> <p>
{t("Add one of the publicly available sources to your style.")} Add one of the publicly available sources to your style.
</p> </p>
<div className="maputnik-public-sources" style={{maxWidth: 500}}> <div className="maputnik-public-sources" style={{maxWidth: 500}}>
{tilesetOptions} {tilesetOptions}
@@ -318,16 +313,13 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
</section> </section>
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h1>{t("Add New Source")}</h1> <h1>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> <p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
<AddSource <AddSource
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))} onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
{...i18nProps}
/> />
</section> </section>
</Modal> </Modal>
} }
} }
const ModalSources = withTranslation()(ModalSourcesInternal);
export default ModalSources;
+26 -47
View File
@@ -8,8 +8,6 @@ import FieldDynamicArray from './FieldDynamicArray'
import FieldArray from './FieldArray' import FieldArray from './FieldArray'
import FieldJson from './FieldJson' import FieldJson from './FieldJson'
import FieldCheckbox from './FieldCheckbox' 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; export type EditorMode = "video" | "image" | "tilejson_vector" | "tilexyz_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "tilexyz_vector" | "geojson_url" | "geojson_json" | null;
@@ -19,15 +17,14 @@ type TileJSONSourceEditorProps = {
} }
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
children?: React.ReactNode children?: React.ReactNode
} & WithTranslation; };
class TileJSONSourceEditor extends React.Component<TileJSONSourceEditorProps> { class TileJSONSourceEditor extends React.Component<TileJSONSourceEditorProps> {
render() { render() {
const t = this.props.t;
return <div> return <div>
<FieldUrl <FieldUrl
label={t("TileJSON URL")} label={"TileJSON URL"}
fieldSpec={latest.source_vector.url} fieldSpec={latest.source_vector.url}
value={this.props.source.url} value={this.props.source.url}
onChange={url => this.props.onChange({ onChange={url => this.props.onChange({
@@ -48,7 +45,7 @@ type TileURLSourceEditorProps = {
} }
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
children?: React.ReactNode children?: React.ReactNode
} & WithTranslation; };
class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> { class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
changeTileUrls(tiles: string[]) { changeTileUrls(tiles: string[]) {
@@ -61,7 +58,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
renderTileUrls() { renderTileUrls() {
const tiles = this.props.source.tiles || []; const tiles = this.props.source.tiles || [];
return <FieldDynamicArray return <FieldDynamicArray
label={this.props.t("Tile URL")} label={"Tile URL"}
fieldSpec={latest.source_vector.tiles} fieldSpec={latest.source_vector.tiles}
type="url" type="url"
value={tiles} value={tiles}
@@ -70,11 +67,10 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
} }
render() { render() {
const t = this.props.t;
return <div> return <div>
{this.renderTileUrls()} {this.renderTileUrls()}
<FieldNumber <FieldNumber
label={t("Min Zoom")} label={"Min Zoom"}
fieldSpec={latest.source_vector.minzoom} fieldSpec={latest.source_vector.minzoom}
value={this.props.source.minzoom || 0} value={this.props.source.minzoom || 0}
onChange={minzoom => this.props.onChange({ onChange={minzoom => this.props.onChange({
@@ -83,7 +79,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
})} })}
/> />
<FieldNumber <FieldNumber
label={t("Max Zoom")} label={"Max Zoom"}
fieldSpec={latest.source_vector.maxzoom} fieldSpec={latest.source_vector.maxzoom}
value={this.props.source.maxzoom || 22} value={this.props.source.maxzoom || 22}
onChange={maxzoom => this.props.onChange({ onChange={maxzoom => this.props.onChange({
@@ -97,24 +93,16 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
} }
} }
const createCornerLabels: (t: TFunction) => { label: string, key: string }[] = (t) => ([
{ label: t("Coord top left"), key: "top left" },
{ label: t("Coord top right"), key: "top right" },
{ label: t("Coord bottom right"), key: "bottom right" },
{ label: t("Coord bottom left"), key: "bottom left" },
]);
type ImageSourceEditorProps = { type ImageSourceEditorProps = {
source: { source: {
coordinates: [number, number][] coordinates: [number, number][]
url: string url: string
} }
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; };
class ImageSourceEditor extends React.Component<ImageSourceEditorProps> { class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
render() { render() {
const t = this.props.t;
const changeCoord = (idx: number, val: [number, number]) => { const changeCoord = (idx: number, val: [number, number]) => {
const coordinates = this.props.source.coordinates.slice(0); const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val; coordinates[idx] = val;
@@ -127,7 +115,7 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
return <div> return <div>
<FieldUrl <FieldUrl
label={t("Image URL")} label={"Image URL"}
fieldSpec={latest.source_image.url} fieldSpec={latest.source_image.url}
value={this.props.source.url} value={this.props.source.url}
onChange={url => this.props.onChange({ onChange={url => this.props.onChange({
@@ -135,11 +123,11 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
url, url,
})} })}
/> />
{createCornerLabels(t).map(({label, key}, idx) => { {["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
return ( return (
<FieldArray <FieldArray
label={label} label={`Coord ${label}`}
key={key} key={label}
length={2} length={2}
type="number" type="number"
value={this.props.source.coordinates[idx]} value={this.props.source.coordinates[idx]}
@@ -158,11 +146,10 @@ type VideoSourceEditorProps = {
urls: string[] urls: string[]
} }
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; };
class VideoSourceEditor extends React.Component<VideoSourceEditorProps> { class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
render() { render() {
const t = this.props.t;
const changeCoord = (idx: number, val: [number, number]) => { const changeCoord = (idx: number, val: [number, number]) => {
const coordinates = this.props.source.coordinates.slice(0); const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val; coordinates[idx] = val;
@@ -182,18 +169,18 @@ class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
return <div> return <div>
<FieldDynamicArray <FieldDynamicArray
label={t("Video URL")} label={"Video URL"}
fieldSpec={latest.source_video.urls} fieldSpec={latest.source_video.urls}
type="string" type="string"
value={this.props.source.urls} value={this.props.source.urls}
default={[]} default={[]}
onChange={changeUrls} onChange={changeUrls}
/> />
{createCornerLabels(t).map(({label, key}, idx) => { {["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
return ( return (
<FieldArray <FieldArray
label={label} label={`Coord ${label}`}
key={key} key={label}
length={2} length={2}
type="number" type="number"
value={this.props.source.coordinates[idx]} value={this.props.source.coordinates[idx]}
@@ -211,13 +198,12 @@ type GeoJSONSourceUrlEditorProps = {
data: string data: string
} }
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; };
class GeoJSONSourceUrlEditor extends React.Component<GeoJSONSourceUrlEditorProps> { class GeoJSONSourceUrlEditor extends React.Component<GeoJSONSourceUrlEditorProps> {
render() { render() {
const t = this.props.t;
return <FieldUrl return <FieldUrl
label={t("GeoJSON URL")} label={"GeoJSON URL"}
fieldSpec={latest.source_geojson.data} fieldSpec={latest.source_geojson.data}
value={this.props.source.data} value={this.props.source.data}
onChange={data => this.props.onChange({ onChange={data => this.props.onChange({
@@ -234,13 +220,12 @@ type GeoJSONSourceFieldJsonEditorProps = {
cluster: boolean cluster: boolean
} }
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; };
class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJsonEditorProps> { class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJsonEditorProps> {
render() { render() {
const t = this.props.t;
return <div> return <div>
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}> <Block label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
<FieldJson <FieldJson
layer={this.props.source.data} layer={this.props.source.data}
maxHeight={200} maxHeight={200}
@@ -258,7 +243,7 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
/> />
</Block> </Block>
<FieldCheckbox <FieldCheckbox
label={t('Cluster')} label={'Cluster'}
value={this.props.source.cluster} value={this.props.source.cluster}
onChange={cluster => { onChange={cluster => {
this.props.onChange({ this.props.onChange({
@@ -271,22 +256,18 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
} }
} }
type ModalSourcesTypeEditorInternalProps = { type ModalSourcesTypeEditorProps = {
mode: EditorMode mode: EditorMode
source: any source: any
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
} & WithTranslation; };
class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEditorInternalProps> { export default class ModalSourcesTypeEditor extends React.Component<ModalSourcesTypeEditorProps> {
render() { render() {
const t = this.props.t;
const commonProps = { const commonProps = {
source: this.props.source, source: this.props.source,
onChange: this.props.onChange, onChange: this.props.onChange,
t: this.props.t, }
i18n: this.props.i18n,
tReady: this.props.tReady,
};
switch(this.props.mode) { switch(this.props.mode) {
case 'geojson_url': return <GeoJSONSourceUrlEditor {...commonProps} /> case 'geojson_url': return <GeoJSONSourceUrlEditor {...commonProps} />
case 'geojson_json': return <GeoJSONSourceFieldJsonEditor {...commonProps} /> case 'geojson_json': return <GeoJSONSourceFieldJsonEditor {...commonProps} />
@@ -297,7 +278,7 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} /> case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}> case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
<FieldSelect <FieldSelect
label={t("Encoding")} label={"Encoding"}
fieldSpec={latest.source_raster_dem.encoding} fieldSpec={latest.source_raster_dem.encoding}
options={Object.keys(latest.source_raster_dem.encoding.values)} options={Object.keys(latest.source_raster_dem.encoding.values)}
onChange={encoding => this.props.onChange({ onChange={encoding => this.props.onChange({
@@ -314,5 +295,3 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
} }
} }
const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
export default ModalSourcesTypeEditor;
+38
View File
@@ -0,0 +1,38 @@
import React from 'react'
import InputButton from './InputButton'
import Modal from './Modal'
// @ts-ignore
import logoImage from 'maputnik-design/logos/logo-color.svg'
type ModalSurveyProps = {
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
};
export default class ModalSurvey extends React.Component<ModalSurveyProps> {
onClick = () => {
window.open('https://gregorywolanski.typeform.com/to/cPgaSY', '_blank');
this.props.onOpenToggle();
}
render() {
return <Modal
data-wd-key="modal:survey"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title="Maputnik Survey"
>
<div className="maputnik-modal-survey">
<img src={logoImage} className="maputnik-modal-survey__logo" />
<h1>You + Maputnik = Maputnik better for you</h1>
<p className="maputnik-modal-survey__description">We dont track you, so we dont know how you use Maputnik. Help us make Maputnik better for you by completing a 7minute survey carried out by our contributing designer.</p>
<InputButton onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</InputButton>
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
</div>
</Modal>
}
}
+6 -10
View File
@@ -1,22 +1,18 @@
import React from 'react' import React from 'react'
import { WithTranslation, withTranslation } from 'react-i18next';
import './SmallError.scss'; import './SmallError.scss';
type SmallErrorInternalProps = { type SmallErrorProps = {
children?: React.ReactNode children?: React.ReactNode
} & WithTranslation; };
class SmallErrorInternal extends React.Component<SmallErrorInternalProps> {
export default class SmallError extends React.Component<SmallErrorProps> {
render () { render () {
const t = this.props.t;
return ( return (
<div className="SmallError"> <div className="SmallError">
{t("Error:")} {this.props.children} Error: {this.props.children}
</div> </div>
); );
} }
} }
const SmallError = withTranslation()(SmallErrorInternal);
export default SmallError;
+21 -27
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js'; import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import {latest} from '@maplibre/maplibre-gl-style-spec'
import InputButton from './InputButton' import InputButton from './InputButton'
import InputSpec from './InputSpec' import InputSpec from './InputSpec'
@@ -11,14 +11,13 @@ import Block from './Block'
import docUid from '../libs/document-uid' import docUid from '../libs/document-uid'
import sortNumerically from '../libs/sort-numerically' import sortNumerically from '../libs/sort-numerically'
import {findDefaultFromSpec} from '../libs/spec-helper'; import {findDefaultFromSpec} from '../libs/spec-helper';
import { WithTranslation, withTranslation } from 'react-i18next';
import labelFromFieldName from '../libs/label-from-field-name' import labelFromFieldName from '../libs/label-from-field-name'
import DeleteStopButton from './_DeleteStopButton' import DeleteStopButton from './_DeleteStopButton'
function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState) { function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
// This is initialsed below only if required to improved performance. // This is initialsed below only if required to improved performance.
let newRefs: {[key: number]: string} | undefined; let newRefs: {[key: number]: string} | undefined;
@@ -36,7 +35,7 @@ function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState)
return newRefs; return newRefs;
} }
type DataPropertyInternalProps = { type DataPropertyProps = {
onChange?(fieldName: string, value: any): unknown onChange?(fieldName: string, value: any): unknown
onDeleteStop?(...args: unknown[]): unknown onDeleteStop?(...args: unknown[]): unknown
onAddStop?(...args: unknown[]): unknown onAddStop?(...args: unknown[]): unknown
@@ -47,7 +46,7 @@ type DataPropertyInternalProps = {
fieldSpec?: object fieldSpec?: object
value?: DataPropertyValue value?: DataPropertyValue
errors?: object errors?: object
} & WithTranslation; };
type DataPropertyState = { type DataPropertyState = {
refs: {[key: number]: string} refs: {[key: number]: string}
@@ -66,7 +65,7 @@ export type Stop = [{
value: number value: number
}, number] }, number]
class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> { export default class DataProperty extends React.Component<DataPropertyProps, DataPropertyState> {
state = { state = {
refs: {} as {[key: number]: string} refs: {} as {[key: number]: string}
} }
@@ -81,7 +80,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
} }
} }
static getDerivedStateFromProps(props: Readonly<DataPropertyInternalProps>, state: DataPropertyState) { static getDerivedStateFromProps(props: DataPropertyProps, state: DataPropertyState) {
const newRefs = setStopRefs(props, state); const newRefs = setStopRefs(props, state);
if(newRefs) { if(newRefs) {
return { return {
@@ -214,8 +213,6 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
} }
render() { render() {
const t = this.props.t;
if (typeof this.props.value?.type === "undefined") { if (typeof this.props.value?.type === "undefined") {
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec) this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec)
} }
@@ -230,8 +227,8 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} /> const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
const dataProps = { const dataProps = {
'aria-label': t("Input value"), 'aria-label': "Input value",
label: t("Data value"), label: "Data value",
value: dataLevel as any, value: dataLevel as any,
onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value) onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
} }
@@ -266,7 +263,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
</td> </td>
<td> <td>
<InputSpec <InputSpec
aria-label={t("Output value")} aria-label="Output value"
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={value} value={value}
@@ -285,27 +282,27 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
<legend>{labelFromFieldName(this.props.fieldName)}</legend> <legend>{labelFromFieldName(this.props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner"> <div className="maputnik-data-fieldset-inner">
<Block <Block
label={t("Function")} label={"Function"}
key="function" key="function"
> >
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<InputSelect <InputSelect
value={this.props.value!.type} value={this.props.value!.type}
onChange={(propVal: string) => this.changeDataType(propVal)} onChange={(propVal: string) => this.changeDataType(propVal)}
title={t("Select a type of data scale (default is 'categorical').")} title={"Select a type of data scale (default is 'categorical')."}
options={this.getDataFunctionTypes(this.props.fieldSpec)} options={this.getDataFunctionTypes(this.props.fieldSpec)}
/> />
</div> </div>
</Block> </Block>
{this.props.value?.type !== "identity" && {this.props.value?.type !== "identity" &&
<Block <Block
label={t("Base")} label={"Base"}
key="base" key="base"
> >
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<InputSpec <InputSpec
fieldName={"base"} fieldName={"base"}
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }} fieldSpec={latest.function.base}
value={this.props.value?.base} value={this.props.value?.base}
onChange={(_, newValue) => this.changeBase(newValue as number)} onChange={(_, newValue) => this.changeBase(newValue as number)}
/> />
@@ -319,14 +316,14 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<InputString <InputString
value={this.props.value?.property} value={this.props.value?.property}
title={t("Input a data property to base styles off of.")} title={"Input a data property to base styles off of."}
onChange={propVal => this.changeDataProperty("property", propVal)} onChange={propVal => this.changeDataProperty("property", propVal)}
/> />
</div> </div>
</Block> </Block>
{dataFields && {dataFields &&
<Block <Block
label={t("Default")} label={"Default"}
key="default" key="default"
> >
<InputSpec <InputSpec
@@ -340,12 +337,12 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
{dataFields && {dataFields &&
<div className="maputnik-function-stop"> <div className="maputnik-function-stop">
<table className="maputnik-function-stop-table"> <table className="maputnik-function-stop-table">
<caption>{t("Stops")}</caption> <caption>Stops</caption>
<thead> <thead>
<tr> <tr>
<th>{t("Zoom")}</th> <th>Zoom</th>
<th>{t("Input value")}</th> <th>Input value</th>
<th rowSpan={2}>{t("Output value")}</th> <th rowSpan={2}>Output value</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -362,7 +359,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} /> <path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> {t("Add stop")} </svg> Add stop
</InputButton> </InputButton>
} }
<InputButton <InputButton
@@ -371,7 +368,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} /> <path fill="currentColor" d={mdiFunctionVariant} />
</svg> {t("Convert to expression")} </svg> Convert to expression
</InputButton> </InputButton>
</div> </div>
</div> </div>
@@ -379,6 +376,3 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
</div> </div>
} }
} }
const DataProperty = withTranslation()(DataPropertyInternal);
export default DataProperty;
+4 -9
View File
@@ -2,26 +2,21 @@ import React from 'react'
import InputButton from './InputButton' import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md' import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
type DeleteStopButtonInternalProps = { type DeleteStopButtonProps = {
onClick?(...args: unknown[]): unknown onClick?(...args: unknown[]): unknown
} & WithTranslation; };
class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalProps> { export default class DeleteStopButton extends React.Component<DeleteStopButtonProps> {
render() { render() {
const t = this.props.t;
return <InputButton return <InputButton
className="maputnik-delete-stop" className="maputnik-delete-stop"
onClick={this.props.onClick} onClick={this.props.onClick}
title={t("Remove zoom level from stop")} title={"Remove zoom level from stop"}
> >
<MdDelete /> <MdDelete />
</InputButton> </InputButton>
} }
} }
const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
export default DeleteStopButton;
+8 -12
View File
@@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import {MdDelete, MdUndo} from 'react-icons/md' import {MdDelete, MdUndo} from 'react-icons/md'
import stringifyPretty from 'json-stringify-pretty-compact' import stringifyPretty from 'json-stringify-pretty-compact'
import { WithTranslation, withTranslation } from 'react-i18next';
import Block from './Block' import Block from './Block'
import InputButton from './InputButton' import InputButton from './InputButton'
@@ -9,7 +8,7 @@ import labelFromFieldName from '../libs/label-from-field-name'
import FieldJson from './FieldJson' import FieldJson from './FieldJson'
type ExpressionPropertyInternalProps = { type ExpressionPropertyProps = {
onDelete?(...args: unknown[]): unknown onDelete?(...args: unknown[]): unknown
fieldName: string fieldName: string
fieldType?: string fieldType?: string
@@ -21,20 +20,20 @@ type ExpressionPropertyInternalProps = {
canUndo?(...args: unknown[]): unknown canUndo?(...args: unknown[]): unknown
onFocus?(...args: unknown[]): unknown onFocus?(...args: unknown[]): unknown
onBlur?(...args: unknown[]): unknown onBlur?(...args: unknown[]): unknown
} & WithTranslation; };
type ExpressionPropertyState = { type ExpressionPropertyState = {
jsonError: boolean jsonError: boolean
}; };
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps, ExpressionPropertyState> { export default class ExpressionProperty extends React.Component<ExpressionPropertyProps, ExpressionPropertyState> {
static defaultProps = { static defaultProps = {
errors: {}, errors: {},
onFocus: () => {}, onFocus: () => {},
onBlur: () => {}, onBlur: () => {},
} }
constructor(props: ExpressionPropertyInternalProps) { constructor (props:ExpressionPropertyProps) {
super(props); super(props);
this.state = { this.state = {
jsonError: false, jsonError: false,
@@ -54,7 +53,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
} }
render() { render() {
const {t, errors, fieldName, fieldType, value, canUndo} = this.props; const {errors, fieldName, fieldType, value, canUndo} = this.props;
const {jsonError} = this.state; const {jsonError} = this.state;
const undoDisabled = canUndo ? !canUndo() : true; const undoDisabled = canUndo ? !canUndo() : true;
@@ -66,7 +65,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
onClick={this.props.onUndo} onClick={this.props.onUndo}
disabled={undoDisabled} disabled={undoDisabled}
className="maputnik-delete-stop" className="maputnik-delete-stop"
title={t("Revert from expression")} title="Revert from expression"
> >
<MdUndo /> <MdUndo />
</InputButton> </InputButton>
@@ -75,7 +74,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
key="delete_action" key="delete_action"
onClick={this.props.onDelete} onClick={this.props.onDelete}
className="maputnik-delete-stop" className="maputnik-delete-stop"
title={t("Delete expression")} title="Delete expression"
> >
<MdDelete /> <MdDelete />
</InputButton> </InputButton>
@@ -113,7 +112,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
// this feels like an incorrect type...? `foundErrors` is an array of objects, not a single object // this feels like an incorrect type...? `foundErrors` is an array of objects, not a single object
error={foundErrors as any} error={foundErrors as any}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
label={t(labelFromFieldName(this.props.fieldName))} label={labelFromFieldName(this.props.fieldName)}
action={deleteStopBtn} action={deleteStopBtn}
wideMode={true} wideMode={true}
> >
@@ -138,6 +137,3 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
</Block> </Block>
} }
} }
const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
export default ExpressionProperty;
+6 -11
View File
@@ -3,18 +3,16 @@ import React from 'react'
import InputButton from './InputButton' import InputButton from './InputButton'
import {MdFunctions, MdInsertChart} from 'react-icons/md' import {MdFunctions, MdInsertChart} from 'react-icons/md'
import {mdiFunctionVariant} from '@mdi/js'; import {mdiFunctionVariant} from '@mdi/js';
import { WithTranslation, withTranslation } from 'react-i18next';
type FunctionInputButtonsInternalProps = { type FunctionInputButtonsProps = {
fieldSpec?: any fieldSpec?: any
onZoomClick?(...args: unknown[]): unknown onZoomClick?(...args: unknown[]): unknown
onDataClick?(...args: unknown[]): unknown onDataClick?(...args: unknown[]): unknown
onExpressionClick?(...args: unknown[]): unknown onExpressionClick?(...args: unknown[]): unknown
} & WithTranslation; };
class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> { export default class FunctionInputButtons extends React.Component<FunctionInputButtonsProps> {
render() { render() {
const t = this.props.t;
let makeZoomInputButton, makeDataInputButton, expressionInputButton; let makeZoomInputButton, makeDataInputButton, expressionInputButton;
if (this.props.fieldSpec.expression.parameters.includes('zoom')) { if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
@@ -22,7 +20,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
<InputButton <InputButton
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
onClick={this.props.onExpressionClick} onClick={this.props.onExpressionClick}
title={t("Convert to expression")} title="Convert to expression"
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} /> <path fill="currentColor" d={mdiFunctionVariant} />
@@ -33,7 +31,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
makeZoomInputButton = <InputButton makeZoomInputButton = <InputButton
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
onClick={this.props.onZoomClick} onClick={this.props.onZoomClick}
title={t("Convert property into a zoom function")} title="Convert property into a zoom function"
> >
<MdFunctions /> <MdFunctions />
</InputButton> </InputButton>
@@ -42,7 +40,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
makeDataInputButton = <InputButton makeDataInputButton = <InputButton
className="maputnik-make-data-function" className="maputnik-make-data-function"
onClick={this.props.onDataClick} onClick={this.props.onDataClick}
title={t("Convert property to data function")} title="Convert property to data function"
> >
<MdInsertChart /> <MdInsertChart />
</InputButton> </InputButton>
@@ -58,6 +56,3 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
} }
} }
} }
const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
export default FunctionInputButtons;
+20 -24
View File
@@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js'; import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' import {latest} from '@maplibre/maplibre-gl-style-spec'
import { WithTranslation, withTranslation } from 'react-i18next';
import InputButton from './InputButton' import InputButton from './InputButton'
import InputSpec from './InputSpec' import InputSpec from './InputSpec'
@@ -21,7 +20,7 @@ import sortNumerically from '../libs/sort-numerically'
* *
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus. * When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
*/ */
function setStopRefs(props: ZoomPropertyInternalProps, state: ZoomPropertyState) { function setStopRefs(props: ZoomPropertyProps, state: ZoomPropertyState) {
// This is initialsed below only if required to improved performance. // This is initialsed below only if required to improved performance.
let newRefs: {[key: number]: string} = {}; let newRefs: {[key: number]: string} = {};
@@ -46,7 +45,7 @@ type ZoomWithStops = {
} }
type ZoomPropertyInternalProps = { type ZoomPropertyProps = {
onChange?(...args: unknown[]): unknown onChange?(...args: unknown[]): unknown
onChangeToDataFunction?(...args: unknown[]): unknown onChangeToDataFunction?(...args: unknown[]): unknown
onDeleteStop?(...args: unknown[]): unknown onDeleteStop?(...args: unknown[]): unknown
@@ -60,13 +59,13 @@ type ZoomPropertyInternalProps = {
} }
errors?: object errors?: object
value?: ZoomWithStops value?: ZoomWithStops
} & WithTranslation; };
type ZoomPropertyState = { type ZoomPropertyState = {
refs: {[key: number]: string} refs: {[key: number]: string}
} }
class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> { export default class ZoomProperty extends React.Component<ZoomPropertyProps, ZoomPropertyState> {
static defaultProps = { static defaultProps = {
errors: {}, errors: {},
} }
@@ -85,7 +84,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
} }
} }
static getDerivedStateFromProps(props: Readonly<ZoomPropertyInternalProps>, state: ZoomPropertyState) { static getDerivedStateFromProps(props: ZoomPropertyProps, state: ZoomPropertyState) {
const newRefs = setStopRefs(props, state); const newRefs = setStopRefs(props, state);
if(newRefs) { if(newRefs) {
return { return {
@@ -153,17 +152,17 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
} }
render() { render() {
const t = this.props.t;
const zoomFields = this.props.value?.stops.map((stop, idx) => { const zoomFields = this.props.value?.stops.map((stop, idx) => {
const zoomLevel = stop[0] const zoomLevel = stop[0]
const key = this.state.refs[idx];
const value = stop[1] 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 return <tr
key={`${stop[0]}-${stop[1]}`} key={key}
> >
<td> <td>
<InputNumber <InputNumber
aria-label={t("Zoom")} aria-label="Zoom"
value={zoomLevel} value={zoomLevel}
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)} onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
min={0} min={0}
@@ -172,7 +171,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
</td> </td>
<td> <td>
<InputSpec <InputSpec
aria-label={t("Output value")} aria-label="Output value"
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec as any} fieldSpec={this.props.fieldSpec as any}
value={value} value={value}
@@ -191,24 +190,24 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
<legend>{labelFromFieldName(this.props.fieldName)}</legend> <legend>{labelFromFieldName(this.props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner"> <div className="maputnik-data-fieldset-inner">
<Block <Block
label={t("Function")} label={"Function"}
> >
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<InputSelect <InputSelect
value={"interpolate"} value={"interpolate"}
onChange={(propVal: string) => this.changeDataType(propVal)} onChange={(propVal: string) => this.changeDataType(propVal)}
title={t("Select a type of data scale (default is 'categorical').")} title={"Select a type of data scale (default is 'categorical')."}
options={this.getDataFunctionTypes(this.props.fieldSpec!)} options={this.getDataFunctionTypes(this.props.fieldSpec!)}
/> />
</div> </div>
</Block> </Block>
<Block <Block
label={t("Base")} label={"Base"}
> >
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<InputSpec <InputSpec
fieldName={"base"} fieldName={"base"}
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }} fieldSpec={latest.function.base}
value={this.props.value?.base} value={this.props.value?.base}
onChange={(_, newValue) => this.changeBase(newValue as number | undefined)} onChange={(_, newValue) => this.changeBase(newValue as number | undefined)}
/> />
@@ -216,11 +215,11 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
</Block> </Block>
<div className="maputnik-function-stop"> <div className="maputnik-function-stop">
<table className="maputnik-function-stop-table maputnik-function-stop-table--zoom"> <table className="maputnik-function-stop-table maputnik-function-stop-table--zoom">
<caption>{t("Stops")}</caption> <caption>Stops</caption>
<thead> <thead>
<tr> <tr>
<th>{t("Zoom")}</th> <th>Zoom</th>
<th rowSpan={2}>{t("Output value")}</th> <th rowSpan={2}>Output value</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -235,7 +234,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} /> <path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> {t("Add stop")} </svg> Add stop
</InputButton> </InputButton>
<InputButton <InputButton
className="maputnik-add-stop" className="maputnik-add-stop"
@@ -243,7 +242,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} /> <path fill="currentColor" d={mdiFunctionVariant} />
</svg> {t("Convert to expression")} </svg> Convert to expression
</InputButton> </InputButton>
</div> </div>
</div> </div>
@@ -263,6 +262,3 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
} }
} }
} }
const ZoomProperty = withTranslation()(ZoomPropertyInternal);
export default ZoomProperty;
+36 -48
View File
@@ -1,40 +1,40 @@
[ [
{ {
"id": "0-empty-style", "id": "osm-liberty",
"title": "Empty Style", "title": "OSM Liberty",
"url": "https://cdn.jsdelivr.net/gh/maputnik/editor@9cf74ca405d2be0608b57db8109cf3a6af5b9f49/src/config/empty-style.json", "url": "https://maputnik.github.io/osm-liberty/style.json",
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII=" "thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png"
},
{
"id": "dark-matter",
"title": "Dark Matter",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
}, },
{ {
"id": "maptiler-basic-gl-style", "id": "maptiler-basic-gl-style",
"title": "Maptiler Basic", "title": "Maptiler Basic",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.10/style.json", "url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png" "thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
}, },
{
"id": "dark-matter",
"title": "Dark Matter",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.8/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
},
{
"id": "positron",
"title": "Positron",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.8/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
},
{
"id": "osm-bright",
"title": "OSM Bright",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
},
{ {
"id": "maptiler-toner-gl-style", "id": "maptiler-toner-gl-style",
"title": "Toner", "title": "Toner",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@v1.0/style.json", "url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@339e5b7/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png" "thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
}, },
{
"id": "os-zoomstack-light",
"title": "Zoomstack Light",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-light/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
},
{
"id": "os-zoomstack-night",
"title": "Zoomstack Night",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-night/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
},
{ {
"id": "os-zoomstack-outdoor", "id": "os-zoomstack-outdoor",
"title": "Zoomstack Outdoor", "title": "Zoomstack Outdoor",
@@ -48,33 +48,21 @@
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png" "thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
}, },
{ {
"id": "osm-bright", "id": "os-zoomstack-light",
"title": "OSM Bright", "title": "Zoomstack Light",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.11/style.json", "url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-light/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png" "thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
}, },
{ {
"id": "osm-liberty", "id": "os-zoomstack-night",
"title": "OSM Liberty", "title": "Zoomstack Night",
"url": "https://maputnik.github.io/osm-liberty/style.json", "url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-night/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png" "thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
}, },
{ {
"id": "positron", "id": "empty-style",
"title": "Positron", "title": "Empty Style",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.9/style.json", "url": "https://cdn.jsdelivr.net/gh/maputnik/editor@9cf74ca405d2be0608b57db8109cf3a6af5b9f49/src/config/empty-style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png" "thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
},
{
"id": "stadia-outdoors",
"title": "Stadia Outdoors",
"url": "https://tiles.stadiamaps.com/styles/outdoors.json",
"thumbnail": "https://tiles.stadiamaps.com/static/outdoors.png?size=480x320&center=47.350259,8.49035&zoom=16"
},
{
"id": "versatiles-colorful",
"title": "Versatiles Colorful",
"url": "https://tiles.versatiles.org/assets/styles/colorful.json",
"thumbnail": "https://github.com/maplibre/maputnik/assets/649392/6cd69818-c541-46e4-a920-65fb4f654931"
} }
] ]
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"openmaptiles": { "openmaptiles": {
"type": "vector", "type": "vector",
"url": "https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key={key}", "url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}",
"title": "OpenMapTiles v3" "title": "OpenMapTiles v3"
}, },
"thunderforest_transport": { "thunderforest_transport": {
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"openmaptiles": "get_your_own_OpIi9ZULNHzrESv6T2vL", "openmaptiles": "KDhMfHvorAFkFe64wlZb",
"thunderforest": "b71f7f0ba4064f5eb9e903859a9cf5c6" "thunderforest": "b71f7f0ba4064f5eb9e903859a9cf5c6"
} }
-40
View File
@@ -1,40 +0,0 @@
import i18n from "i18next";
import detector from "i18next-browser-languagedetector";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next";
export const supportedLanguages = {
"en": "English",
"ja": "日本語",
"he": "עברית",
"zh": "简体中文"
} as const;
i18n
.use(detector) // detect user language from browser settings
.use(
resourcesToBackend((lang: string, ns: string) => {
if (lang === "en") {
// English is the default language, so we don't need to load any resources for it.
return {};
}
return import(`./locales/${lang}/${ns}.json`);
})
)
.use(initReactI18next) // required to initialize react-i18next
.init({
supportedLngs: Object.keys(supportedLanguages),
keySeparator: false, // we do not use keys in form messages.welcome
nsSeparator: false,
interpolation: {
escapeValue: false // React already escapes for us
},
saveMissing: true, // this needs to be set for missingKeyHandler to work
fallbackLng: false, // we set the fallback to false so we can get the correct language in the missingKeyHandler
missingKeyHandler: (lngs, _ns, key) => {
if (lngs[0] === "en") { return; }
console.warn(`Missing translation for "${key}" in "${lngs.join(", ")}"`);
}
});
export default i18n;

Some files were not shown because too many files have changed in this diff Show More