mirror of
https://github.com/maputnik/editor.git
synced 2026-09-01 18:07:37 +00:00
Compare commits
6 Commits
v2.1.0
..
085eda76a1
| Author | SHA1 | Date | |
|---|---|---|---|
| 085eda76a1 | |||
| 0dc3f4bf5d | |||
| efc507aecd | |||
| ef902910c8 | |||
| 6ed08c7416 | |||
| 9014e12ca6 |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"packages": [],
|
||||
"sandboxes": ["/"]
|
||||
}
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
github: [maplibre]
|
||||
open_collective: maplibre
|
||||
custom: "https://maputnik.github.io/donate"
|
||||
|
||||
@@ -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.
|
||||
|
||||
+39
-15
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- 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-node:
|
||||
@@ -34,6 +34,12 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
@@ -51,42 +57,60 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: artifacts/maputnik
|
||||
uses: actions/upload-artifact@v4
|
||||
- run: npm run build-storybook
|
||||
- name: artifacts/editor
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik
|
||||
name: editor
|
||||
path: dist
|
||||
- name: artifacts/storybook
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: storybook
|
||||
path: build/storybook
|
||||
|
||||
# Build and upload desktop CLI artifacts
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
go-version: ^1.19.x
|
||||
id: go
|
||||
|
||||
- name: Build desktop artifacts
|
||||
run: npm run build-desktop
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: maputnik/desktop
|
||||
ref: master
|
||||
path: ./src/github.com/maputnik/desktop/
|
||||
|
||||
- name: Make
|
||||
run: cd src/github.com/maputnik/desktop/ && make
|
||||
|
||||
- name: Artifacts/linux
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik-linux
|
||||
path: ./desktop/bin/linux/
|
||||
path: ./src/github.com/maputnik/desktop/bin/linux/
|
||||
|
||||
- name: Artifacts/darwin
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik-darwin
|
||||
path: ./desktop/bin/darwin/
|
||||
path: ./src/github.com/maputnik/desktop/bin/darwin/
|
||||
|
||||
- name: Artifacts/windows
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik-windows
|
||||
path: ./desktop/bin/windows/
|
||||
path: ./src/github.com/maputnik/desktop/bin/windows/
|
||||
|
||||
e2e-tests:
|
||||
name: "E2E tests using ${{ matrix.browser }}"
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
name: Create bump version PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to change to.
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version-pr:
|
||||
name: Bump version PR
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
npm version --commit-hooks false --git-tag-version false ${{ inputs.version }}
|
||||
./build/bump-version-changelog.js ${{ inputs.version }}
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
commit-message: Bump version to ${{ inputs.version }}
|
||||
branch: bump-version-to-${{ inputs.version }}
|
||||
title: Bump version to ${{ inputs.version }}
|
||||
@@ -3,49 +3,24 @@ name: deploy
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
deploy-pages:
|
||||
name: deploy/pages
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: dist
|
||||
|
||||
# publish docker to GitHub registry
|
||||
deploy-docker:
|
||||
name: deploy/docker
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
|
||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
|
||||
- run: docker push docker.pkg.github.com/maputnik/editor/editor:main
|
||||
|
||||
@@ -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
@@ -33,6 +33,6 @@ node_modules
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
/cypress/screenshots
|
||||
/dist/
|
||||
/desktop/version.go
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { addons } from '@storybook/addons';
|
||||
import { themes } from '@storybook/theming';
|
||||
import theme from './maputnik.theme';
|
||||
|
||||
addons.setConfig({
|
||||
theme: theme,
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { create } from '@storybook/theming/create';
|
||||
|
||||
export default create({
|
||||
base: 'light',
|
||||
|
||||
brandTitle: 'Maputnik',
|
||||
brandUrl: 'https://github.com/maputnik/editor',
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
## main
|
||||
|
||||
### ✨ Features and improvements
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
- _...Add new stuff here..._
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### ✨ Features and improvements
|
||||
|
||||
- Add GitHub workflows for releasing new versions
|
||||
- Update desktop build to pull from this repo (#922)
|
||||
|
||||
## 2.0.0
|
||||
|
||||
- Update MapLibre to version 4 (#872)
|
||||
- Start continuous deployment of maputnik website
|
||||
|
||||
## 1.7.0
|
||||
|
||||
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# Contributor Covenant
|
||||
[](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)
|
||||
+3
-3
@@ -2,12 +2,12 @@ FROM node:18 as builder
|
||||
WORKDIR /maputnik
|
||||
|
||||
# Only copy package.json to prevent npm install from running on every build
|
||||
COPY package.json package-lock.json .npmrc ./
|
||||
RUN npm ci
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
# Build maputnik
|
||||
COPY . .
|
||||
RUN npx vite build
|
||||
RUN npm run build
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Create a clean nginx-alpine slim image with just the build results
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Lukas Martinelli
|
||||
Copyright (c) 2024 MapLibre contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
||||
|
||||
# Maputnik
|
||||
[][github-action-ci]
|
||||
[][github-action-ci]
|
||||
[][license]
|
||||
|
||||
[github-action-ci]: https://github.com/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
|
||||
|
||||
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
|
||||
|
||||
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage)
|
||||
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/wiki/Maputnik-CLI) for local style development
|
||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
||||
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8888:80 ghcr.io/maplibre/maputnik:main
|
||||
docker run -it --rm -p 8888:8888 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
|
||||
|
||||
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
|
||||
|
||||
[](https://youtu.be/XoDh0gEnBQo)
|
||||
|
||||
## 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).
|
||||
|
||||
### Getting Involved
|
||||
Join the #maplibre or #maputnik slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/ Read the the below guide in order to get familiar with how we do things around here.
|
||||
|
||||
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
||||
|
||||
```bash
|
||||
@@ -66,8 +67,7 @@ Lint the JavaScript code.
|
||||
```
|
||||
# run linter
|
||||
npm run lint
|
||||
npm run lint-css
|
||||
npm run sort-styles
|
||||
npm run lint-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
|
||||
```
|
||||
|
||||
## Release process
|
||||
|
||||
1. Review [`CHANGELOG.md`](/CHANGELOG.md)
|
||||
- Double-check that all changes included in the release are appropriately documented.
|
||||
- To-be-released changes should be under the "main" header.
|
||||
- Commit any final changes to the changelog.
|
||||
2. Run [Create bump version PR](https://github.com/maplibre/maputnik/actions/workflows/create-bump-version-pr.yml) by manual workflow dispatch and set the version number in the input. This will create a PR that changes the changelog and `package.json` file to review and merge.
|
||||
3. Once merged, an automatic process will kick in and creates a GitHub release and uploads release assets.
|
||||
## Related Projects
|
||||
|
||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
||||
|
||||
## Sponsors
|
||||
|
||||
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
|
||||
|
||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and Maplibre contributors.
|
||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by other map studios and make your own decisions for a good style editor.
|
||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
||||
|
||||
**Disclaimer** This is an independent style editor.
|
||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
For an up-to-date policy refer to
|
||||
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt
|
||||
@@ -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
|
||||
@@ -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');
|
||||
@@ -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());
|
||||
@@ -1,7 +1,8 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
import { then } from "@shellygo/cypress-test-utils/assertable";
|
||||
import MaputnikDriver from "./maputnik-driver";
|
||||
|
||||
describe("accessibility", () => {
|
||||
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, get, when, should } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
describe("skip links", () => {
|
||||
@@ -11,9 +12,9 @@ describe("accessibility", () => {
|
||||
|
||||
it("skip link to layer list", () => {
|
||||
const selector = "root:skip:layer-list";
|
||||
then(get.elementByTestId(selector)).shouldExist();
|
||||
should.exist(selector);
|
||||
when.tab();
|
||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
should.beFocused(selector);
|
||||
when.click(selector);
|
||||
then(get.skipTargetLayerList()).shouldBeFocused();
|
||||
});
|
||||
@@ -21,18 +22,18 @@ describe("accessibility", () => {
|
||||
// This fails for some reason only in Chrome, but passes in firefox. Adding a skip here to allow merge and later on we'll decide if we want to fix this or not.
|
||||
it.skip("skip link to layer editor", () => {
|
||||
const selector = "root:skip:layer-editor";
|
||||
then(get.elementByTestId(selector)).shouldExist();
|
||||
should.exist(selector);
|
||||
when.tab().tab();
|
||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
should.beFocused(selector);
|
||||
when.click(selector);
|
||||
then(get.skipTargetLayerEditor()).shouldBeFocused();
|
||||
});
|
||||
|
||||
it("skip link to map view", () => {
|
||||
const selector = "root:skip:map-view";
|
||||
then(get.elementByTestId(selector)).shouldExist();
|
||||
should.exist(selector);
|
||||
when.tab().tab().tab();
|
||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
should.beFocused(selector);
|
||||
when.click(selector);
|
||||
then(get.canvas()).shouldBeFocused();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||
|
||||
export default class MaputnikCypressHelper {
|
||||
export default class CypressWrapperDriver {
|
||||
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
||||
|
||||
public given = {
|
||||
@@ -13,6 +13,9 @@ export default class MaputnikCypressHelper {
|
||||
|
||||
public when = {
|
||||
...this.helper.when,
|
||||
confirmAlert() {
|
||||
cy.on("window:confirm", () => true);
|
||||
},
|
||||
};
|
||||
|
||||
public beforeAndAfter = this.helper.beforeAndAfter;
|
||||
+31
-59
@@ -1,7 +1,7 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
import MaputnikDriver from "./maputnik-driver";
|
||||
|
||||
describe("history", () => {
|
||||
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, when, get, should } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
let undoKeyCombo: string;
|
||||
@@ -16,20 +16,23 @@ describe("history", () => {
|
||||
it("undo/redo", () => {
|
||||
when.setStyle("geojson");
|
||||
when.modal.open();
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||
|
||||
should.equalStyleStore((a: any) => a.layers, []);
|
||||
|
||||
when.modal.fillLayers({
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
});
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
@@ -37,8 +40,9 @@ describe("history", () => {
|
||||
type: "background",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
@@ -47,35 +51,38 @@ describe("history", () => {
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
|
||||
when.typeKeys(undoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
|
||||
when.typeKeys(undoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||
should.equalStyleStore((a: any) => a.layers, []);
|
||||
|
||||
when.typeKeys(redoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
|
||||
when.typeKeys(redoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
@@ -84,42 +91,7 @@ describe("history", () => {
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
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",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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("スタイル設定");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
import { then } from "@shellygo/cypress-test-utils/assertable";
|
||||
import MaputnikDriver from "./maputnik-driver";
|
||||
|
||||
describe("keyboard", () => {
|
||||
let { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, given, when, get } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
describe("shortcuts", () => {
|
||||
beforeEach(() => {
|
||||
|
||||
+114
-137
@@ -1,8 +1,9 @@
|
||||
import { then } from "@shellygo/cypress-test-utils/assertable";
|
||||
import { v1 as uuid } from "uuid";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
import MaputnikDriver from "./maputnik-driver";
|
||||
|
||||
describe("layers", () => {
|
||||
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, get, when, should } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
beforeEach(() => {
|
||||
when.setStyle("both");
|
||||
@@ -18,7 +19,7 @@ describe("layers", () => {
|
||||
});
|
||||
|
||||
it("should update layers in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
then(get.maputnikStyleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
@@ -33,7 +34,7 @@ describe("layers", () => {
|
||||
when.click("layer-list-item:" + id + ":delete");
|
||||
});
|
||||
it("should empty layers in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
then(get.maputnikStyleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [],
|
||||
});
|
||||
});
|
||||
@@ -44,7 +45,7 @@ describe("layers", () => {
|
||||
when.click("layer-list-item:" + id + ":copy");
|
||||
});
|
||||
it("should add copy layer in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
then(get.maputnikStyleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id + "-copy",
|
||||
@@ -65,7 +66,7 @@ describe("layers", () => {
|
||||
});
|
||||
|
||||
it("should update visibility to none in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
then(get.maputnikStyleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
@@ -84,7 +85,7 @@ describe("layers", () => {
|
||||
});
|
||||
|
||||
it("should update visibility to visible in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
then(get.maputnikStyleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
@@ -104,14 +105,16 @@ describe("layers", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "background",
|
||||
});
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
describe("modify", () => {
|
||||
@@ -124,14 +127,15 @@ describe("layers", () => {
|
||||
|
||||
when.click("add-layer");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -147,90 +151,83 @@ describe("layers", () => {
|
||||
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
||||
when.click("min-zoom");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "foobar:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
describe("min-zoom", () => {
|
||||
let bgId: string;
|
||||
it("min-zoom", () => {
|
||||
let bgId = createBackground();
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("min-zoom.input-text", "1");
|
||||
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update min-zoom in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
]
|
||||
);
|
||||
|
||||
it("when clicking next layer should update style on local storage", () => {
|
||||
when.type("min-zoom.input-text", "{backspace}");
|
||||
// AND RESET!
|
||||
when.type("{backspace}", "min-zoom.input-text");
|
||||
when.click("max-zoom.input-text");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
describe("max-zoom", () => {
|
||||
let bgId: string;
|
||||
it("max-zoom", () => {
|
||||
let bgId = createBackground();
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("max-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
when.click("layer-editor.layer-id");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
maxzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
describe("comments", () => {
|
||||
let bgId: string;
|
||||
it("comments", () => {
|
||||
let bgId = createBackground();
|
||||
let comment = "42";
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("layer-comment.input", comment);
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
when.click("layer-editor.layer-id");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
@@ -238,68 +235,42 @@ describe("layers", () => {
|
||||
"maputnik:comment": comment,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
]
|
||||
);
|
||||
|
||||
describe("when unsetting", () => {
|
||||
beforeEach(() => {
|
||||
when.clear("layer-comment.input");
|
||||
// Unset it again.
|
||||
when.type("{backspace}{backspace}", "layer-comment.input");
|
||||
when.click("min-zoom.input-text");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
describe("color", () => {
|
||||
let bgId: string;
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
it("color", () => {
|
||||
let bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
when.click("spec-field:background-color");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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", () => {
|
||||
it("expand/collapse");
|
||||
@@ -325,6 +296,7 @@ describe("layers", () => {
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
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();
|
||||
|
||||
when.click(".CodeMirror");
|
||||
@@ -344,15 +316,16 @@ describe("layers", () => {
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "fill",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: Change source
|
||||
@@ -366,15 +339,16 @@ describe("layers", () => {
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("groups", () => {
|
||||
@@ -390,15 +364,16 @@ describe("layers", () => {
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "symbol",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -409,15 +384,16 @@ describe("layers", () => {
|
||||
layer: "raster",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "raster",
|
||||
source: "raster",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -428,15 +404,16 @@ describe("layers", () => {
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "circle",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -447,15 +424,16 @@ describe("layers", () => {
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "fill-extrusion",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -481,17 +459,16 @@ describe("layers", () => {
|
||||
type: "background",
|
||||
});
|
||||
|
||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
|
||||
then(
|
||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||
).shouldNotBeVisible();
|
||||
should.beVisible("layer-list-item:foo");
|
||||
|
||||
should.notBeVisible("layer-list-item:foo_bar");
|
||||
should.notBeVisible("layer-list-item:foo_bar_baz");
|
||||
|
||||
when.click("layer-list-group:foo-0");
|
||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
|
||||
then(
|
||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||
).shouldBeVisible();
|
||||
|
||||
should.beVisible("layer-list-item:foo");
|
||||
should.beVisible("layer-list-item:foo_bar");
|
||||
should.beVisible("layer-list-item:foo_bar_baz");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
import { then } from "@shellygo/cypress-test-utils/assertable";
|
||||
import MaputnikDriver from "./maputnik-driver";
|
||||
|
||||
describe("map", () => {
|
||||
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, get, when } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
describe("zoom level", () => {
|
||||
it("via url", () => {
|
||||
@@ -17,16 +18,10 @@ describe("map", () => {
|
||||
let zoomLevel = 12.37;
|
||||
when.setStyle("geojson", zoomLevel);
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||
when.clickZoomIn();
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
||||
"Zoom: " + (zoomLevel + 1)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("search", () => {
|
||||
it('should exist', () => {
|
||||
then(get.searchControl()).shouldBeVisible();
|
||||
when.clickZoomIn();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,30 +1,10 @@
|
||||
/// <reference types="cypress-plugin-tab" />
|
||||
|
||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
|
||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
||||
import CypressWrapperDriver from "./cypress-wrapper-driver";
|
||||
import ModalDriver from "./modal-driver";
|
||||
|
||||
const baseUrl = "http://localhost:8888/";
|
||||
|
||||
const styleFromWindow = (win: Window) => {
|
||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
||||
const obj = JSON.parse(styleItem || "");
|
||||
return obj;
|
||||
};
|
||||
|
||||
export class MaputnikAssertable<T> extends Assertable<T> {
|
||||
shouldEqualToStoredStyle = () =>
|
||||
then(
|
||||
new CypressHelper().get.window().then((win: Window) => {
|
||||
const style = styleFromWindow(win);
|
||||
then(this.chainable).shouldDeepNestedInclude(style);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export class MaputnikDriver {
|
||||
private helper = new MaputnikCypressHelper();
|
||||
export default class MaputnikDriver {
|
||||
private helper = new CypressWrapperDriver();
|
||||
private modalDriver = new ModalDriver();
|
||||
|
||||
public beforeAndAfter = () => {
|
||||
@@ -34,11 +14,7 @@ export class MaputnikDriver {
|
||||
});
|
||||
};
|
||||
|
||||
public then = (chainable: Cypress.Chainable<any>) =>
|
||||
new MaputnikAssertable(chainable);
|
||||
|
||||
public given = {
|
||||
...this.helper.given,
|
||||
setupMockBackedResponses: () => {
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
@@ -50,7 +26,7 @@ export class MaputnikDriver {
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "example-layer-style.json",
|
||||
url: baseUrl + "/example-layer-style.json",
|
||||
response: {
|
||||
fixture: "example-layer-style.json",
|
||||
},
|
||||
@@ -96,7 +72,7 @@ export class MaputnikDriver {
|
||||
this.helper.when.within(fn, selector);
|
||||
},
|
||||
tab: () => this.helper.get.element("body").tab(),
|
||||
waitForExampleFileResponse: () => {
|
||||
waitForExampleFileRequset: () => {
|
||||
this.helper.when.waitForResponse("example-style.json");
|
||||
},
|
||||
chooseExampleFile: () => {
|
||||
@@ -128,15 +104,13 @@ export class MaputnikDriver {
|
||||
}
|
||||
this.helper.when.visit(baseUrl + url);
|
||||
if (styleProperties) {
|
||||
this.helper.when.acceptConfirm();
|
||||
this.helper.when.confirmAlert();
|
||||
}
|
||||
// when methods should not include assertions
|
||||
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
|
||||
toolbarLink.scrollIntoView();
|
||||
toolbarLink.should("be.visible");
|
||||
this.helper.get.elementByTestId("toolbar:link").should("be.visible");
|
||||
},
|
||||
|
||||
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
|
||||
typeKeys: (keys: string, selector?: string) =>
|
||||
this.helper.get.element("body").type(keys),
|
||||
|
||||
clickZoomIn: () => {
|
||||
this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
|
||||
@@ -169,10 +143,19 @@ export class MaputnikDriver {
|
||||
isMac: () => {
|
||||
return Cypress.platform === "darwin";
|
||||
},
|
||||
styleFromWindow: (win: Window) => {
|
||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
||||
const obj = JSON.parse(styleItem || "");
|
||||
return obj;
|
||||
},
|
||||
|
||||
styleFromLocalStorage: () =>
|
||||
this.helper.get.window().then((win) => styleFromWindow(win)),
|
||||
|
||||
maputnikStyleFromLocalStorage: () => {
|
||||
const styleId = localStorage.getItem("maputnik:latest_style");
|
||||
const styleItem = localStorage.getItem(`maputnik:style:${styleId}`);
|
||||
const obj = JSON.parse(styleItem || "");
|
||||
return cy.wrap(obj);
|
||||
},
|
||||
exampleFileUrl: () => {
|
||||
return baseUrl + "example-style.json";
|
||||
},
|
||||
@@ -181,6 +164,55 @@ export class MaputnikDriver {
|
||||
skipTargetLayerEditor: () =>
|
||||
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
||||
canvas: () => this.helper.get.element("canvas"),
|
||||
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
|
||||
};
|
||||
|
||||
public should = {
|
||||
notExist: (selector: string) => {
|
||||
this.helper.get.element(selector).should("not.exist");
|
||||
},
|
||||
beFocused: (selector: string) => {
|
||||
this.helper.get.elementByTestId(selector).should("have.focus");
|
||||
},
|
||||
|
||||
notBeFocused: (selector: string) => {
|
||||
this.helper.get.elementByTestId(selector).should("not.have.focus");
|
||||
},
|
||||
|
||||
beVisible: (selector: string) => {
|
||||
this.helper.get.elementByTestId(selector).should("be.visible");
|
||||
},
|
||||
|
||||
notBeVisible: (selector: string) => {
|
||||
this.helper.get.elementByTestId(selector).should("not.be.visible");
|
||||
},
|
||||
|
||||
equalStyleStore: (getter: (obj: any) => any, styleObj: any) => {
|
||||
cy.window().then((win: any) => {
|
||||
const obj = this.get.styleFromWindow(win);
|
||||
assert.deepEqual(getter(obj), styleObj);
|
||||
});
|
||||
},
|
||||
|
||||
styleStoreEqualToExampleFileData: () => {
|
||||
cy.window().then((win: any) => {
|
||||
const obj = this.get.styleFromWindow(win);
|
||||
this.helper.given
|
||||
.fixture("example-style.json", "file:example-style.json")
|
||||
.should("deep.equal", obj);
|
||||
});
|
||||
},
|
||||
|
||||
exist: (selector: string) => {
|
||||
this.helper.get.elementByTestId(selector).should("exist");
|
||||
},
|
||||
beSelected: (selector: string, value: string) => {
|
||||
this.helper.get
|
||||
.elementByTestId(selector)
|
||||
.find(`option[value="${value}"]`)
|
||||
.should("be.selected");
|
||||
},
|
||||
containText: (selector: string, text: string) => {
|
||||
this.helper.get.elementByTestId(selector).should("contain.text", text);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { v1 as uuid } from "uuid";
|
||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
||||
import CypressWrapperDriver from "./cypress-wrapper-driver";
|
||||
|
||||
export default class ModalDriver {
|
||||
private helper = new MaputnikCypressHelper();
|
||||
private helper = new CypressWrapperDriver();
|
||||
|
||||
public when = {
|
||||
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
|
||||
// Having logic in test code is an anti pattern.
|
||||
// This should be splitted to multiple single responsibility functions
|
||||
let type = opts.type;
|
||||
let layer = opts.layer;
|
||||
let id;
|
||||
|
||||
+40
-66
@@ -1,9 +1,8 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
import MaputnikDriver from "./maputnik-driver";
|
||||
|
||||
describe("modals", () => {
|
||||
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, when, get, should } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
beforeEach(() => {
|
||||
when.setStyle("");
|
||||
});
|
||||
@@ -12,28 +11,26 @@ describe("modals", () => {
|
||||
when.click("nav:open");
|
||||
});
|
||||
|
||||
it("close", () => {
|
||||
it.only("close", () => {
|
||||
when.modal.close("modal:open");
|
||||
then(get.elementByTestId("modal:open")).shouldNotExist();
|
||||
should.notExist("modal:open");
|
||||
});
|
||||
|
||||
it.skip("upload", () => {
|
||||
// HM: I was not able to make the following choose file actually to select a file and close the modal...
|
||||
when.chooseExampleFile();
|
||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||
|
||||
should.styleStoreEqualToExampleFileData();
|
||||
});
|
||||
|
||||
describe("when click open url", () => {
|
||||
beforeEach(() => {
|
||||
it("load from url", () => {
|
||||
let styleFileUrl = get.exampleFileUrl();
|
||||
|
||||
when.setValue("modal:open.url.input", styleFileUrl);
|
||||
when.click("modal:open.url.button");
|
||||
when.wait(200);
|
||||
});
|
||||
it("load from url", () => {
|
||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||
});
|
||||
when.waitForExampleFileRequset();
|
||||
|
||||
should.styleStoreEqualToExampleFileData();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,7 +39,7 @@ describe("modals", () => {
|
||||
when.setStyle("");
|
||||
when.typeKeys("?");
|
||||
when.modal.close("modal:shortcuts");
|
||||
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
|
||||
should.notExist("modal:shortcuts");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,7 +50,7 @@ describe("modals", () => {
|
||||
|
||||
it("close", () => {
|
||||
when.modal.close("modal:export");
|
||||
then(get.elementByTestId("modal:export")).shouldNotExist();
|
||||
should.notExist("modal:export");
|
||||
});
|
||||
|
||||
// TODO: Work out how to download a file and check the contents
|
||||
@@ -68,9 +65,9 @@ describe("modals", () => {
|
||||
|
||||
describe("inspect", () => {
|
||||
it("toggle", () => {
|
||||
// There is no assertion in this test
|
||||
when.setStyle("geojson");
|
||||
when.select("maputnik-select", "inspect");
|
||||
|
||||
when.selectWithin("nav:inspect", "inspect");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,59 +76,37 @@ describe("modals", () => {
|
||||
when.click("nav:settings");
|
||||
});
|
||||
|
||||
describe("when click name filed spec information", () => {
|
||||
beforeEach(() => {
|
||||
it("name", () => {
|
||||
when.click("field-doc-button-Name");
|
||||
});
|
||||
|
||||
it("should show the spec information", () => {
|
||||
then(get.elementsText("spec-field-doc")).shouldInclude(
|
||||
"name for the style"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when set name and click owner", () => {
|
||||
beforeEach(() => {
|
||||
when.setValue("modal:settings.name", "foobar");
|
||||
when.click("modal:settings.owner");
|
||||
when.wait(200);
|
||||
should.containText("spec-field-doc", "name for the style");
|
||||
});
|
||||
|
||||
it("show name specifications", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
name: "foobar",
|
||||
});
|
||||
});
|
||||
when.setValue("modal:settings.name", "foobar");
|
||||
when.click("modal:settings.owner");
|
||||
|
||||
should.equalStyleStore((obj) => obj.name, "foobar");
|
||||
});
|
||||
|
||||
describe("when set owner and click name", () => {
|
||||
beforeEach(() => {
|
||||
it("owner", () => {
|
||||
when.setValue("modal:settings.owner", "foobar");
|
||||
when.click("modal:settings.name");
|
||||
when.wait(200);
|
||||
});
|
||||
it("should update owner in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
owner: "foobar",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
should.equalStyleStore((obj) => obj.owner, "foobar");
|
||||
});
|
||||
it("sprite url", () => {
|
||||
when.setValue("modal:settings.sprite", "http://example.com");
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
sprite: "http://example.com",
|
||||
});
|
||||
|
||||
should.equalStyleStore((obj) => obj.sprite, "http://example.com");
|
||||
});
|
||||
it("glyphs url", () => {
|
||||
let glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
||||
when.setValue("modal:settings.glyphs", glyphsUrl);
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
glyphs: glyphsUrl,
|
||||
});
|
||||
|
||||
should.equalStyleStore((obj) => obj.glyphs, glyphsUrl);
|
||||
});
|
||||
|
||||
it("maptiler access token", () => {
|
||||
@@ -141,11 +116,11 @@ describe("modals", () => {
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
).shouldInclude({
|
||||
"maputnik:openmaptiles_access_token": apiKey,
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(obj) => obj.metadata["maputnik:openmaptiles_access_token"],
|
||||
apiKey
|
||||
);
|
||||
});
|
||||
|
||||
it("thunderforest access token", () => {
|
||||
@@ -155,22 +130,21 @@ describe("modals", () => {
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
|
||||
|
||||
should.equalStyleStore(
|
||||
(obj) => obj.metadata["maputnik:thunderforest_access_token"],
|
||||
apiKey
|
||||
);
|
||||
});
|
||||
|
||||
it("style renderer", () => {
|
||||
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
|
||||
when.select("modal:settings.maputnik:renderer", "ol");
|
||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
||||
"ol"
|
||||
);
|
||||
should.beSelected("modal:settings.maputnik:renderer", "ol");
|
||||
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
metadata: { "maputnik:renderer": "ol" },
|
||||
});
|
||||
|
||||
should.equalStyleStore((obj) => obj.metadata["maputnik:renderer"], "ol");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -1,72 +0,0 @@
|
||||
# Maputnik Desktop [][github-action-ci]
|
||||
---
|
||||
|
||||
A Golang based cross platform executable for integrating Maputnik locally.
|
||||
This binary packages up the JavaScript and CSS bundle produced by maputnik
|
||||
and embeds it in the program for easy distribution. It also allows
|
||||
exposing a local style file and work on it both in Maputnik and with your favorite
|
||||
editor.
|
||||
|
||||
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
|
||||
|
||||
## Install
|
||||
|
||||
You can download a single binary for Linux, OSX or Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/editor/releases/latest).
|
||||
|
||||
### Usage
|
||||
|
||||
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
|
||||
|
||||
```bash
|
||||
maputnik
|
||||
```
|
||||
|
||||
Expose a local style file to Maputnik allowing the web based editor
|
||||
to save to the local filesystem.
|
||||
|
||||
```bash
|
||||
maputnik --file basic-v9.json
|
||||
```
|
||||
|
||||
Watch the local style for changes and inform the editor via web socket.
|
||||
This makes it possible to edit the style with a local text editor and still
|
||||
use Maputnik.
|
||||
|
||||
```bash
|
||||
maputnik --watch --file basic-v9.json
|
||||
```
|
||||
|
||||
Choose a local port to listen on, instead of using the default port 8000.
|
||||
|
||||
```bash
|
||||
maputnik --port 8001
|
||||
```
|
||||
|
||||
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
|
||||
Could be used to serve sprites and glyphs.
|
||||
|
||||
```bash
|
||||
maputnik --static ./localFolder
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
`maputnik` exposes the configured styles via a HTTP API.
|
||||
|
||||
| Method | Description
|
||||
|---------------------------------|---------------------------------------
|
||||
| `GET /styles` | List the ID of all configured style files
|
||||
| `GET /styles/{filename}` | Get contents of a single style file
|
||||
| `PUT /styles/{filename}` | Update contents of a style file
|
||||
| `WEBSOCKET /ws` | Listen to change events for the configured style files
|
||||
|
||||
### Build
|
||||
|
||||
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build-desktop
|
||||
```
|
||||
|
||||
You should now find the `maputnik` binary in your `bin` directory.
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 |
Generated
+10756
-3321
File diff suppressed because it is too large
Load Diff
+60
-60
@@ -1,49 +1,41 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "2.1.0",
|
||||
"version": "2.0.0-pre.2",
|
||||
"description": "A MapLibre GL visual style editor",
|
||||
"type": "module",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build --base=/maputnik/",
|
||||
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
|
||||
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"test": "cypress run",
|
||||
"cy:open": "cypress open",
|
||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
||||
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
|
||||
"storybook": "storybook dev -h 0.0.0.0 -p 6006",
|
||||
"build-storybook": "storybook build -o build/storybook"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maplibre/maputnik"
|
||||
"url": "https://github.com/maputnik/editor"
|
||||
},
|
||||
"author": "Lukas Martinelli",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"dependencies": {
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||
"@maplibre/maplibre-gl-geocoder": "^1.6.0",
|
||||
"@maplibre/maplibre-gl-inspect": "^1.6.3",
|
||||
"@maplibre/maplibre-gl-style-spec": "^20.1.1",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.3.1",
|
||||
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
|
||||
"@mdi/js": "^6.6.96",
|
||||
"@mdi/react": "^1.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"array-move": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
"color": "^4.2.3",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"detect-browser": "^5.3.0",
|
||||
"events": "^3.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^23.12.2",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"json-stringify-pretty-compact": "^3.0.0",
|
||||
"json-to-ast": "^2.1.0",
|
||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -53,30 +45,30 @@
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"maplibre-gl": "^4.1.2",
|
||||
"mapbox-gl-inspect": "^1.3.1",
|
||||
"maplibre-gl": "^2.4.0",
|
||||
"maputnik-design": "github:maputnik/design#172b06c",
|
||||
"ol": "^6.14.1",
|
||||
"ol-mapbox-style": "^7.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-accessible-accordion": "^5.0.0",
|
||||
"react": "^16.0.0",
|
||||
"react-accessible-accordion": "^4.0.0",
|
||||
"react-aria-menubutton": "^7.0.3",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"react-aria-modal": "^4.0.1",
|
||||
"react-autobind": "^1.0.6",
|
||||
"react-autocomplete": "^1.8.1",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-file-reader-input": "^2.0.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-icon-base": "^2.1.2",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"sass": "^1.72.0",
|
||||
"slugify": "^1.6.6",
|
||||
"sass": "^1.50.0",
|
||||
"slugify": "^1.6.5",
|
||||
"string-hash": "^1.1.3",
|
||||
"url": "^0.11.3"
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
@@ -96,15 +88,24 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/code-coverage": "^3.12.30",
|
||||
"@cypress/code-coverage": "^3.12.15",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@shellygo/cypress-test-utils": "^2.1.9",
|
||||
"@shellygo/cypress-test-utils": "^2.0.14",
|
||||
"@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/color": "^3.0.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/geojson": "^7946.0.14",
|
||||
"@types/geojson": "^7946.0.13",
|
||||
"@types/json-to-ast": "^2.1.4",
|
||||
"@types/lodash.capitalize": "^4.2.9",
|
||||
"@types/lodash.clamp": "^4.0.9",
|
||||
@@ -112,38 +113,37 @@
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/randomcolor": "^0.5.9",
|
||||
"@types/react": "^18.2.67",
|
||||
"@types/react-aria-menubutton": "^6.2.14",
|
||||
"@types/react-aria-modal": "^4.0.10",
|
||||
"@types/react-autocomplete": "^1.8.10",
|
||||
"@types/react": "^16.14.52",
|
||||
"@types/react-aria-menubutton": "^6.2.13",
|
||||
"@types/react-aria-modal": "^4.0.9",
|
||||
"@types/react-autocomplete": "^1.8.9",
|
||||
"@types/react-collapse": "^5.0.4",
|
||||
"@types/react-color": "^3.0.12",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/react-color": "^3.0.10",
|
||||
"@types/react-dom": "^16.9.24",
|
||||
"@types/react-file-reader-input": "^2.0.4",
|
||||
"@types/react-icon-base": "^2.1.6",
|
||||
"@types/string-hash": "^1.1.3",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"cypress": "^13.13.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"cypress": "^13.6.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"i18next-parser": "^9.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"express": "^4.17.3",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"mocha": "^10.3.0",
|
||||
"postcss": "^8.4.38",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"stylelint": "^16.2.1",
|
||||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-scss": "^6.2.1",
|
||||
"typescript": "^5.4.3",
|
||||
"uuid": "^9.0.1",
|
||||
"vite": "^5.2.6",
|
||||
"vite-plugin-istanbul": "^6.0.0"
|
||||
"istanbul-lib-coverage": "^3.2.0",
|
||||
"mocha": "^9.2.2",
|
||||
"postcss": "^8.4.12",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"storybook": "^7.6.5",
|
||||
"stylelint": "^14.6.1",
|
||||
"stylelint-config-recommended-scss": "^6.0.0",
|
||||
"stylelint-scss": "^4.2.0",
|
||||
"typescript": "^5.3.3",
|
||||
"uuid": "^8.3.2",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-istanbul": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -9,7 +9,7 @@ import {unset} from 'lodash'
|
||||
import {arrayMoveMutable} from 'array-move'
|
||||
import hash from "string-hash";
|
||||
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
|
||||
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
|
||||
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
|
||||
|
||||
import MapMaplibreGl from './MapMaplibreGl'
|
||||
import MapOpenLayers from './MapOpenLayers'
|
||||
@@ -24,6 +24,7 @@ import ModalExport from './ModalExport'
|
||||
import ModalSources from './ModalSources'
|
||||
import ModalOpen from './ModalOpen'
|
||||
import ModalShortcuts from './ModalShortcuts'
|
||||
import ModalSurvey from './ModalSurvey'
|
||||
import ModalDebug from './ModalDebug'
|
||||
|
||||
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
||||
@@ -127,6 +128,7 @@ type AppState = {
|
||||
open: boolean
|
||||
shortcuts: boolean
|
||||
export: boolean
|
||||
survey: boolean
|
||||
debug: boolean
|
||||
}
|
||||
}
|
||||
@@ -135,6 +137,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
revisionStore: RevisionStore;
|
||||
styleStore: StyleStore | ApiStyleStore;
|
||||
layerWatcher: LayerWatcher;
|
||||
shortcutEl: ModalShortcuts | null = null;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
@@ -274,6 +277,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
|
||||
survey: false,
|
||||
debug: false,
|
||||
},
|
||||
maplibreGlDebugOptions: {
|
||||
@@ -375,7 +379,8 @@ export default class App extends React.Component<any, AppState> {
|
||||
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
|
||||
// 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) {
|
||||
if(modalName === 'survey' && value === false) {
|
||||
localStorage.setItem('survey', '');
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
@@ -934,6 +943,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
mapView={this.state.mapView}
|
||||
/>
|
||||
<ModalShortcuts
|
||||
ref={(el) => this.shortcutEl = el}
|
||||
isOpen={this.state.isOpen.shortcuts}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||
/>
|
||||
@@ -961,6 +971,10 @@ export default class App extends React.Component<any, AppState> {
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
/>
|
||||
<ModalSurvey
|
||||
isOpen={this.state.isOpen.survey}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'survey')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
return <AppLayout
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type AppLayoutInternalProps = {
|
||||
type AppLayoutProps = {
|
||||
toolbar: React.ReactElement
|
||||
layerList: React.ReactElement
|
||||
layerEditor?: React.ReactElement
|
||||
map: React.ReactElement
|
||||
bottom?: React.ReactElement
|
||||
modals?: React.ReactNode
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
class AppLayout extends React.Component<AppLayoutProps> {
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
@@ -24,11 +23,8 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
document.body.dir = this.props.i18n.dir();
|
||||
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div className="maputnik-layout-main">
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
</div>
|
||||
@@ -38,7 +34,6 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{this.props.map}
|
||||
</div>
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
@@ -48,5 +43,4 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const AppLayout = withTranslation()(AppLayoutInternal);
|
||||
export default AppLayout;
|
||||
export default AppLayout
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import React from 'react'
|
||||
import {formatLayerId} from '../libs/format';
|
||||
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type AppMessagePanelInternalProps = {
|
||||
type AppMessagePanelProps = {
|
||||
errors?: unknown[]
|
||||
infos?: string[]
|
||||
infos?: unknown[]
|
||||
mapStyle?: StyleSpecification
|
||||
onLayerSelect?(...args: unknown[]): unknown
|
||||
currentLayer?: LayerSpecification
|
||||
selectedLayerIndex?: number
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
||||
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {t, selectedLayerIndex} = this.props;
|
||||
const {selectedLayerIndex} = this.props;
|
||||
const errors = this.props.errors?.map((error: any, idx) => {
|
||||
let content;
|
||||
if (error.parsed && error.parsed.type === "layer") {
|
||||
@@ -26,9 +25,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
||||
content = (
|
||||
<>
|
||||
<Trans t={t}>
|
||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
</Trans>
|
||||
{selectedLayerIndex !== parsed.data.index &&
|
||||
<>
|
||||
—
|
||||
@@ -36,7 +33,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
||||
className="maputnik-message-panel__switch-button"
|
||||
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
||||
>
|
||||
{t("switch to layer")}
|
||||
switch to layer
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
@@ -62,5 +59,3 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
||||
}
|
||||
}
|
||||
|
||||
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
|
||||
export default AppMessagePanel;
|
||||
|
||||
@@ -2,12 +2,9 @@ import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {detect} from 'detect-browser';
|
||||
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdLanguage} from 'react-icons/md'
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
|
||||
import pkgJson from '../../package.json'
|
||||
//@ts-ignore
|
||||
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
||||
import { withTranslation, WithTranslation } from 'react-i18next';
|
||||
import { supportedLanguages } from '../i18n';
|
||||
|
||||
|
||||
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
||||
const browser = detect();
|
||||
@@ -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 = {
|
||||
children?: React.ReactNode
|
||||
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";
|
||||
|
||||
type AppToolbarInternalProps = {
|
||||
type AppToolbarProps = {
|
||||
mapStyle: object
|
||||
inspectModeEnabled: boolean
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
@@ -95,9 +115,9 @@ type AppToolbarInternalProps = {
|
||||
onSetMapState(mapState: MapState): unknown
|
||||
mapState?: MapState
|
||||
renderer?: string
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
state = {
|
||||
isOpen: {
|
||||
settings: false,
|
||||
@@ -112,10 +132,6 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
this.props.onSetMapState(val);
|
||||
}
|
||||
|
||||
handleLanguageChange(val: string) {
|
||||
this.props.i18n.changeLanguage(val);
|
||||
}
|
||||
|
||||
onSkip = (target: string) => {
|
||||
if (target === "map") {
|
||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||
@@ -127,41 +143,40 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const views = [
|
||||
{
|
||||
id: "map",
|
||||
group: "general",
|
||||
title: t("Map"),
|
||||
title: "Map",
|
||||
},
|
||||
{
|
||||
id: "inspect",
|
||||
group: "general",
|
||||
title: t("Inspect"),
|
||||
title: "Inspect",
|
||||
disabled: this.props.renderer === 'ol',
|
||||
},
|
||||
{
|
||||
id: "filter-deuteranopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Deuteranopia filter"),
|
||||
title: "Deuteranopia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-protanopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Protanopia filter"),
|
||||
title: "Protanopia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-tritanopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Tritanopia filter"),
|
||||
title: "Tritanopia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-achromatopsia",
|
||||
group: "color-accessibility",
|
||||
title: t("Achromatopsia filter"),
|
||||
title: "Achromatopsia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
];
|
||||
@@ -181,29 +196,29 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-list")}
|
||||
>
|
||||
{t("Layers list")}
|
||||
Layers list
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:layer-editor"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-editor")}
|
||||
>
|
||||
{t("Layer editor")}
|
||||
Layer editor
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:map-view"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("map")}
|
||||
>
|
||||
{t("Map view")}
|
||||
Map view
|
||||
</button>
|
||||
<a
|
||||
className="maputnik-toolbar-logo"
|
||||
target="blank"
|
||||
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>
|
||||
<span className="maputnik-toolbar-name">{pkgJson.name}</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">
|
||||
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||
<MdOpenInBrowser />
|
||||
<IconText>{t("Open")}</IconText>
|
||||
<IconText>Open</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>{t("Export")}</IconText>
|
||||
<IconText>Export</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||
<MdLayers />
|
||||
<IconText>{t("Data Sources")}</IconText>
|
||||
<IconText>Data Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<MdSettings />
|
||||
<IconText>{t("Style Settings")}</IconText>
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
|
||||
<ToolbarSelect wdKey="nav:inspect">
|
||||
<MdFindInPage />
|
||||
<label>{t("View")}
|
||||
<label>View
|
||||
<select
|
||||
className="maputnik-select"
|
||||
data-wd-key="maputnik-select"
|
||||
@@ -244,7 +259,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
<optgroup label={t("Color accessibility")}>
|
||||
<optgroup label="Color accessibility">
|
||||
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||
@@ -257,35 +272,16 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
</label>
|
||||
</ToolbarSelect>
|
||||
|
||||
<ToolbarSelect wdKey="nav:language">
|
||||
<MdLanguage />
|
||||
<label>{t("Language")}
|
||||
<select
|
||||
className="maputnik-select"
|
||||
data-wd-key="maputnik-lang-select"
|
||||
onChange={(e) => this.handleLanguageChange(e.target.value)}
|
||||
value={this.props.i18n.language}
|
||||
>
|
||||
{Object.entries(supportedLanguages).map(([code, name]) => {
|
||||
return (
|
||||
<option key={code} value={code}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
</ToolbarSelect>
|
||||
|
||||
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
|
||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||
<MdHelpOutline />
|
||||
<IconText>{t("Help")}</IconText>
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
||||
<MdAssignmentTurnedIn />
|
||||
<IconText>Take the Maputnik Survey</IconText>
|
||||
</ToolbarLinkHighlighted>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
const AppToolbar = withTranslation()(AppToolbarInternal);
|
||||
export default AppToolbar;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, {PropsWithChildren, SyntheticEvent} from 'react'
|
||||
import React, {SyntheticEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
|
||||
|
||||
type BlockProps = PropsWithChildren & {
|
||||
type BlockProps = {
|
||||
"data-wd-key"?: string
|
||||
label?: string
|
||||
action?: React.ReactElement
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class Doc extends React.Component<DocProps> {
|
||||
const renderValues = (
|
||||
!!values &&
|
||||
// HACK: Currently we merge additional values into the style spec, so this is required
|
||||
// See <https://github.com/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)
|
||||
);
|
||||
|
||||
|
||||
@@ -2,23 +2,21 @@ import React from 'react'
|
||||
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldCommentInternalProps = {
|
||||
type FieldCommentProps = {
|
||||
value?: string
|
||||
onChange(value: string | undefined): unknown
|
||||
error: {message: string}
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
|
||||
export default class FieldComment extends React.Component<FieldCommentProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const fieldSpec = {
|
||||
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
|
||||
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||
};
|
||||
|
||||
return <Block
|
||||
label={t("Comments")}
|
||||
label={"Comments"}
|
||||
fieldSpec={fieldSpec}
|
||||
data-wd-key="layer-comment"
|
||||
error={this.props.error}
|
||||
@@ -27,12 +25,9 @@ class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
|
||||
multi={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
default={t("Comment...")}
|
||||
default="Comment..."
|
||||
data-wd-key="layer-comment.input"
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldComment = withTranslation()(FieldCommentInternal);
|
||||
export default FieldComment;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||
|
||||
type FieldDocLabelProps = {
|
||||
label: JSX.Element | string | undefined
|
||||
label: object | string | undefined
|
||||
fieldSpec?: {
|
||||
doc?: string
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
|
||||
static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
|
||||
// Because otherwise when editing values we end up accidentally changing field type.
|
||||
if (state.isEditing) {
|
||||
return {};
|
||||
@@ -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}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 InputString from './InputString'
|
||||
|
||||
@@ -13,8 +13,7 @@ type FieldIdProps = {
|
||||
|
||||
export default class FieldId extends React.Component<FieldIdProps> {
|
||||
render() {
|
||||
return <Block label="ID" fieldSpec={latest.layer.id}
|
||||
|
||||
return <Block label={"ID"} fieldSpec={latest.layer.id}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
>
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
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 InputNumber from './InputNumber'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldMaxZoomInternalProps = {
|
||||
type FieldMaxZoomProps = {
|
||||
value?: number
|
||||
onChange(value: number | undefined): unknown
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
|
||||
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
|
||||
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||
error={this.props.error}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
@@ -30,6 +28,3 @@ class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
|
||||
export default FieldMaxZoom;
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
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 InputNumber from './InputNumber'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldMinZoomInternalProps = {
|
||||
type FieldMinZoomProps = {
|
||||
value?: number
|
||||
onChange(...args: unknown[]): unknown
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
|
||||
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
|
||||
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||
error={this.props.error}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
@@ -30,6 +28,3 @@ class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
||||
export default FieldMinZoom;
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
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 InputAutocomplete from './InputAutocomplete'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldSourceInternalProps = {
|
||||
type FieldSourceProps = {
|
||||
value?: string
|
||||
wdKey?: string
|
||||
onChange?(value: string| undefined): unknown
|
||||
sourceIds?: unknown[]
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
|
||||
export default class FieldSource extends React.Component<FieldSourceProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block
|
||||
label={t("Source")}
|
||||
label={"Source"}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={this.props.error}
|
||||
data-wd-key={this.props.wdKey}
|
||||
@@ -35,6 +33,3 @@ class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||
export default FieldSource;
|
||||
|
||||
@@ -3,17 +3,16 @@ import React from 'react'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldSourceLayerInternalProps = {
|
||||
type FieldSourceLayerProps = {
|
||||
value?: string
|
||||
onChange?(...args: unknown[]): unknown
|
||||
sourceLayerIds?: unknown[]
|
||||
isFixed?: boolean
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
|
||||
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
@@ -21,9 +20,8 @@ class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalP
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block
|
||||
label={t("Source Layer")}
|
||||
label={"Source Layer"}
|
||||
fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={this.props.error}
|
||||
@@ -37,6 +35,3 @@ class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalP
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||
export default FieldSourceLayer;
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
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 InputSelect from './InputSelect'
|
||||
import InputString from './InputString'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldTypeInternalProps = {
|
||||
type FieldTypeProps = {
|
||||
value: string
|
||||
wdKey?: string
|
||||
onChange(value: string): unknown
|
||||
error?: {message: string}
|
||||
disabled?: boolean
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
||||
export default class FieldType extends React.Component<FieldTypeProps> {
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Type")} fieldSpec={latest.layer.type}
|
||||
return <Block label={"Type"} fieldSpec={latest.layer.type}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
>
|
||||
@@ -52,6 +50,3 @@ class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldType = withTranslation()(FieldTypeInternal);
|
||||
export default FieldType;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { PropsWithChildren, ReactElement } from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
|
||||
type FieldsetProps = PropsWithChildren & {
|
||||
type FieldsetProps = {
|
||||
label?: string,
|
||||
fieldSpec?: { doc?: string },
|
||||
action?: ReactElement,
|
||||
|
||||
@@ -13,10 +13,9 @@ import FilterEditorBlock from './FilterEditorBlock'
|
||||
import InputButton from './InputButton'
|
||||
import Doc from './Doc'
|
||||
import ExpressionProperty from './_ExpressionProperty';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
const filter = props.filter || ['all'];
|
||||
|
||||
if (!Array.isArray(filter)) {
|
||||
@@ -48,7 +47,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
|
||||
"sources": {
|
||||
"tmp": {
|
||||
"type": "geojson",
|
||||
"data": ''
|
||||
"data": {}
|
||||
}
|
||||
},
|
||||
"sprite": "",
|
||||
@@ -90,13 +89,13 @@ function hasNestedCombiningFilter(filter: LegacyFilterSpecification | Expression
|
||||
return false
|
||||
}
|
||||
|
||||
type FilterEditorInternalProps = {
|
||||
type FilterEditorProps = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties?: {[key:string]: any}
|
||||
filter?: any[]
|
||||
errors?: {[key:string]: any}
|
||||
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
type FilterEditorState = {
|
||||
showDoc: boolean
|
||||
@@ -104,12 +103,12 @@ type FilterEditorState = {
|
||||
valueIsSimpleFilter?: boolean
|
||||
};
|
||||
|
||||
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
|
||||
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
|
||||
static defaultProps = {
|
||||
filter: ["all"],
|
||||
}
|
||||
|
||||
constructor (props: FilterEditorInternalProps) {
|
||||
constructor (props: FilterEditorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
@@ -156,17 +155,17 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
})
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
|
||||
static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
|
||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||
|
||||
// Upgrade but never downgrade
|
||||
if (!displaySimpleFilter && state.displaySimpleFilter === true) {
|
||||
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
|
||||
return {
|
||||
displaySimpleFilter: false,
|
||||
valueIsSimpleFilter: false,
|
||||
};
|
||||
}
|
||||
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
|
||||
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
|
||||
return {
|
||||
valueIsSimpleFilter: true,
|
||||
}
|
||||
@@ -179,7 +178,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
}
|
||||
|
||||
render() {
|
||||
const {errors, t} = this.props;
|
||||
const {errors} = this.props;
|
||||
const {displaySimpleFilter} = this.state;
|
||||
const fieldSpec={
|
||||
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
||||
@@ -191,16 +190,16 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
if (isNestedCombiningFilter) {
|
||||
return <div className="maputnik-filter-editor-unsupported">
|
||||
<p>
|
||||
{t("Nested filters are not supported.")}
|
||||
Nested filters are not supported.
|
||||
</p>
|
||||
<InputButton
|
||||
onClick={this.makeExpression}
|
||||
title={t("Convert to expression")}
|
||||
title="Convert to expression"
|
||||
>
|
||||
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg>
|
||||
{t("Upgrade to expression")}
|
||||
Upgrade to expression
|
||||
</InputButton>
|
||||
</div>
|
||||
}
|
||||
@@ -213,7 +212,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
<div>
|
||||
<InputButton
|
||||
onClick={this.makeExpression}
|
||||
title={t("Convert to expression")}
|
||||
title="Convert to expression"
|
||||
className="maputnik-make-zoom-function"
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
@@ -248,17 +247,13 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
<Block
|
||||
key="top"
|
||||
fieldSpec={fieldSpec}
|
||||
label={t("Filter")}
|
||||
label={"Filter"}
|
||||
action={actions}
|
||||
>
|
||||
<InputSelect
|
||||
value={combiningOp}
|
||||
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
|
||||
options={[
|
||||
["all", t("every filter matches")],
|
||||
["none", t("no filter matches")],
|
||||
["any", t("any filter matches")]
|
||||
]}
|
||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||
/>
|
||||
</Block>
|
||||
{editorBlocks}
|
||||
@@ -273,7 +268,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> {t("Add filter")}
|
||||
</svg> Add filter
|
||||
</InputButton>
|
||||
</div>
|
||||
<div
|
||||
@@ -304,13 +299,12 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
/>
|
||||
{this.state.valueIsSimpleFilter &&
|
||||
<div className="maputnik-expr-infobox">
|
||||
{t("You've entered an old style filter.")}
|
||||
{' '}
|
||||
You've entered a old style filter,{' '}
|
||||
<button
|
||||
onClick={this.makeFilter}
|
||||
className="maputnik-expr-infobox__button"
|
||||
>
|
||||
{t("Switch to filter editor.")}
|
||||
switch to filter editor
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -319,6 +313,3 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FilterEditor = withTranslation()(FilterEditorInternal);
|
||||
export default FilterEditor;
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
import React from 'react'
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FilterEditorBlockInternalProps = PropsWithChildren & {
|
||||
type FilterEditorBlockProps = {
|
||||
onDelete(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> {
|
||||
export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div className="maputnik-filter-editor-block">
|
||||
<div className="maputnik-filter-editor-block-action">
|
||||
<InputButton
|
||||
className="maputnik-delete-filter"
|
||||
onClick={this.props.onDelete}
|
||||
title={t("Delete filter block")}
|
||||
title="Delete filter block"
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
@@ -27,5 +25,3 @@ class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInterna
|
||||
}
|
||||
}
|
||||
|
||||
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
|
||||
export default FilterEditorBlock;
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) {
|
||||
static getDerivedStateFromProps(props: FieldArrayProps, state: FieldArrayState) {
|
||||
const value: any[] = [];
|
||||
const initialPropsValue = state.initialPropsValue.slice(0);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import InputString from './InputString'
|
||||
import InputNumber from './InputNumber'
|
||||
@@ -22,11 +21,10 @@ export type FieldDynamicArrayProps = {
|
||||
}
|
||||
'aria-label'?: string
|
||||
label: string
|
||||
}
|
||||
};
|
||||
|
||||
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation;
|
||||
|
||||
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> {
|
||||
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||
changeValue(idx: number, newValue: string | number | undefined) {
|
||||
const values = this.values.slice(0)
|
||||
values[idx] = newValue
|
||||
@@ -64,13 +62,8 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const i18nProps = { t, i18n: this.props.i18n, tReady: this.props.tReady };
|
||||
const inputs = this.values.map((v, i) => {
|
||||
const deleteValueBtn= <DeleteValueInputButton
|
||||
onClick={this.deleteValue.bind(this, i)}
|
||||
{...i18nProps}
|
||||
/>;
|
||||
const deleteValueBtn= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
|
||||
let input;
|
||||
if(this.props.type === 'url') {
|
||||
input = <InputUrl
|
||||
@@ -124,27 +117,23 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
|
||||
className="maputnik-array-add-value"
|
||||
onClick={this.addValue}
|
||||
>
|
||||
{t("Add value")}
|
||||
Add value
|
||||
</InputButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
|
||||
export default FieldDynamicArray;
|
||||
|
||||
type DeleteValueInputButtonProps = {
|
||||
onClick?(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <InputButton
|
||||
className="maputnik-delete-stop"
|
||||
onClick={this.props.onClick}
|
||||
title={t("Remove array item")}
|
||||
title="Remove array item"
|
||||
>
|
||||
<FieldDocLabel
|
||||
label={<MdDelete />}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames';
|
||||
import CodeMirror, { ModeSpec } from 'codemirror';
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/addon/lint/lint'
|
||||
@@ -28,7 +27,6 @@ export type InputJsonProps = {
|
||||
mode?: ModeSpec<any>
|
||||
lint?: boolean | object
|
||||
};
|
||||
type InputJsonInternalProps = InputJsonProps & WithTranslation;
|
||||
|
||||
type InputJsonState = {
|
||||
isEditing: boolean
|
||||
@@ -36,7 +34,7 @@ type InputJsonState = {
|
||||
prevValue: string
|
||||
};
|
||||
|
||||
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
|
||||
export default class InputJson extends React.Component<InputJsonProps, InputJsonState> {
|
||||
static defaultProps = {
|
||||
lineNumbers: true,
|
||||
lineWrapping: false,
|
||||
@@ -54,7 +52,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
||||
_el: HTMLDivElement | null = null;
|
||||
_cancelNextChange: boolean = false;
|
||||
|
||||
constructor(props: InputJsonInternalProps) {
|
||||
constructor(props: InputJsonProps) {
|
||||
super(props);
|
||||
this._keyEvent = "keyboard";
|
||||
this.state = {
|
||||
@@ -158,7 +156,6 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const {showMessage} = this.state;
|
||||
const style = {} as {maxHeight?: number};
|
||||
if (this.props.maxHeight) {
|
||||
@@ -167,9 +164,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
||||
|
||||
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
|
||||
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
|
||||
<Trans t={t}>
|
||||
Press <kbd>ESC</kbd> to lose focus
|
||||
</Trans>
|
||||
</div>
|
||||
<div
|
||||
className={classnames("codemirror-container", this.props.className)}
|
||||
@@ -179,6 +174,3 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const InputJson = withTranslation()(InputJsonInternal);
|
||||
export default InputJson;
|
||||
|
||||
@@ -19,10 +19,7 @@ type InputNumberState = {
|
||||
editing: boolean
|
||||
editingRange?: boolean
|
||||
value?: number
|
||||
/**
|
||||
* This is the value that is currently being edited. It can be an invalid value.
|
||||
*/
|
||||
dirtyValue?: number | string | undefined
|
||||
dirtyValue?: number
|
||||
}
|
||||
|
||||
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
|
||||
@@ -41,7 +38,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
|
||||
static getDerivedStateFromProps(props: InputNumberProps, state: InputNumberState) {
|
||||
if (!state.editing && props.value !== state.value) {
|
||||
return {
|
||||
value: props.value,
|
||||
@@ -69,7 +66,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dirtyValue: newValue === "" ? undefined : newValue,
|
||||
dirtyValue: newValue === "" ? undefined : value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -128,7 +125,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
// for example we might go from 13 to 13.23, however because we know
|
||||
// that came from a keyboard event we always want to increase by a
|
||||
// single step value.
|
||||
if (value < +this.state.dirtyValue!) {
|
||||
if (value < this.state.dirtyValue!) {
|
||||
value = this.state.value! - step;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -49,7 +49,6 @@ export default class SpecField extends React.Component<SpecFieldProps> {
|
||||
value: this.props.value,
|
||||
default: this.props.fieldSpec?.default,
|
||||
name: this.props.fieldName,
|
||||
"data-wd-key": "spec-field-input:" + this.props.fieldName,
|
||||
onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
|
||||
'aria-label': this.props['aria-label'],
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class InputString extends React.Component<InputStringProps, Input
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
|
||||
static getDerivedStateFromProps(props: InputStringProps, state: InputStringState) {
|
||||
if (!state.editing) {
|
||||
return {
|
||||
value: props.value
|
||||
|
||||
+13
-24
@@ -1,10 +1,9 @@
|
||||
import React from 'react'
|
||||
import InputString from './InputString'
|
||||
import SmallError from './SmallError'
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
function validate(url: string, t: TFunction): JSX.Element | undefined {
|
||||
|
||||
function validate(url: string) {
|
||||
if (url === "") {
|
||||
return;
|
||||
}
|
||||
@@ -23,19 +22,15 @@ function validate(url: string, t: TFunction): JSX.Element | undefined {
|
||||
const isSsl = window.location.protocol === "https:";
|
||||
|
||||
if (!protocol) {
|
||||
if (isSsl) {
|
||||
error = (
|
||||
<SmallError>
|
||||
<Trans t={t}>Must provide protocol: <code>https://</code></Trans>
|
||||
</SmallError>
|
||||
);
|
||||
} else {
|
||||
error = (
|
||||
<SmallError>
|
||||
<Trans t={t}>Must provide protocol: <code>http://</code> or <code>https://</code></Trans>
|
||||
</SmallError>
|
||||
);
|
||||
Must provide protocol {
|
||||
isSsl
|
||||
? <code>https://</code>
|
||||
: <><code>http://</code> or <code>https://</code></>
|
||||
}
|
||||
</SmallError>
|
||||
);
|
||||
}
|
||||
else if (
|
||||
protocol &&
|
||||
@@ -44,9 +39,7 @@ function validate(url: string, t: TFunction): JSX.Element | undefined {
|
||||
) {
|
||||
error = (
|
||||
<SmallError>
|
||||
<Trans t={t}>
|
||||
CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain
|
||||
</Trans>
|
||||
</SmallError>
|
||||
);
|
||||
}
|
||||
@@ -68,34 +61,32 @@ export type FieldUrlProps = {
|
||||
className?: string
|
||||
};
|
||||
|
||||
type FieldUrlInternalProps = FieldUrlProps & WithTranslation;
|
||||
|
||||
type FieldUrlState = {
|
||||
error?: React.ReactNode
|
||||
}
|
||||
|
||||
class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlState> {
|
||||
export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlState> {
|
||||
static defaultProps = {
|
||||
onInput: () => {},
|
||||
}
|
||||
|
||||
constructor (props: FieldUrlInternalProps) {
|
||||
constructor (props: FieldUrlProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: validate(props.value, props.t),
|
||||
error: validate(props.value)
|
||||
};
|
||||
}
|
||||
|
||||
onInput = (url: string) => {
|
||||
this.setState({
|
||||
error: validate(url, this.props.t),
|
||||
error: validate(url)
|
||||
});
|
||||
if (this.props.onInput) this.props.onInput(url);
|
||||
}
|
||||
|
||||
onChange = (url: string) => {
|
||||
this.setState({
|
||||
error: validate(url, this.props.t),
|
||||
error: validate(url)
|
||||
});
|
||||
this.props.onChange(url);
|
||||
}
|
||||
@@ -115,5 +106,3 @@ class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlSt
|
||||
}
|
||||
}
|
||||
|
||||
const FieldUrl = withTranslation()(FieldUrlInternal);
|
||||
export default FieldUrl;
|
||||
|
||||
@@ -19,45 +19,31 @@ import FieldSourceLayer from './FieldSourceLayer'
|
||||
import { changeType, changeProperty } from '../libs/layer'
|
||||
import layout from '../config/layout.json'
|
||||
import {formatLayerId} from '../libs/format';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
|
||||
function getLayoutForType(type: LayerSpecification["type"], t: TFunction) {
|
||||
return layout[type] ? {
|
||||
...layout[type],
|
||||
groups: layout[type].groups.map(group => {
|
||||
return {
|
||||
...group,
|
||||
id: group.title.replace(/ /g, "_"),
|
||||
title: t(group.title)
|
||||
};
|
||||
}),
|
||||
} : layout.invalid;
|
||||
function getLayoutForType(type: LayerSpecification["type"]) {
|
||||
return layout[type] ? layout[type] : layout.invalid;
|
||||
}
|
||||
|
||||
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
|
||||
function layoutGroups(layerType: LayerSpecification["type"]): {title: string, type: string, fields?: string[]}[] {
|
||||
const layerGroup = {
|
||||
id: 'layer',
|
||||
title: t('Layer'),
|
||||
title: 'Layer',
|
||||
type: 'layer'
|
||||
}
|
||||
const filterGroup = {
|
||||
id: 'filter',
|
||||
title: t('Filter'),
|
||||
title: 'Filter',
|
||||
type: 'filter'
|
||||
}
|
||||
const editorGroup = {
|
||||
id: 'jsoneditor',
|
||||
title: t('JSON Editor'),
|
||||
title: 'JSON Editor',
|
||||
type: 'jsoneditor'
|
||||
}
|
||||
return [layerGroup, filterGroup]
|
||||
.concat(getLayoutForType(layerType, t).groups)
|
||||
.concat(getLayoutForType(layerType).groups)
|
||||
.concat([editorGroup])
|
||||
}
|
||||
|
||||
type LayerEditorInternalProps = {
|
||||
type LayerEditorProps = {
|
||||
layer: LayerSpecification
|
||||
sources: {[key: string]: SourceSpecification}
|
||||
vectorLayers: {[key: string]: any}
|
||||
@@ -72,14 +58,14 @@ type LayerEditorInternalProps = {
|
||||
isLastLayer?: boolean
|
||||
layerIndex: number
|
||||
errors?: any[]
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
type LayerEditorState = {
|
||||
editorGroups: {[keys:string]: boolean}
|
||||
};
|
||||
|
||||
/** Layer editor supporting multiple types of layers. */
|
||||
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
|
||||
export default class LayerEditor extends React.Component<LayerEditorProps, LayerEditorState> {
|
||||
static defaultProps = {
|
||||
onLayerChanged: () => {},
|
||||
onLayerIdChange: () => {},
|
||||
@@ -90,22 +76,22 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props: LayerEditorInternalProps) {
|
||||
constructor(props: LayerEditorProps) {
|
||||
super(props)
|
||||
|
||||
//TODO: Clean this up and refactor into function
|
||||
const editorGroups: {[keys:string]: boolean} = {}
|
||||
layoutGroups(this.props.layer.type, props.t).forEach(group => {
|
||||
layoutGroups(this.props.layer.type).forEach(group => {
|
||||
editorGroups[group.title] = true
|
||||
})
|
||||
|
||||
this.state = { editorGroups }
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
|
||||
static getDerivedStateFromProps(props: LayerEditorProps, state: LayerEditorState) {
|
||||
const additionalGroups = { ...state.editorGroups }
|
||||
|
||||
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
|
||||
getLayoutForType(props.layer.type).groups.forEach(group => {
|
||||
if(!(group.title in additionalGroups)) {
|
||||
additionalGroups[group.title] = true
|
||||
}
|
||||
@@ -256,19 +242,17 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const groupIds: string[] = [];
|
||||
const layerType = this.props.layer.type
|
||||
const groups = layoutGroups(layerType, t).filter(group => {
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
}).map(group => {
|
||||
const groupId = group.id;
|
||||
const groupId = group.title.replace(/ /g, "_");
|
||||
groupIds.push(groupId);
|
||||
return <LayerEditorGroup
|
||||
data-wd-key={group.title}
|
||||
id={groupId}
|
||||
key={groupId}
|
||||
key={group.title}
|
||||
title={group.title}
|
||||
isActive={this.state.editorGroups[group.title]}
|
||||
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
|
||||
@@ -281,25 +265,25 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
|
||||
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
|
||||
delete: {
|
||||
text: t("Delete"),
|
||||
text: "Delete",
|
||||
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
|
||||
},
|
||||
duplicate: {
|
||||
text: t("Duplicate"),
|
||||
text: "Duplicate",
|
||||
handler: () => this.props.onLayerCopy(this.props.layerIndex)
|
||||
},
|
||||
hide: {
|
||||
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
||||
text: (layout.visibility === "none") ? "Show" : "Hide",
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
|
||||
},
|
||||
moveLayerUp: {
|
||||
text: t("Move layer up"),
|
||||
text: "Move layer up",
|
||||
// Not actually used...
|
||||
disabled: this.props.isFirstLayer,
|
||||
handler: () => this.moveLayer(-1)
|
||||
},
|
||||
moveLayerDown: {
|
||||
text: t("Move layer down"),
|
||||
text: "Move layer down",
|
||||
// Not actually used...
|
||||
disabled: this.props.isLastLayer,
|
||||
handler: () => this.moveLayer(+1)
|
||||
@@ -313,12 +297,12 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
|
||||
return <section className="maputnik-layer-editor"
|
||||
role="main"
|
||||
aria-label={t("Layer editor")}
|
||||
aria-label="Layer editor"
|
||||
>
|
||||
<header>
|
||||
<div className="layer-header">
|
||||
<h2 className="layer-header__title">
|
||||
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
|
||||
Layer: {formatLayerId(this.props.layer.id)}
|
||||
</h2>
|
||||
<div className="layer-header__info">
|
||||
<Wrapper
|
||||
@@ -326,11 +310,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
onSelection={handleSelection}
|
||||
closeOnSelection={false}
|
||||
>
|
||||
<Button
|
||||
id="skip-target-layer-editor"
|
||||
data-wd-key="skip-target-layer-editor"
|
||||
className='more-menu__button'
|
||||
title={"Layer options"}>
|
||||
<Button id="skip-target-layer-editor" data-wd-key="skip-target-layer-editor" className='more-menu__button' title="Layer options">
|
||||
<MdMoreVert className="more-menu__button__svg" />
|
||||
</Button>
|
||||
<Menu>
|
||||
@@ -360,6 +340,3 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
const LayerEditor = withTranslation()(LayerEditorInternal);
|
||||
export default LayerEditor;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
|
||||
import type {LayerSpecification} from 'maplibre-gl';
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type LayerListContainerProps = {
|
||||
layers: LayerSpecification[]
|
||||
@@ -23,7 +22,6 @@ type LayerListContainerProps = {
|
||||
sources: object
|
||||
errors: any[]
|
||||
};
|
||||
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
|
||||
|
||||
type LayerListContainerState = {
|
||||
collapsedGroups: {[ket: string]: boolean}
|
||||
@@ -33,14 +31,14 @@ type LayerListContainerState = {
|
||||
};
|
||||
|
||||
// List of collapsible layer editors
|
||||
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
|
||||
class LayerListContainer extends React.Component<LayerListContainerProps, LayerListContainerState> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
selectedItemRef: React.RefObject<any>;
|
||||
scrollContainerRef: React.RefObject<HTMLElement>;
|
||||
|
||||
constructor(props: LayerListContainerInternalProps) {
|
||||
constructor(props: LayerListContainerProps) {
|
||||
super(props);
|
||||
this.selectedItemRef = React.createRef();
|
||||
this.scrollContainerRef = React.createRef();
|
||||
@@ -261,12 +259,10 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
})
|
||||
})
|
||||
|
||||
const t = this.props.t;
|
||||
|
||||
return <section
|
||||
className="maputnik-layer-list"
|
||||
role="complementary"
|
||||
aria-label={t("Layers list")}
|
||||
aria-label="Layers list"
|
||||
ref={this.scrollContainerRef}
|
||||
>
|
||||
<ModalAdd
|
||||
@@ -278,7 +274,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
onLayersChange={this.props.onLayersChange}
|
||||
/>
|
||||
<header className="maputnik-layer-list-header">
|
||||
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
|
||||
<span className="maputnik-layer-list-header-title">Layers</span>
|
||||
<span className="maputnik-space" />
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
@@ -287,11 +283,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
data-wd-key="skip-target-layer-list"
|
||||
onClick={this.toggleLayers}
|
||||
className="maputnik-button">
|
||||
{this.state.areAllGroupsExpanded === true ?
|
||||
t("Collapse")
|
||||
:
|
||||
t("Expand")
|
||||
}
|
||||
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,14 +293,14 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
onClick={this.toggleModal.bind(this, 'add')}
|
||||
data-wd-key="layer-list:add-layer"
|
||||
className="maputnik-button maputnik-button-selected">
|
||||
{t("Add Layer")}
|
||||
Add Layer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
role="navigation"
|
||||
aria-label={t("Layers list")}
|
||||
aria-label="Layers list"
|
||||
>
|
||||
<ul className="maputnik-layer-list-container">
|
||||
{listItems}
|
||||
@@ -318,13 +310,6 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
}
|
||||
}
|
||||
|
||||
// The next two lines have react-refresh/only-export-components disabled because they are
|
||||
// internal components that are not intended to be used outside of this file.
|
||||
// For some reason, the linter is not recognizing these components correctly.
|
||||
// When these components are migrated to functional components, the HOCs will no longer be needed
|
||||
// and the comments can be removed.
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
const LayerListContainer = withTranslation()(LayerListContainerInternal);
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, {type JSX} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
|
||||
import MaplibreInspect from '@maplibre/maplibre-gl-inspect'
|
||||
import colors from '@maplibre/maplibre-gl-inspect/lib/colors'
|
||||
// @ts-ignore
|
||||
import MapboxInspect from 'mapbox-gl-inspect'
|
||||
// @ts-ignore
|
||||
import colors from 'mapbox-gl-inspect/lib/colors'
|
||||
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'
|
||||
import MapMaplibreGlFeaturePropertyPopup, { InspectFeature } from './MapMaplibreGlFeaturePropertyPopup'
|
||||
import Color from 'color'
|
||||
@@ -11,13 +13,13 @@ import { HighlightedLayer, colorHighlightedLayer } from '../libs/highlight'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import '../maplibregl.css'
|
||||
import '../libs/maplibre-rtl'
|
||||
import MaplibreGeocoder, { MaplibreGeocoderApi, MaplibreGeocoderApiConfig } from '@maplibre/maplibre-gl-geocoder';
|
||||
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);
|
||||
return mountNode as HTMLElement;
|
||||
return mountNode;
|
||||
}
|
||||
|
||||
function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) {
|
||||
@@ -35,7 +37,6 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
|
||||
}
|
||||
|
||||
const sources: {[key:string]: SourceSpecification} = {}
|
||||
|
||||
Object.keys(originalMapStyle.sources).forEach(sourceId => {
|
||||
const source = originalMapStyle.sources[sourceId]
|
||||
if(source.type !== 'raster' && source.type !== 'raster-dem') {
|
||||
@@ -51,7 +52,7 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
|
||||
return inspectStyle
|
||||
}
|
||||
|
||||
type MapMaplibreGlInternalProps = {
|
||||
type MapMaplibreGlProps = {
|
||||
onDataChange?(event: {map: Map | null}): unknown
|
||||
onLayerSelect(...args: unknown[]): unknown
|
||||
mapStyle: StyleSpecification
|
||||
@@ -64,15 +65,15 @@ type MapMaplibreGlInternalProps = {
|
||||
}
|
||||
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
|
||||
onChange(value: {center: LngLat, zoom: number}): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
type MapMaplibreGlState = {
|
||||
map: Map | null
|
||||
inspect: MaplibreInspect | null
|
||||
inspect: MapboxInspect | null
|
||||
zoom?: number
|
||||
};
|
||||
|
||||
class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> {
|
||||
export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, MapMaplibreGlState> {
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
@@ -82,7 +83,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
}
|
||||
container: HTMLDivElement | null = null
|
||||
|
||||
constructor(props: MapMaplibreGlInternalProps) {
|
||||
constructor(props: MapMaplibreGlProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
map: null,
|
||||
@@ -90,8 +91,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;
|
||||
try {
|
||||
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() {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
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) {
|
||||
// Maplibre GL now does diffing natively so we don't need to calculate
|
||||
// the necessary operations ourselves!
|
||||
// We also need to update the style for inspect to work properly
|
||||
map.setStyle(styleWithTokens, {diff: true});
|
||||
if (this.props.inspectModeEnabled) {
|
||||
// HACK: We need to work out why we need to do this and what's causing
|
||||
// this error. I'm assuming an issue with maplibre-gl update and
|
||||
// mapbox-gl-inspect.
|
||||
try {
|
||||
this.state.inspect.render();
|
||||
} catch(err) {
|
||||
console.error("FIXME: Caught error", err);
|
||||
}
|
||||
}
|
||||
|
||||
map.showTileBoundaries = this.props.options?.showTileBoundaries!;
|
||||
map.showCollisionBoxes = this.props.options?.showCollisionBoxes!;
|
||||
map.showOverdrawInspector = this.props.options?.showOverdrawInspector!;
|
||||
}
|
||||
|
||||
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
|
||||
this.state.inspect.toggleInspector()
|
||||
}
|
||||
if (this.state.inspect && this.props.inspectModeEnabled) {
|
||||
this.state.inspect.setOriginalStyle(styleWithTokens);
|
||||
// In case the sources are the same, there's a need to refresh the style
|
||||
setTimeout(() => {
|
||||
this.state.inspect!.render();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
const mapOpts = {
|
||||
...this.props.options,
|
||||
container: this.container!,
|
||||
style: this.props.mapStyle,
|
||||
hash: true,
|
||||
maxZoom: 24,
|
||||
// setting to always load glyphs of CJK fonts from server
|
||||
// https://maplibre.org/maplibre-gl-js/docs/examples/local-ideographs/
|
||||
localIdeographFontFamily: false
|
||||
} satisfies MapOptions;
|
||||
maxZoom: 24
|
||||
}
|
||||
|
||||
const map = new MapLibreGl.Map(mapOpts);
|
||||
|
||||
@@ -152,9 +169,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
map.showCollisionBoxes = mapOpts.showCollisionBoxes!;
|
||||
map.showOverdrawInspector = mapOpts.showOverdrawInspector!;
|
||||
|
||||
this.initGeocoder(map);
|
||||
|
||||
const zoomControl = new ZoomControl(this.props.t("Zoom:"));
|
||||
const zoomControl = new ZoomControl;
|
||||
map.addControl(zoomControl, 'top-right');
|
||||
|
||||
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
|
||||
@@ -162,7 +177,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
|
||||
const tmpNode = document.createElement('div');
|
||||
|
||||
const inspect = new MaplibreInspect({
|
||||
const inspect = new MapboxInspect({
|
||||
popup: new MapLibreGl.Popup({
|
||||
closeOnClick: false
|
||||
}),
|
||||
@@ -219,61 +234,25 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
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() {
|
||||
const t = this.props.t;
|
||||
if(IS_SUPPORTED) {
|
||||
return <div
|
||||
className="maputnik-map__map"
|
||||
role="region"
|
||||
aria-label={t("Map view")}
|
||||
aria-label="Map view"
|
||||
ref={x => this.container = x}
|
||||
data-wd-key="maplibre:map"
|
||||
></div>
|
||||
}
|
||||
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 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
|
||||
counter?: number
|
||||
}
|
||||
|
||||
function displayValue(value: string | number | Date | object | undefined) {
|
||||
function displayValue(value: string | number | Date | object) {
|
||||
if (typeof value === 'undefined' || value === null) return value;
|
||||
if (value instanceof Date) return value.toLocaleString();
|
||||
if (typeof value === 'object' ||
|
||||
@@ -15,25 +21,30 @@ function displayValue(value: string | number | Date | object | undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function renderKeyValueTableRow(key: string, value: string | undefined) {
|
||||
return <tr key={key}>
|
||||
<td className="maputnik-popup-table-cell">{key}</td>
|
||||
<td className="maputnik-popup-table-cell">{value}</td>
|
||||
</tr>
|
||||
function renderProperties(feature: InspectFeature) {
|
||||
return Object.keys(feature.properties).map(propertyName => {
|
||||
const property = feature.properties[propertyName]
|
||||
return <Block key={propertyName} label={propertyName}>
|
||||
<FieldString value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
|
||||
</Block>
|
||||
})
|
||||
}
|
||||
|
||||
function renderFeatureId(feature: InspectFeature) {
|
||||
return <Block key={"feature-id"} label={"feature_id"}>
|
||||
<FieldString value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} />
|
||||
</Block>
|
||||
}
|
||||
|
||||
function renderFeature(feature: InspectFeature, idx: number) {
|
||||
return <React.Fragment key={idx}>
|
||||
<tr>
|
||||
<td colSpan={2} className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</td>
|
||||
</tr>
|
||||
{renderKeyValueTableRow("$type", feature.geometry.type)}
|
||||
{renderKeyValueTableRow("$id", displayValue(feature.id))}
|
||||
{Object.keys(feature.properties).map(propertyName => {
|
||||
const property = feature.properties[propertyName];
|
||||
return renderKeyValueTableRow(propertyName, displayValue(property))
|
||||
})}
|
||||
</React.Fragment>
|
||||
return <div key={`${feature.sourceLayer}-${idx}`}>
|
||||
<div className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
|
||||
<Block key={"property-type"} label={"$type"}>
|
||||
<FieldString value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
||||
</Block>
|
||||
{renderFeatureId(feature)}
|
||||
{renderProperties(feature)}
|
||||
</div>
|
||||
}
|
||||
|
||||
function removeDuplicatedFeatures(features: InspectFeature[]) {
|
||||
@@ -67,11 +78,7 @@ class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
|
||||
render() {
|
||||
const features = removeDuplicatedFeatures(this.props.features)
|
||||
return <div className="maputnik-feature-property-popup">
|
||||
<table className="maputnik-popup-table">
|
||||
<tbody>
|
||||
{features.map(renderFeature)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,15 @@ function groupFeaturesBySourceLayer(features: InspectFeature[]) {
|
||||
const returnedFeatures: {[key: string]: number} = {}
|
||||
|
||||
features.forEach(feature => {
|
||||
const sourceKey = feature.layer['source-layer'] as string;
|
||||
if(Object.prototype.hasOwnProperty.call(returnedFeatures, feature.layer.id)) {
|
||||
returnedFeatures[feature.layer.id]++
|
||||
|
||||
const featureObject = sources[sourceKey].find((f: InspectFeature) => f.layer.id === feature.layer.id)
|
||||
const featureObject = sources[feature.layer['source-layer']].find((f: InspectFeature) => f.layer.id === feature.layer.id)
|
||||
|
||||
featureObject!.counter = returnedFeatures[feature.layer.id]
|
||||
} else {
|
||||
sources[sourceKey] = sources[sourceKey] || []
|
||||
sources[sourceKey].push(feature)
|
||||
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
||||
sources[feature.layer['source-layer']].push(feature)
|
||||
|
||||
returnedFeatures[feature.layer.id] = 1
|
||||
}
|
||||
@@ -41,22 +40,30 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
|
||||
|
||||
try {
|
||||
const paintProps = feature.layer.paint;
|
||||
let propName;
|
||||
|
||||
if("text-color" in paintProps && paintProps["text-color"]) {
|
||||
return String(paintProps["text-color"]);
|
||||
if(Object.prototype.hasOwnProperty.call(paintProps, "text-color") && paintProps["text-color"]) {
|
||||
propName = "text-color";
|
||||
}
|
||||
if ("fill-color" in paintProps && paintProps["fill-color"]) {
|
||||
return String(paintProps["fill-color"]);
|
||||
else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-color") && paintProps["fill-color"]) {
|
||||
propName = "fill-color";
|
||||
}
|
||||
if ("line-color" in paintProps && paintProps["line-color"]) {
|
||||
return String(paintProps["line-color"]);
|
||||
else if (Object.prototype.hasOwnProperty.call(paintProps, "line-color") && paintProps["line-color"]) {
|
||||
propName = "line-color";
|
||||
}
|
||||
if ("fill-extrusion-color" in paintProps && paintProps["fill-extrusion-color"]) {
|
||||
return String(paintProps["fill-extrusion-color"]);
|
||||
else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
|
||||
propName = "fill-extrusion-color";
|
||||
}
|
||||
|
||||
if(propName) {
|
||||
const color = feature.layer.paint[propName];
|
||||
return String(color);
|
||||
}
|
||||
else {
|
||||
// Default color
|
||||
return "black";
|
||||
}
|
||||
}
|
||||
// This is quite complex, just incase there's an edgecase we're missing
|
||||
// always return black if we get an unexpected error.
|
||||
catch (err) {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React from 'react'
|
||||
import {throttle} from 'lodash';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup';
|
||||
|
||||
import 'ol/ol.css'
|
||||
//@ts-ignore
|
||||
import {apply} from 'ol-mapbox-style';
|
||||
import {Map, View, Overlay} from 'ol';
|
||||
|
||||
@@ -24,7 +22,7 @@ function renderCoords (coords: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
type MapOpenLayersInternalProps = {
|
||||
type MapOpenLayersProps = {
|
||||
onDataChange?(...args: unknown[]): unknown
|
||||
mapStyle: object
|
||||
accessToken?: string
|
||||
@@ -33,7 +31,7 @@ type MapOpenLayersInternalProps = {
|
||||
debugToolbox: boolean
|
||||
replaceAccessTokens(...args: unknown[]): unknown
|
||||
onChange(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
type MapOpenLayersState = {
|
||||
zoom: string
|
||||
@@ -43,7 +41,7 @@ type MapOpenLayersState = {
|
||||
selectedFeatures?: any[]
|
||||
};
|
||||
|
||||
class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps, MapOpenLayersState> {
|
||||
export default class MapOpenLayers extends React.Component<MapOpenLayersProps, MapOpenLayersState> {
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
@@ -55,7 +53,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
overlay: Overlay | undefined;
|
||||
popupContainer: HTMLElement | null = null;
|
||||
|
||||
constructor(props: MapOpenLayersInternalProps) {
|
||||
constructor(props: MapOpenLayersProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
zoom: "0",
|
||||
@@ -74,7 +72,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
apply(this.map, newMapStyle);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: MapOpenLayersInternalProps) {
|
||||
componentDidUpdate(prevProps: MapOpenLayersProps) {
|
||||
if (this.props.mapStyle !== prevProps.mapStyle) {
|
||||
this.updateStyle(
|
||||
this.props.replaceAccessTokens(this.props.mapStyle)
|
||||
@@ -152,7 +150,6 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div className="maputnik-ol-container">
|
||||
<div
|
||||
ref={x => this.popupContainer = x}
|
||||
@@ -162,7 +159,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
<button
|
||||
className="maplibregl-popup-close-button"
|
||||
onClick={this.closeOverlay}
|
||||
aria-label={t("Close popup")}
|
||||
aria-label="Close popup"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -172,20 +169,20 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-ol-zoom">
|
||||
{t("Zoom:")} {this.state.zoom}
|
||||
Zoom: {this.state.zoom}
|
||||
</div>
|
||||
{this.props.debugToolbox &&
|
||||
<div className="maputnik-ol-debug">
|
||||
<div>
|
||||
<label>{t("cursor:")} </label>
|
||||
<label>cursor: </label>
|
||||
<span>{renderCoords(this.state.cursor)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>{t("center:")} </label>
|
||||
<label>center: </label>
|
||||
<span>{renderCoords(this.state.center)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>{t("rotation:")} </label>
|
||||
<label>rotation: </label>
|
||||
<span>{this.state.rotation}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,7 +191,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
className="maputnik-ol"
|
||||
ref={x => this.container = x}
|
||||
role="region"
|
||||
aria-label={t("Map view")}
|
||||
aria-label="Map view"
|
||||
style={{
|
||||
...this.props.style,
|
||||
}}>
|
||||
@@ -203,5 +200,3 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
}
|
||||
}
|
||||
|
||||
const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
|
||||
export default MapOpenLayers;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
import React from 'react'
|
||||
import {MdClose} from 'react-icons/md'
|
||||
import AriaModal from 'react-aria-modal'
|
||||
import classnames from 'classnames';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type ModalInternalProps = PropsWithChildren & {
|
||||
|
||||
type ModalProps = {
|
||||
"data-wd-key"?: string
|
||||
isOpen: boolean
|
||||
title: string
|
||||
@@ -12,15 +12,15 @@ type ModalInternalProps = PropsWithChildren & {
|
||||
underlayClickExits?: boolean
|
||||
underlayProps?: any
|
||||
className?: string
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
|
||||
class ModalInternal extends React.Component<ModalInternalProps> {
|
||||
export default class Modal extends React.Component<ModalProps> {
|
||||
static defaultProps = {
|
||||
underlayClickExits: true
|
||||
}
|
||||
|
||||
// See <https://github.com/maplibre/maputnik/issues/416>
|
||||
// See <https://github.com/maputnik/editor/issues/416>
|
||||
onClose = () => {
|
||||
if (document.activeElement) {
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
@@ -32,7 +32,6 @@ class ModalInternal extends React.Component<ModalInternalProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
if(this.props.isOpen) {
|
||||
return <AriaModal
|
||||
titleText={this.props.title}
|
||||
@@ -50,7 +49,7 @@ class ModalInternal extends React.Component<ModalInternalProps> {
|
||||
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
||||
<span className="maputnik-modal-header-space"></span>
|
||||
<button className="maputnik-modal-header-toggle"
|
||||
title={t("Close modal")}
|
||||
title="Close modal"
|
||||
onClick={this.onClose}
|
||||
data-wd-key={this.props["data-wd-key"]+".close-modal"}
|
||||
>
|
||||
@@ -69,5 +68,3 @@ class ModalInternal extends React.Component<ModalInternalProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const Modal = withTranslation()(ModalInternal);
|
||||
export default Modal;
|
||||
|
||||
@@ -7,16 +7,15 @@ import FieldId from './FieldId'
|
||||
import FieldSource from './FieldSource'
|
||||
import FieldSourceLayer from './FieldSourceLayer'
|
||||
import type {LayerSpecification} from 'maplibre-gl'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type ModalAddInternalProps = {
|
||||
type ModalAddProps = {
|
||||
layers: LayerSpecification[]
|
||||
onLayersChange(layers: LayerSpecification[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(open: boolean): unknown
|
||||
// A dict of source id's and the available source layers
|
||||
sources: any
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
type ModalAddState = {
|
||||
type: LayerSpecification["type"]
|
||||
@@ -25,7 +24,7 @@ type ModalAddState = {
|
||||
'source-layer'?: string
|
||||
};
|
||||
|
||||
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
|
||||
export default class ModalAdd extends React.Component<ModalAddProps, ModalAddState> {
|
||||
addLayer = () => {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const layer: ModalAddState = {
|
||||
@@ -46,7 +45,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
||||
this.props.onOpenToggle(false)
|
||||
}
|
||||
|
||||
constructor(props: ModalAddInternalProps) {
|
||||
constructor(props: ModalAddProps) {
|
||||
super(props)
|
||||
const state: ModalAddState = {
|
||||
type: 'fill',
|
||||
@@ -55,12 +54,12 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
||||
|
||||
if(props.sources.length > 0) {
|
||||
state.source = Object.keys(this.props.sources)[0];
|
||||
state['source-layer'] = this.props.sources[state.source as keyof ModalAddInternalProps["sources"]][0]
|
||||
state['source-layer'] = this.props.sources[state.source as keyof ModalAddProps["sources"]][0]
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
|
||||
componentDidUpdate(_prevProps: ModalAddProps, prevState: ModalAddState) {
|
||||
// Check if source is valid for new type
|
||||
const oldType = prevState.type;
|
||||
const newType = this.state.type;
|
||||
@@ -126,14 +125,13 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
||||
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const sources = this.getSources(this.state.type);
|
||||
const layers = this.getLayersForSource(this.state.source!);
|
||||
|
||||
return <Modal
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={t('Add Layer')}
|
||||
title={'Add Layer'}
|
||||
data-wd-key="modal:add-layer"
|
||||
className="maputnik-add-modal"
|
||||
>
|
||||
@@ -171,12 +169,10 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
||||
onClick={this.addLayer}
|
||||
data-wd-key="add-layer"
|
||||
>
|
||||
{t("Add Layer")}
|
||||
Add Layer
|
||||
</InputButton>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalAdd = withTranslation()(ModalAddInternal);
|
||||
export default ModalAdd;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
type ModalDebugInternalProps = {
|
||||
type ModalDebugProps = {
|
||||
isOpen: boolean
|
||||
renderer: string
|
||||
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
|
||||
@@ -19,12 +18,12 @@ type ModalDebugInternalProps = {
|
||||
lat: number
|
||||
}
|
||||
}
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
|
||||
class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
|
||||
export default class ModalDebug extends React.Component<ModalDebugProps> {
|
||||
render() {
|
||||
const {t, mapView} = this.props;
|
||||
const {mapView} = this.props;
|
||||
|
||||
const osmZoom = Math.round(mapView.zoom)+1;
|
||||
const osmLon = +(mapView.center.lng).toFixed(5);
|
||||
@@ -34,10 +33,10 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
|
||||
data-wd-key="modal:debug"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={t('Debug')}
|
||||
title={'Debug'}
|
||||
>
|
||||
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||
<h1>{t("Options")}</h1>
|
||||
<h1>Options</h1>
|
||||
{this.props.renderer === 'mlgljs' &&
|
||||
<ul>
|
||||
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
|
||||
@@ -64,7 +63,6 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Links</h1>
|
||||
<p>
|
||||
<Trans t={t}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -72,12 +70,9 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
|
||||
>
|
||||
Open in OSM
|
||||
</a> — Opens the current view on openstreetmap.org
|
||||
</Trans>
|
||||
</p>
|
||||
</section>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalDebug = withTranslation()(ModalDebugInternal);
|
||||
export default ModalDebug;
|
||||
|
||||
@@ -5,7 +5,6 @@ import {version} from 'maplibre-gl/package.json'
|
||||
import {format} from '@maplibre/maplibre-gl-style-spec'
|
||||
import type {StyleSpecification} from 'maplibre-gl'
|
||||
import {MdFileDownload} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import FieldString from './FieldString'
|
||||
import InputButton from './InputButton'
|
||||
@@ -17,15 +16,15 @@ import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||
const MAPLIBRE_GL_VERSION = version;
|
||||
|
||||
|
||||
type ModalExportInternalProps = {
|
||||
type ModalExportProps = {
|
||||
mapStyle: StyleSpecification & { id: string }
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
|
||||
class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||
export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
|
||||
tokenizedStyle () {
|
||||
return format(
|
||||
@@ -49,7 +48,7 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||
|
||||
downloadHtml() {
|
||||
const tokenStyle = this.tokenizedStyle();
|
||||
const htmlTitle = this.props.mapStyle.name || this.props.t("Map");
|
||||
const htmlTitle = this.props.mapStyle.name || "Map";
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -101,32 +100,30 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const fsa = fieldSpecAdditional(t);
|
||||
return <Modal
|
||||
data-wd-key="modal:export"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={t('Export Style')}
|
||||
title={'Export Style'}
|
||||
className="maputnik-export-modal"
|
||||
>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>{t("Download Style")}</h1>
|
||||
<h1>Download Style</h1>
|
||||
<p>
|
||||
{t("Download a JSON style to your computer.")}
|
||||
Download a JSON style to your computer.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<FieldString
|
||||
label={fsa.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fsa.maputnik.maptiler_access_token}
|
||||
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||
value={(this.props.mapStyle.metadata || {} as any)['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
<FieldString
|
||||
label={fsa.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fsa.maputnik.thunderforest_access_token}
|
||||
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||
value={(this.props.mapStyle.metadata || {} as any)['maputnik:thunderforest_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||
/>
|
||||
@@ -137,14 +134,14 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||
onClick={this.downloadStyle.bind(this)}
|
||||
>
|
||||
<MdFileDownload />
|
||||
{t("Download Style")}
|
||||
Download Style
|
||||
</InputButton>
|
||||
|
||||
<InputButton
|
||||
onClick={this.downloadHtml.bind(this)}
|
||||
>
|
||||
<MdFileDownload />
|
||||
{t("Download HTML")}
|
||||
Download HTML
|
||||
</InputButton>
|
||||
</div>
|
||||
</section>
|
||||
@@ -153,5 +150,3 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const ModalExport = withTranslation()(ModalExportInternal);
|
||||
export default ModalExport;
|
||||
|
||||
@@ -2,25 +2,23 @@ import React from 'react'
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import Modal from './Modal'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
type ModalLoadingInternalProps = {
|
||||
type ModalLoadingProps = {
|
||||
isOpen: boolean
|
||||
onCancel(...args: unknown[]): unknown
|
||||
title: string
|
||||
message: React.ReactNode
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
|
||||
class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
|
||||
export default class ModalLoading extends React.Component<ModalLoadingProps> {
|
||||
underlayOnClick(e: Event) {
|
||||
// This stops click events falling through to underlying modals.
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Modal
|
||||
data-wd-key="modal:loading"
|
||||
isOpen={this.props.isOpen}
|
||||
@@ -37,12 +35,10 @@ class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
|
||||
</p>
|
||||
<p className="maputnik-dialog__buttons">
|
||||
<InputButton onClick={(e) => this.props.onCancel(e)}>
|
||||
{t("Cancel")}
|
||||
Cancel
|
||||
</InputButton>
|
||||
</p>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalLoading = withTranslation()(ModalLoadingInternal);
|
||||
export default ModalLoading;
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { FormEvent } from 'react'
|
||||
import {MdFileUpload} from 'react-icons/md'
|
||||
import {MdAddCircleOutline} from 'react-icons/md'
|
||||
import FileReaderInput, { Result } from 'react-file-reader-input'
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import ModalLoading from './ModalLoading'
|
||||
import Modal from './Modal'
|
||||
@@ -43,11 +42,11 @@ class PublicStyle extends React.Component<PublicStyleProps> {
|
||||
}
|
||||
}
|
||||
|
||||
type ModalOpenInternalProps = {
|
||||
type ModalOpenProps = {
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onStyleOpen(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
type ModalOpenState = {
|
||||
styleUrl: string
|
||||
@@ -56,8 +55,8 @@ type ModalOpenState = {
|
||||
activeRequestUrl?: string | null
|
||||
};
|
||||
|
||||
class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> {
|
||||
constructor(props: ModalOpenInternalProps) {
|
||||
export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpenState> {
|
||||
constructor(props: ModalOpenProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
styleUrl: ""
|
||||
@@ -175,7 +174,6 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const styleOptions = publicStyles.map(style => {
|
||||
return <PublicStyle
|
||||
key={style.id}
|
||||
@@ -202,31 +200,29 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
||||
data-wd-key="modal:open"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={() => this.onOpenToggle()}
|
||||
title={t('Open Style')}
|
||||
title={'Open Style'}
|
||||
>
|
||||
{errorElement}
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>{t("Upload Style")}</h1>
|
||||
<p>{t("Upload a JSON style from your computer.")}</p>
|
||||
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label={t("Style file")}>
|
||||
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Upload")}</InputButton>
|
||||
<h1>Upload Style</h1>
|
||||
<p>Upload a JSON style from your computer.</p>
|
||||
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label="Style file">
|
||||
<InputButton className="maputnik-upload-button"><MdFileUpload /> Upload</InputButton>
|
||||
</FileReaderInput>
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<form onSubmit={this.onSubmitUrl}>
|
||||
<h1>{t("Load from URL")}</h1>
|
||||
<h1>Load from URL</h1>
|
||||
<p>
|
||||
<Trans t={t}>
|
||||
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
|
||||
</Trans>
|
||||
</p>
|
||||
<InputUrl
|
||||
aria-label={t("Style URL")}
|
||||
aria-label="Style URL"
|
||||
data-wd-key="modal:open.url.input"
|
||||
type="text"
|
||||
className="maputnik-input"
|
||||
default={t("Enter URL...")}
|
||||
default="Enter URL..."
|
||||
value={this.state.styleUrl}
|
||||
onInput={this.onChangeUrl}
|
||||
onChange={this.onChangeUrl}
|
||||
@@ -243,9 +239,9 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section maputnik-modal-section--shrink">
|
||||
<h1>{t("Gallery Styles")}</h1>
|
||||
<h1>Gallery Styles</h1>
|
||||
<p>
|
||||
{t("Open one of the publicly available styles to start from.")}
|
||||
Open one of the publicly available styles to start from.
|
||||
</p>
|
||||
<div className="maputnik-style-gallery-container">
|
||||
{styleOptions}
|
||||
@@ -255,14 +251,12 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
||||
|
||||
<ModalLoading
|
||||
isOpen={!!this.state.activeRequest}
|
||||
title={t('Loading style')}
|
||||
title={'Loading style'}
|
||||
onCancel={(e: Event) => this.onCancelActiveRequest(e)}
|
||||
message={t("Loading: {{requestUrl}}", { requestUrl: this.state.activeRequestUrl })}
|
||||
message={"Loading: "+this.state.activeRequestUrl}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ModalOpen = withTranslation()(ModalOpenInternal);
|
||||
export default ModalOpen;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import type {LightSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from 'maplibre-gl'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import type {LightSpecification, StyleSpecification, TransitionSpecification} from 'maplibre-gl'
|
||||
|
||||
import FieldArray from './FieldArray'
|
||||
import FieldNumber from './FieldNumber'
|
||||
@@ -13,15 +12,15 @@ import FieldColor from './FieldColor'
|
||||
import Modal from './Modal'
|
||||
import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||
|
||||
type ModalSettingsInternalProps = {
|
||||
type ModalSettingsProps = {
|
||||
mapStyle: StyleSpecification
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
onChangeMetadataProperty(...args: unknown[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
|
||||
export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
|
||||
const transition = {
|
||||
...this.props.mapStyle.transition,
|
||||
@@ -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) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
@@ -96,86 +76,87 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
|
||||
render() {
|
||||
const metadata = this.props.mapStyle.metadata || {} as any;
|
||||
const {t, onChangeMetadataProperty, mapStyle} = this.props;
|
||||
const fsa = fieldSpecAdditional(t);
|
||||
const {onChangeMetadataProperty, mapStyle} = this.props;
|
||||
const inputProps = { }
|
||||
|
||||
const light = this.props.mapStyle.light || {};
|
||||
const transition = this.props.mapStyle.transition || {};
|
||||
const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification;
|
||||
|
||||
return <Modal
|
||||
data-wd-key="modal:settings"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={t('Style Settings')}
|
||||
title={'Style Settings'}
|
||||
>
|
||||
<div className="modal:settings">
|
||||
<FieldString
|
||||
label={t("Name")}
|
||||
<FieldString {...inputProps}
|
||||
label={"Name"}
|
||||
fieldSpec={latest.$root.name}
|
||||
data-wd-key="modal:settings.name"
|
||||
value={this.props.mapStyle.name}
|
||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||
/>
|
||||
<FieldString
|
||||
label={t("Owner")}
|
||||
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
|
||||
<FieldString {...inputProps}
|
||||
label={"Owner"}
|
||||
fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}
|
||||
data-wd-key="modal:settings.owner"
|
||||
value={(this.props.mapStyle as any).owner}
|
||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||
/>
|
||||
<FieldUrl
|
||||
<FieldUrl {...inputProps}
|
||||
fieldSpec={latest.$root.sprite}
|
||||
label={t("Sprite URL")}
|
||||
label="Sprite URL"
|
||||
data-wd-key="modal:settings.sprite"
|
||||
value={this.props.mapStyle.sprite as string}
|
||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||
/>
|
||||
|
||||
<FieldUrl
|
||||
label={t("Glyphs URL")}
|
||||
<FieldUrl {...inputProps}
|
||||
label="Glyphs URL"
|
||||
fieldSpec={latest.$root.glyphs}
|
||||
data-wd-key="modal:settings.glyphs"
|
||||
value={this.props.mapStyle.glyphs as string}
|
||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||
/>
|
||||
|
||||
<FieldString
|
||||
label={fsa.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fsa.maputnik.maptiler_access_token}
|
||||
<FieldString {...inputProps}
|
||||
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||
data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
|
||||
value={metadata['maputnik:openmaptiles_access_token']}
|
||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
|
||||
<FieldString
|
||||
label={fsa.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fsa.maputnik.thunderforest_access_token}
|
||||
<FieldString {...inputProps}
|
||||
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||
data-wd-key="modal:settings.maputnik:thunderforest_access_token"
|
||||
value={metadata['maputnik:thunderforest_access_token']}
|
||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||
/>
|
||||
|
||||
<FieldArray
|
||||
label={t("Center")}
|
||||
label={"Center"}
|
||||
fieldSpec={latest.$root.center}
|
||||
length={2}
|
||||
type="number"
|
||||
value={mapStyle.center || []}
|
||||
default={[0, 0]}
|
||||
default={latest.$root.center.default || [0, 0]}
|
||||
onChange={this.changeStyleProperty.bind(this, "center")}
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={t("Zoom")}
|
||||
{...inputProps}
|
||||
label={"Zoom"}
|
||||
fieldSpec={latest.$root.zoom}
|
||||
value={mapStyle.zoom}
|
||||
default={0}
|
||||
default={latest.$root.zoom.default || 0}
|
||||
onChange={this.changeStyleProperty.bind(this, "zoom")}
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={t("Bearing")}
|
||||
{...inputProps}
|
||||
label={"Bearing"}
|
||||
fieldSpec={latest.$root.bearing}
|
||||
value={mapStyle.bearing}
|
||||
default={latest.$root.bearing.default}
|
||||
@@ -183,7 +164,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={t("Pitch")}
|
||||
{...inputProps}
|
||||
label={"Pitch"}
|
||||
fieldSpec={latest.$root.pitch}
|
||||
value={mapStyle.pitch}
|
||||
default={latest.$root.pitch.default}
|
||||
@@ -191,7 +173,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
/>
|
||||
|
||||
<FieldEnum
|
||||
label={t("Light anchor")}
|
||||
{...inputProps}
|
||||
label={"Light anchor"}
|
||||
fieldSpec={latest.light.anchor}
|
||||
name="light-anchor"
|
||||
value={light.anchor as string}
|
||||
@@ -201,7 +184,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
/>
|
||||
|
||||
<FieldColor
|
||||
label={t("Light color")}
|
||||
{...inputProps}
|
||||
label={"Light color"}
|
||||
fieldSpec={latest.light.color}
|
||||
value={light.color as string}
|
||||
default={latest.light.color.default}
|
||||
@@ -209,7 +193,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={t("Light intensity")}
|
||||
{...inputProps}
|
||||
label={"Light intensity"}
|
||||
fieldSpec={latest.light.intensity}
|
||||
value={light.intensity as number}
|
||||
default={latest.light.intensity.default}
|
||||
@@ -217,7 +202,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
/>
|
||||
|
||||
<FieldArray
|
||||
label={t("Light position")}
|
||||
{...inputProps}
|
||||
label={"Light position"}
|
||||
fieldSpec={latest.light.position}
|
||||
type="number"
|
||||
length={latest.light.position.length}
|
||||
@@ -226,24 +212,9 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
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
|
||||
label={t("Terrain exaggeration")}
|
||||
fieldSpec={latest.terrain.exaggeration}
|
||||
value={terrain.exaggeration}
|
||||
default={latest.terrain.exaggeration.default}
|
||||
onChange={this.changeTerrainProperty.bind(this, "exaggeration")}
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={t("Transition delay")}
|
||||
{...inputProps}
|
||||
label={"Transition delay"}
|
||||
fieldSpec={latest.transition.delay}
|
||||
value={transition.delay}
|
||||
default={latest.transition.delay.default}
|
||||
@@ -251,20 +222,21 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={t("Transition duration")}
|
||||
{...inputProps}
|
||||
label={"Transition duration"}
|
||||
fieldSpec={latest.transition.duration}
|
||||
value={transition.duration}
|
||||
default={latest.transition.duration.default}
|
||||
onChange={this.changeTransitionProperty.bind(this, "duration")}
|
||||
/>
|
||||
|
||||
<FieldSelect
|
||||
label={fsa.maputnik.style_renderer.label}
|
||||
fieldSpec={fsa.maputnik.style_renderer}
|
||||
<FieldSelect {...inputProps}
|
||||
label={fieldSpecAdditional.maputnik.style_renderer.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
|
||||
data-wd-key="modal:settings.maputnik:renderer"
|
||||
options={[
|
||||
['mlgljs', 'MapLibreGL JS'],
|
||||
['ol', t('Open Layers (experimental)')],
|
||||
['ol', 'Open Layers (experimental)'],
|
||||
]}
|
||||
value={metadata['maputnik:renderer'] || 'mlgljs'}
|
||||
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')}
|
||||
@@ -274,5 +246,3 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalSettings = withTranslation()(ModalSettingsInternal)
|
||||
export default ModalSettings;
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
import React from 'react'
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
type ModalShortcutsInternalProps = {
|
||||
type ModalShortcutsProps = {
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
|
||||
class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps> {
|
||||
export default class ModalShortcuts extends React.Component<ModalShortcutsProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const help = [
|
||||
{
|
||||
key: <kbd>?</kbd>,
|
||||
text: t("Shortcuts menu")
|
||||
text: "Shortcuts menu"
|
||||
},
|
||||
{
|
||||
key: <kbd>o</kbd>,
|
||||
text: t("Open modal")
|
||||
text: "Open modal"
|
||||
},
|
||||
{
|
||||
key: <kbd>e</kbd>,
|
||||
text: t("Export modal")
|
||||
text: "Export modal"
|
||||
},
|
||||
{
|
||||
key: <kbd>d</kbd>,
|
||||
text: t("Data Sources modal")
|
||||
text: "Data Sources modal"
|
||||
},
|
||||
{
|
||||
key: <kbd>s</kbd>,
|
||||
text: t("Style Settings modal")
|
||||
text: "Style Settings modal"
|
||||
},
|
||||
{
|
||||
key: <kbd>i</kbd>,
|
||||
text: t("Toggle inspect")
|
||||
text: "Toggle inspect"
|
||||
},
|
||||
{
|
||||
key: <kbd>m</kbd>,
|
||||
text: t("Focus map")
|
||||
text: "Focus map"
|
||||
},
|
||||
{
|
||||
key: <kbd>!</kbd>,
|
||||
text: t("Debug modal")
|
||||
text: "Debug modal"
|
||||
},
|
||||
]
|
||||
|
||||
@@ -52,51 +50,51 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
|
||||
const mapShortcuts = [
|
||||
{
|
||||
key: <kbd>+</kbd>,
|
||||
text: t("Increase the zoom level by 1.",)
|
||||
text: "Increase the zoom level by 1.",
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>+</kbd></>,
|
||||
text: t("Increase the zoom level by 2.",)
|
||||
text: "Increase the zoom level by 2.",
|
||||
},
|
||||
{
|
||||
key: <kbd>-</kbd>,
|
||||
text: t("Decrease the zoom level by 1.",)
|
||||
text: "Decrease the zoom level by 1.",
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>-</kbd></>,
|
||||
text: t("Decrease the zoom level by 2.",)
|
||||
text: "Decrease the zoom level by 2.",
|
||||
},
|
||||
{
|
||||
key: <kbd>Up</kbd>,
|
||||
text: t("Pan up by 100 pixels.",)
|
||||
text: "Pan up by 100 pixels.",
|
||||
},
|
||||
{
|
||||
key: <kbd>Down</kbd>,
|
||||
text: t("Pan down by 100 pixels.",)
|
||||
text: "Pan down by 100 pixels.",
|
||||
},
|
||||
{
|
||||
key: <kbd>Left</kbd>,
|
||||
text: t("Pan left by 100 pixels.",)
|
||||
text: "Pan left by 100 pixels.",
|
||||
},
|
||||
{
|
||||
key: <kbd>Right</kbd>,
|
||||
text: t("Pan right by 100 pixels.",)
|
||||
text: "Pan right by 100 pixels.",
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>,
|
||||
text: t("Increase the rotation by 15 degrees.",)
|
||||
text: "Increase the rotation by 15 degrees.",
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>,
|
||||
text: t("Decrease the rotation by 15 degrees.")
|
||||
text: "Decrease the rotation by 15 degrees."
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>,
|
||||
text: t("Increase the pitch by 10 degrees.")
|
||||
text: "Increase the pitch by 10 degrees."
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>,
|
||||
text: t("Decrease the pitch by 10 degrees.")
|
||||
text: "Decrease the pitch by 10 degrees."
|
||||
},
|
||||
]
|
||||
|
||||
@@ -105,13 +103,11 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
|
||||
data-wd-key="modal:shortcuts"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={t('Shortcuts')}
|
||||
title={'Shortcuts'}
|
||||
>
|
||||
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||
<p>
|
||||
<Trans t={t}>
|
||||
Press <code>ESC</code> to lose focus of any active elements, then press one of:
|
||||
</Trans>
|
||||
</p>
|
||||
<dl>
|
||||
{help.map((item, idx) => {
|
||||
@@ -121,7 +117,7 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
|
||||
</div>
|
||||
})}
|
||||
</dl>
|
||||
<p>{t("If the Map is in focused you can use the following shortcuts")}</p>
|
||||
<p>If the Map is in focused you can use the following shortcuts</p>
|
||||
<ul>
|
||||
{mapShortcuts.map((item, idx) => {
|
||||
return <li key={idx}>
|
||||
@@ -134,5 +130,3 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
|
||||
}
|
||||
}
|
||||
|
||||
const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
|
||||
export default ModalShortcuts;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import Modal from './Modal'
|
||||
import InputButton from './InputButton'
|
||||
@@ -75,17 +74,16 @@ type ActiveModalSourcesTypeEditorProps = {
|
||||
source: SourceSpecification
|
||||
onDelete(...args: unknown[]): unknown
|
||||
onChange(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTypeEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div className="maputnik-active-source-type-editor">
|
||||
<div className="maputnik-active-source-type-editor-header">
|
||||
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
||||
<span className="maputnik-space" />
|
||||
<InputButton
|
||||
aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})}
|
||||
aria-label={`Remove '${this.props.sourceId}' source`}
|
||||
className="maputnik-active-source-type-editor-header-delete"
|
||||
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
@@ -106,7 +104,7 @@ class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTyp
|
||||
|
||||
type AddSourceProps = {
|
||||
onAdd(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
type AddSourceState = {
|
||||
mode: EditorMode
|
||||
@@ -136,7 +134,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
case 'geojson_json': return {
|
||||
type: 'geojson',
|
||||
cluster: (source as GeoJSONSourceSpecification).cluster || false,
|
||||
data: ''
|
||||
data: {}
|
||||
}
|
||||
case 'tilejson_vector': return {
|
||||
type: 'vector',
|
||||
@@ -204,7 +202,6 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
// Kind of a hack because the type changes, however maputnik has 1..n
|
||||
// options per type, for example
|
||||
//
|
||||
@@ -218,25 +215,25 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
|
||||
return <div className="maputnik-add-source">
|
||||
<FieldString
|
||||
label={t("Source ID")}
|
||||
fieldSpec={{doc: t("Unique ID that identifies the source and is used in the layer to reference the source.")}}
|
||||
label={"Source ID"}
|
||||
fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}
|
||||
value={this.state.sourceId}
|
||||
onChange={(v: string) => this.setState({ sourceId: v})}
|
||||
/>
|
||||
<FieldSelect
|
||||
label={t("Source Type")}
|
||||
label={"Source Type"}
|
||||
fieldSpec={sourceTypeFieldSpec}
|
||||
options={[
|
||||
['geojson_json', t('GeoJSON (JSON)')],
|
||||
['geojson_url', t('GeoJSON (URL)')],
|
||||
['tilejson_vector', t('Vector (TileJSON URL)')],
|
||||
['tilexyz_vector', t('Vector (XYZ URLs)')],
|
||||
['tilejson_raster', t('Raster (TileJSON URL)')],
|
||||
['tilexyz_raster', t('Raster (XYZ URL)')],
|
||||
['tilejson_raster-dem', t('Raster DEM (TileJSON URL)')],
|
||||
['tilexyz_raster-dem', t('Raster DEM (XYZ URLs)')],
|
||||
['image', t('Image')],
|
||||
['video', t('Video')],
|
||||
['geojson_json', 'GeoJSON (JSON)'],
|
||||
['geojson_url', 'GeoJSON (URL)'],
|
||||
['tilejson_vector', 'Vector (TileJSON URL)'],
|
||||
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
||||
['tilejson_raster', 'Raster (TileJSON URL)'],
|
||||
['tilexyz_raster', 'Raster (XYZ URL)'],
|
||||
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
|
||||
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
|
||||
['image', 'Image'],
|
||||
['video', 'Video'],
|
||||
]}
|
||||
onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
|
||||
value={this.state.mode as string}
|
||||
@@ -250,20 +247,20 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
className="maputnik-add-source-button"
|
||||
onClick={this.onAdd}
|
||||
>
|
||||
{t("Add Source")}
|
||||
Add Source
|
||||
</InputButton>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
type ModalSourcesInternalProps = {
|
||||
type ModalSourcesProps = {
|
||||
mapStyle: StyleSpecification
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
||||
export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
|
||||
const strippedSource = {...source}
|
||||
delete strippedSource['title']
|
||||
@@ -271,8 +268,7 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {t, mapStyle} = this.props;
|
||||
const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady};
|
||||
const mapStyle = this.props.mapStyle
|
||||
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
||||
const source = mapStyle.sources[sourceId]
|
||||
return <ActiveModalSourcesTypeEditor
|
||||
@@ -281,7 +277,6 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
||||
source={source}
|
||||
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
||||
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
||||
{...i18nProps}
|
||||
/>
|
||||
})
|
||||
|
||||
@@ -300,17 +295,17 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
||||
data-wd-key="modal:sources"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={t('Sources')}
|
||||
title={'Sources'}
|
||||
>
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>{t("Active Sources")}</h1>
|
||||
<h1>Active Sources</h1>
|
||||
{activeSources}
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>{t("Choose Public Source")}</h1>
|
||||
<h1>Choose Public Source</h1>
|
||||
<p>
|
||||
{t("Add one of the publicly available sources to your style.")}
|
||||
Add one of the publicly available sources to your style.
|
||||
</p>
|
||||
<div className="maputnik-public-sources" style={{maxWidth: 500}}>
|
||||
{tilesetOptions}
|
||||
@@ -318,16 +313,13 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>{t("Add New Source")}</h1>
|
||||
<p>{t("Add a new source to your style. You can only choose the source type and id at creation time!")}</p>
|
||||
<h1>Add New Source</h1>
|
||||
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
|
||||
<AddSource
|
||||
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
|
||||
{...i18nProps}
|
||||
/>
|
||||
</section>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalSources = withTranslation()(ModalSourcesInternal);
|
||||
export default ModalSources;
|
||||
|
||||
@@ -8,8 +8,6 @@ import FieldDynamicArray from './FieldDynamicArray'
|
||||
import FieldArray from './FieldArray'
|
||||
import FieldJson from './FieldJson'
|
||||
import FieldCheckbox from './FieldCheckbox'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next'
|
||||
|
||||
export type EditorMode = "video" | "image" | "tilejson_vector" | "tilexyz_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "tilexyz_vector" | "geojson_url" | "geojson_json" | null;
|
||||
|
||||
@@ -19,15 +17,14 @@ type TileJSONSourceEditorProps = {
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
children?: React.ReactNode
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
|
||||
class TileJSONSourceEditor extends React.Component<TileJSONSourceEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div>
|
||||
<FieldUrl
|
||||
label={t("TileJSON URL")}
|
||||
label={"TileJSON URL"}
|
||||
fieldSpec={latest.source_vector.url}
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
@@ -48,7 +45,7 @@ type TileURLSourceEditorProps = {
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
children?: React.ReactNode
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
changeTileUrls(tiles: string[]) {
|
||||
@@ -61,7 +58,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
renderTileUrls() {
|
||||
const tiles = this.props.source.tiles || [];
|
||||
return <FieldDynamicArray
|
||||
label={this.props.t("Tile URL")}
|
||||
label={"Tile URL"}
|
||||
fieldSpec={latest.source_vector.tiles}
|
||||
type="url"
|
||||
value={tiles}
|
||||
@@ -70,11 +67,10 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div>
|
||||
{this.renderTileUrls()}
|
||||
<FieldNumber
|
||||
label={t("Min Zoom")}
|
||||
label={"Min Zoom"}
|
||||
fieldSpec={latest.source_vector.minzoom}
|
||||
value={this.props.source.minzoom || 0}
|
||||
onChange={minzoom => this.props.onChange({
|
||||
@@ -83,7 +79,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
})}
|
||||
/>
|
||||
<FieldNumber
|
||||
label={t("Max Zoom")}
|
||||
label={"Max Zoom"}
|
||||
fieldSpec={latest.source_vector.maxzoom}
|
||||
value={this.props.source.maxzoom || 22}
|
||||
onChange={maxzoom => this.props.onChange({
|
||||
@@ -97,24 +93,16 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const createCornerLabels: (t: TFunction) => { label: string, key: string }[] = (t) => ([
|
||||
{ label: t("Coord top left"), key: "top left" },
|
||||
{ label: t("Coord top right"), key: "top right" },
|
||||
{ label: t("Coord bottom right"), key: "bottom right" },
|
||||
{ label: t("Coord bottom left"), key: "bottom left" },
|
||||
]);
|
||||
|
||||
type ImageSourceEditorProps = {
|
||||
source: {
|
||||
coordinates: [number, number][]
|
||||
url: string
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const changeCoord = (idx: number, val: [number, number]) => {
|
||||
const coordinates = this.props.source.coordinates.slice(0);
|
||||
coordinates[idx] = val;
|
||||
@@ -127,7 +115,7 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
||||
|
||||
return <div>
|
||||
<FieldUrl
|
||||
label={t("Image URL")}
|
||||
label={"Image URL"}
|
||||
fieldSpec={latest.source_image.url}
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
@@ -135,11 +123,11 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
||||
url,
|
||||
})}
|
||||
/>
|
||||
{createCornerLabels(t).map(({label, key}, idx) => {
|
||||
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
||||
return (
|
||||
<FieldArray
|
||||
label={label}
|
||||
key={key}
|
||||
label={`Coord ${label}`}
|
||||
key={label}
|
||||
length={2}
|
||||
type="number"
|
||||
value={this.props.source.coordinates[idx]}
|
||||
@@ -158,11 +146,10 @@ type VideoSourceEditorProps = {
|
||||
urls: string[]
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const changeCoord = (idx: number, val: [number, number]) => {
|
||||
const coordinates = this.props.source.coordinates.slice(0);
|
||||
coordinates[idx] = val;
|
||||
@@ -182,18 +169,18 @@ class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
|
||||
|
||||
return <div>
|
||||
<FieldDynamicArray
|
||||
label={t("Video URL")}
|
||||
label={"Video URL"}
|
||||
fieldSpec={latest.source_video.urls}
|
||||
type="string"
|
||||
value={this.props.source.urls}
|
||||
default={[]}
|
||||
onChange={changeUrls}
|
||||
/>
|
||||
{createCornerLabels(t).map(({label, key}, idx) => {
|
||||
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
||||
return (
|
||||
<FieldArray
|
||||
label={label}
|
||||
key={key}
|
||||
label={`Coord ${label}`}
|
||||
key={label}
|
||||
length={2}
|
||||
type="number"
|
||||
value={this.props.source.coordinates[idx]}
|
||||
@@ -211,13 +198,12 @@ type GeoJSONSourceUrlEditorProps = {
|
||||
data: string
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class GeoJSONSourceUrlEditor extends React.Component<GeoJSONSourceUrlEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <FieldUrl
|
||||
label={t("GeoJSON URL")}
|
||||
label={"GeoJSON URL"}
|
||||
fieldSpec={latest.source_geojson.data}
|
||||
value={this.props.source.data}
|
||||
onChange={data => this.props.onChange({
|
||||
@@ -234,13 +220,12 @@ type GeoJSONSourceFieldJsonEditorProps = {
|
||||
cluster: boolean
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJsonEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div>
|
||||
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}>
|
||||
<Block label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
|
||||
<FieldJson
|
||||
layer={this.props.source.data}
|
||||
maxHeight={200}
|
||||
@@ -258,7 +243,7 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
|
||||
/>
|
||||
</Block>
|
||||
<FieldCheckbox
|
||||
label={t('Cluster')}
|
||||
label={'Cluster'}
|
||||
value={this.props.source.cluster}
|
||||
onChange={cluster => {
|
||||
this.props.onChange({
|
||||
@@ -271,22 +256,18 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
|
||||
}
|
||||
}
|
||||
|
||||
type ModalSourcesTypeEditorInternalProps = {
|
||||
type ModalSourcesTypeEditorProps = {
|
||||
mode: EditorMode
|
||||
source: any
|
||||
onChange(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEditorInternalProps> {
|
||||
export default class ModalSourcesTypeEditor extends React.Component<ModalSourcesTypeEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const commonProps = {
|
||||
source: this.props.source,
|
||||
onChange: this.props.onChange,
|
||||
t: this.props.t,
|
||||
i18n: this.props.i18n,
|
||||
tReady: this.props.tReady,
|
||||
};
|
||||
}
|
||||
switch(this.props.mode) {
|
||||
case 'geojson_url': return <GeoJSONSourceUrlEditor {...commonProps} />
|
||||
case 'geojson_json': return <GeoJSONSourceFieldJsonEditor {...commonProps} />
|
||||
@@ -297,7 +278,7 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
|
||||
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
|
||||
<FieldSelect
|
||||
label={t("Encoding")}
|
||||
label={"Encoding"}
|
||||
fieldSpec={latest.source_raster_dem.encoding}
|
||||
options={Object.keys(latest.source_raster_dem.encoding.values)}
|
||||
onChange={encoding => this.props.onChange({
|
||||
@@ -314,5 +295,3 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
|
||||
}
|
||||
}
|
||||
|
||||
const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
|
||||
export default ModalSourcesTypeEditor;
|
||||
|
||||
@@ -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 don’t track you, so we don’t know how you use Maputnik. Help us make Maputnik better for you by completing a 7–minute survey carried out by our contributing designer.</p>
|
||||
<InputButton onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</InputButton>
|
||||
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import React from 'react'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import './SmallError.scss';
|
||||
|
||||
|
||||
type SmallErrorInternalProps = {
|
||||
type SmallErrorProps = {
|
||||
children?: React.ReactNode
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class SmallErrorInternal extends React.Component<SmallErrorInternalProps> {
|
||||
|
||||
export default class SmallError extends React.Component<SmallErrorProps> {
|
||||
render () {
|
||||
const t = this.props.t;
|
||||
return (
|
||||
<div className="SmallError">
|
||||
{t("Error:")} {this.props.children}
|
||||
Error: {this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SmallError = withTranslation()(SmallErrorInternal);
|
||||
export default SmallError;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
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 InputSpec from './InputSpec'
|
||||
@@ -11,14 +11,13 @@ import Block from './Block'
|
||||
import docUid from '../libs/document-uid'
|
||||
import sortNumerically from '../libs/sort-numerically'
|
||||
import {findDefaultFromSpec} from '../libs/spec-helper';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import labelFromFieldName from '../libs/label-from-field-name'
|
||||
import DeleteStopButton from './_DeleteStopButton'
|
||||
|
||||
|
||||
|
||||
function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState) {
|
||||
function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
|
||||
// This is initialsed below only if required to improved performance.
|
||||
let newRefs: {[key: number]: string} | undefined;
|
||||
|
||||
@@ -36,7 +35,7 @@ function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState)
|
||||
return newRefs;
|
||||
}
|
||||
|
||||
type DataPropertyInternalProps = {
|
||||
type DataPropertyProps = {
|
||||
onChange?(fieldName: string, value: any): unknown
|
||||
onDeleteStop?(...args: unknown[]): unknown
|
||||
onAddStop?(...args: unknown[]): unknown
|
||||
@@ -47,7 +46,7 @@ type DataPropertyInternalProps = {
|
||||
fieldSpec?: object
|
||||
value?: DataPropertyValue
|
||||
errors?: object
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
type DataPropertyState = {
|
||||
refs: {[key: number]: string}
|
||||
@@ -66,7 +65,7 @@ export type Stop = [{
|
||||
value: number
|
||||
}, number]
|
||||
|
||||
class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> {
|
||||
export default class DataProperty extends React.Component<DataPropertyProps, DataPropertyState> {
|
||||
state = {
|
||||
refs: {} as {[key: number]: string}
|
||||
}
|
||||
@@ -81,7 +80,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<DataPropertyInternalProps>, state: DataPropertyState) {
|
||||
static getDerivedStateFromProps(props: DataPropertyProps, state: DataPropertyState) {
|
||||
const newRefs = setStopRefs(props, state);
|
||||
if(newRefs) {
|
||||
return {
|
||||
@@ -214,8 +213,6 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
if (typeof this.props.value?.type === "undefined") {
|
||||
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec)
|
||||
}
|
||||
@@ -230,8 +227,8 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
|
||||
|
||||
const dataProps = {
|
||||
'aria-label': t("Input value"),
|
||||
label: t("Data value"),
|
||||
'aria-label': "Input value",
|
||||
label: "Data value",
|
||||
value: dataLevel as any,
|
||||
onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
|
||||
}
|
||||
@@ -266,7 +263,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
</td>
|
||||
<td>
|
||||
<InputSpec
|
||||
aria-label={t("Output value")}
|
||||
aria-label="Output value"
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
@@ -285,27 +282,27 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
|
||||
<div className="maputnik-data-fieldset-inner">
|
||||
<Block
|
||||
label={t("Function")}
|
||||
label={"Function"}
|
||||
key="function"
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSelect
|
||||
value={this.props.value!.type}
|
||||
onChange={(propVal: string) => this.changeDataType(propVal)}
|
||||
title={t("Select a type of data scale (default is 'categorical').")}
|
||||
title={"Select a type of data scale (default is 'categorical')."}
|
||||
options={this.getDataFunctionTypes(this.props.fieldSpec)}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
{this.props.value?.type !== "identity" &&
|
||||
<Block
|
||||
label={t("Base")}
|
||||
label={"Base"}
|
||||
key="base"
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSpec
|
||||
fieldName={"base"}
|
||||
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
|
||||
fieldSpec={latest.function.base}
|
||||
value={this.props.value?.base}
|
||||
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">
|
||||
<InputString
|
||||
value={this.props.value?.property}
|
||||
title={t("Input a data property to base styles off of.")}
|
||||
title={"Input a data property to base styles off of."}
|
||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
{dataFields &&
|
||||
<Block
|
||||
label={t("Default")}
|
||||
label={"Default"}
|
||||
key="default"
|
||||
>
|
||||
<InputSpec
|
||||
@@ -340,12 +337,12 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
{dataFields &&
|
||||
<div className="maputnik-function-stop">
|
||||
<table className="maputnik-function-stop-table">
|
||||
<caption>{t("Stops")}</caption>
|
||||
<caption>Stops</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("Zoom")}</th>
|
||||
<th>{t("Input value")}</th>
|
||||
<th rowSpan={2}>{t("Output value")}</th>
|
||||
<th>Zoom</th>
|
||||
<th>Input value</th>
|
||||
<th rowSpan={2}>Output value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -362,7 +359,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> {t("Add stop")}
|
||||
</svg> Add stop
|
||||
</InputButton>
|
||||
}
|
||||
<InputButton
|
||||
@@ -371,7 +368,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg> {t("Convert to expression")}
|
||||
</svg> Convert to expression
|
||||
</InputButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -379,6 +376,3 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const DataProperty = withTranslation()(DataPropertyInternal);
|
||||
export default DataProperty;
|
||||
|
||||
@@ -2,26 +2,21 @@ import React from 'react'
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
type DeleteStopButtonInternalProps = {
|
||||
type DeleteStopButtonProps = {
|
||||
onClick?(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
|
||||
class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalProps> {
|
||||
export default class DeleteStopButton extends React.Component<DeleteStopButtonProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <InputButton
|
||||
className="maputnik-delete-stop"
|
||||
onClick={this.props.onClick}
|
||||
title={t("Remove zoom level from stop")}
|
||||
title={"Remove zoom level from stop"}
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
}
|
||||
}
|
||||
|
||||
const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
|
||||
export default DeleteStopButton;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import {MdDelete, MdUndo} from 'react-icons/md'
|
||||
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import Block from './Block'
|
||||
import InputButton from './InputButton'
|
||||
@@ -9,7 +8,7 @@ import labelFromFieldName from '../libs/label-from-field-name'
|
||||
import FieldJson from './FieldJson'
|
||||
|
||||
|
||||
type ExpressionPropertyInternalProps = {
|
||||
type ExpressionPropertyProps = {
|
||||
onDelete?(...args: unknown[]): unknown
|
||||
fieldName: string
|
||||
fieldType?: string
|
||||
@@ -21,20 +20,20 @@ type ExpressionPropertyInternalProps = {
|
||||
canUndo?(...args: unknown[]): unknown
|
||||
onFocus?(...args: unknown[]): unknown
|
||||
onBlur?(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
type ExpressionPropertyState = {
|
||||
jsonError: boolean
|
||||
};
|
||||
|
||||
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps, ExpressionPropertyState> {
|
||||
export default class ExpressionProperty extends React.Component<ExpressionPropertyProps, ExpressionPropertyState> {
|
||||
static defaultProps = {
|
||||
errors: {},
|
||||
onFocus: () => {},
|
||||
onBlur: () => {},
|
||||
}
|
||||
|
||||
constructor(props: ExpressionPropertyInternalProps) {
|
||||
constructor (props:ExpressionPropertyProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
jsonError: false,
|
||||
@@ -54,7 +53,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
|
||||
}
|
||||
|
||||
render() {
|
||||
const {t, errors, fieldName, fieldType, value, canUndo} = this.props;
|
||||
const {errors, fieldName, fieldType, value, canUndo} = this.props;
|
||||
const {jsonError} = this.state;
|
||||
const undoDisabled = canUndo ? !canUndo() : true;
|
||||
|
||||
@@ -66,7 +65,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
|
||||
onClick={this.props.onUndo}
|
||||
disabled={undoDisabled}
|
||||
className="maputnik-delete-stop"
|
||||
title={t("Revert from expression")}
|
||||
title="Revert from expression"
|
||||
>
|
||||
<MdUndo />
|
||||
</InputButton>
|
||||
@@ -75,7 +74,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
|
||||
key="delete_action"
|
||||
onClick={this.props.onDelete}
|
||||
className="maputnik-delete-stop"
|
||||
title={t("Delete expression")}
|
||||
title="Delete expression"
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
@@ -113,7 +112,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
|
||||
// this feels like an incorrect type...? `foundErrors` is an array of objects, not a single object
|
||||
error={foundErrors as any}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
label={t(labelFromFieldName(this.props.fieldName))}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={deleteStopBtn}
|
||||
wideMode={true}
|
||||
>
|
||||
@@ -138,6 +137,3 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
|
||||
export default ExpressionProperty;
|
||||
|
||||
@@ -3,18 +3,16 @@ import React from 'react'
|
||||
import InputButton from './InputButton'
|
||||
import {MdFunctions, MdInsertChart} from 'react-icons/md'
|
||||
import {mdiFunctionVariant} from '@mdi/js';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FunctionInputButtonsInternalProps = {
|
||||
type FunctionInputButtonsProps = {
|
||||
fieldSpec?: any
|
||||
onZoomClick?(...args: unknown[]): unknown
|
||||
onDataClick?(...args: unknown[]): unknown
|
||||
onExpressionClick?(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> {
|
||||
export default class FunctionInputButtons extends React.Component<FunctionInputButtonsProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
|
||||
|
||||
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
||||
@@ -22,7 +20,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
||||
<InputButton
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onExpressionClick}
|
||||
title={t("Convert to expression")}
|
||||
title="Convert to expression"
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
@@ -33,7 +31,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
||||
makeZoomInputButton = <InputButton
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onZoomClick}
|
||||
title={t("Convert property into a zoom function")}
|
||||
title="Convert property into a zoom function"
|
||||
>
|
||||
<MdFunctions />
|
||||
</InputButton>
|
||||
@@ -42,7 +40,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
||||
makeDataInputButton = <InputButton
|
||||
className="maputnik-make-data-function"
|
||||
onClick={this.props.onDataClick}
|
||||
title={t("Convert property to data function")}
|
||||
title="Convert property to data function"
|
||||
>
|
||||
<MdInsertChart />
|
||||
</InputButton>
|
||||
@@ -58,6 +56,3 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
|
||||
export default FunctionInputButtons;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import InputSpec from './InputSpec'
|
||||
@@ -21,7 +20,7 @@ import sortNumerically from '../libs/sort-numerically'
|
||||
*
|
||||
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
|
||||
*/
|
||||
function setStopRefs(props: ZoomPropertyInternalProps, state: ZoomPropertyState) {
|
||||
function setStopRefs(props: ZoomPropertyProps, state: ZoomPropertyState) {
|
||||
// This is initialsed below only if required to improved performance.
|
||||
let newRefs: {[key: number]: string} = {};
|
||||
|
||||
@@ -46,7 +45,7 @@ type ZoomWithStops = {
|
||||
}
|
||||
|
||||
|
||||
type ZoomPropertyInternalProps = {
|
||||
type ZoomPropertyProps = {
|
||||
onChange?(...args: unknown[]): unknown
|
||||
onChangeToDataFunction?(...args: unknown[]): unknown
|
||||
onDeleteStop?(...args: unknown[]): unknown
|
||||
@@ -60,13 +59,13 @@ type ZoomPropertyInternalProps = {
|
||||
}
|
||||
errors?: object
|
||||
value?: ZoomWithStops
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
type ZoomPropertyState = {
|
||||
refs: {[key: number]: string}
|
||||
}
|
||||
|
||||
class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> {
|
||||
export default class ZoomProperty extends React.Component<ZoomPropertyProps, ZoomPropertyState> {
|
||||
static defaultProps = {
|
||||
errors: {},
|
||||
}
|
||||
@@ -85,7 +84,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<ZoomPropertyInternalProps>, state: ZoomPropertyState) {
|
||||
static getDerivedStateFromProps(props: ZoomPropertyProps, state: ZoomPropertyState) {
|
||||
const newRefs = setStopRefs(props, state);
|
||||
if(newRefs) {
|
||||
return {
|
||||
@@ -153,17 +152,17 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const zoomFields = this.props.value?.stops.map((stop, idx) => {
|
||||
const zoomLevel = stop[0]
|
||||
const key = this.state.refs[idx];
|
||||
const value = stop[1]
|
||||
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
|
||||
return <tr
|
||||
key={`${stop[0]}-${stop[1]}`}
|
||||
key={key}
|
||||
>
|
||||
<td>
|
||||
<InputNumber
|
||||
aria-label={t("Zoom")}
|
||||
aria-label="Zoom"
|
||||
value={zoomLevel}
|
||||
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
||||
min={0}
|
||||
@@ -172,7 +171,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
||||
</td>
|
||||
<td>
|
||||
<InputSpec
|
||||
aria-label={t("Output value")}
|
||||
aria-label="Output value"
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec as any}
|
||||
value={value}
|
||||
@@ -191,24 +190,24 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
||||
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
|
||||
<div className="maputnik-data-fieldset-inner">
|
||||
<Block
|
||||
label={t("Function")}
|
||||
label={"Function"}
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSelect
|
||||
value={"interpolate"}
|
||||
onChange={(propVal: string) => this.changeDataType(propVal)}
|
||||
title={t("Select a type of data scale (default is 'categorical').")}
|
||||
title={"Select a type of data scale (default is 'categorical')."}
|
||||
options={this.getDataFunctionTypes(this.props.fieldSpec!)}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
<Block
|
||||
label={t("Base")}
|
||||
label={"Base"}
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSpec
|
||||
fieldName={"base"}
|
||||
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
|
||||
fieldSpec={latest.function.base}
|
||||
value={this.props.value?.base}
|
||||
onChange={(_, newValue) => this.changeBase(newValue as number | undefined)}
|
||||
/>
|
||||
@@ -216,11 +215,11 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
||||
</Block>
|
||||
<div className="maputnik-function-stop">
|
||||
<table className="maputnik-function-stop-table maputnik-function-stop-table--zoom">
|
||||
<caption>{t("Stops")}</caption>
|
||||
<caption>Stops</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("Zoom")}</th>
|
||||
<th rowSpan={2}>{t("Output value")}</th>
|
||||
<th>Zoom</th>
|
||||
<th rowSpan={2}>Output value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -235,7 +234,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> {t("Add stop")}
|
||||
</svg> Add stop
|
||||
</InputButton>
|
||||
<InputButton
|
||||
className="maputnik-add-stop"
|
||||
@@ -243,7 +242,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg> {t("Convert to expression")}
|
||||
</svg> Convert to expression
|
||||
</InputButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,6 +262,3 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ZoomProperty = withTranslation()(ZoomPropertyInternal);
|
||||
export default ZoomProperty;
|
||||
|
||||
+36
-48
@@ -1,40 +1,40 @@
|
||||
[
|
||||
{
|
||||
"id": "0-empty-style",
|
||||
"title": "Empty Style",
|
||||
"url": "https://cdn.jsdelivr.net/gh/maputnik/editor@9cf74ca405d2be0608b57db8109cf3a6af5b9f49/src/config/empty-style.json",
|
||||
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
|
||||
},
|
||||
{
|
||||
"id": "dark-matter",
|
||||
"title": "Dark Matter",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.9/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
|
||||
"id": "osm-liberty",
|
||||
"title": "OSM Liberty",
|
||||
"url": "https://maputnik.github.io/osm-liberty/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png"
|
||||
},
|
||||
{
|
||||
"id": "maptiler-basic-gl-style",
|
||||
"title": "Maptiler Basic",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.10/style.json",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.9/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
|
||||
},
|
||||
{
|
||||
"id": "dark-matter",
|
||||
"title": "Dark Matter",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.8/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
|
||||
},
|
||||
{
|
||||
"id": "positron",
|
||||
"title": "Positron",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.8/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
|
||||
},
|
||||
{
|
||||
"id": "osm-bright",
|
||||
"title": "OSM Bright",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.9/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
|
||||
},
|
||||
{
|
||||
"id": "maptiler-toner-gl-style",
|
||||
"title": "Toner",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@v1.0/style.json",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@339e5b7/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-light",
|
||||
"title": "Zoomstack Light",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-light/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-night",
|
||||
"title": "Zoomstack Night",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-night/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-outdoor",
|
||||
"title": "Zoomstack Outdoor",
|
||||
@@ -48,33 +48,21 @@
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
|
||||
},
|
||||
{
|
||||
"id": "osm-bright",
|
||||
"title": "OSM Bright",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.11/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
|
||||
"id": "os-zoomstack-light",
|
||||
"title": "Zoomstack Light",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-light/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
|
||||
},
|
||||
{
|
||||
"id": "osm-liberty",
|
||||
"title": "OSM Liberty",
|
||||
"url": "https://maputnik.github.io/osm-liberty/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png"
|
||||
"id": "os-zoomstack-night",
|
||||
"title": "Zoomstack Night",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-night/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
|
||||
},
|
||||
{
|
||||
"id": "positron",
|
||||
"title": "Positron",
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.9/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
|
||||
},
|
||||
{
|
||||
"id": "stadia-outdoors",
|
||||
"title": "Stadia Outdoors",
|
||||
"url": "https://tiles.stadiamaps.com/styles/outdoors.json",
|
||||
"thumbnail": "https://tiles.stadiamaps.com/static/outdoors.png?size=480x320¢er=47.350259,8.49035&zoom=16"
|
||||
},
|
||||
{
|
||||
"id": "versatiles-colorful",
|
||||
"title": "Versatiles Colorful",
|
||||
"url": "https://tiles.versatiles.org/assets/styles/colorful.json",
|
||||
"thumbnail": "https://github.com/maplibre/maputnik/assets/649392/6cd69818-c541-46e4-a920-65fb4f654931"
|
||||
"id": "empty-style",
|
||||
"title": "Empty Style",
|
||||
"url": "https://cdn.jsdelivr.net/gh/maputnik/editor@9cf74ca405d2be0608b57db8109cf3a6af5b9f49/src/config/empty-style.json",
|
||||
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"openmaptiles": {
|
||||
"type": "vector",
|
||||
"url": "https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key={key}",
|
||||
"url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}",
|
||||
"title": "OpenMapTiles v3"
|
||||
},
|
||||
"thunderforest_transport": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user