Compare commits

..

130 Commits

Author SHA1 Message Date
HarelM
6010c6810a deploy: f24031dd5c 2026-01-20 05:46:30 +00:00
HarelM
7943252400 deploy: c3a35354f7 2025-12-17 08:28:29 +00:00
HarelM
72b9e624a7 deploy: c168e65d86 2025-12-03 11:40:13 +00:00
HarelM
4c847faaae deploy: 74a21e1b77 2025-12-02 10:04:09 +00:00
HarelM
7089ca1b5c deploy: 9d19ab8606 2025-12-02 09:34:09 +00:00
HarelM
2cd6019cb2 deploy: 522815b4c5 2025-11-20 10:56:01 +00:00
HarelM
775b5f4a39 deploy: 7a93d592ff 2025-11-09 21:58:08 +00:00
HarelM
93baf3d5e2 deploy: 876a3d70df 2025-11-09 11:03:19 +00:00
HarelM
74f44a49af deploy: 0fbba4b362 2025-11-06 14:32:20 +00:00
HarelM
6310d6f11e deploy: 4ba09144e9 2025-11-06 12:19:37 +00:00
HarelM
8153050d38 deploy: 5b34a3791f 2025-11-04 07:12:13 +00:00
HarelM
917da6bc7d deploy: 696e43b474 2025-11-02 13:38:53 +00:00
HarelM
5f0ea7be24 deploy: fe2571addb 2025-10-17 23:43:49 +00:00
HarelM
8db5842e85 deploy: 7e784f80f6 2025-10-17 20:17:50 +00:00
HarelM
5a8effb363 deploy: 8cd5e28f3a 2025-10-10 13:41:06 +00:00
HarelM
236058e95b deploy: 39d63ec7b1 2025-10-05 13:39:07 +00:00
HarelM
2da67c7963 deploy: 1730e9cb1c 2025-09-17 17:52:33 +00:00
HarelM
f67704c545 deploy: 3c3fcadbb6 2025-09-16 13:43:10 +00:00
HarelM
83b96e8f80 deploy: b42afd0027 2025-09-15 05:15:59 +00:00
birkskyum
59cf0fc47c deploy: 5312d61598 2025-09-14 09:49:28 +00:00
HarelM
bc8f9fd685 deploy: 56cdfd23df 2025-09-14 09:13:23 +00:00
HarelM
542e7777d8 deploy: 69143ea5d6 2025-09-14 08:14:50 +00:00
HarelM
12c5dd912b deploy: a322afdcee 2025-09-13 22:47:26 +00:00
HarelM
95dbed3f69 deploy: c6f599cc61 2025-09-13 20:12:13 +00:00
HarelM
3acf7ccf5b deploy: 42e1273241 2025-09-11 17:44:17 +00:00
HarelM
6c4758c89c deploy: d81316435b 2025-09-11 10:10:22 +00:00
HarelM
9bfea8f868 deploy: c3c6118df1 2025-09-10 11:45:37 +00:00
HarelM
2d2a4739f1 deploy: 9c85883b8a 2025-09-10 11:38:22 +00:00
HarelM
fd218db356 deploy: 3725f83b48 2025-09-09 22:53:51 +00:00
HarelM
bc2b08ed92 deploy: 7bfc3188f7 2025-09-09 22:47:57 +00:00
HarelM
9af15e5359 deploy: 25d6e9693d 2025-09-09 22:18:15 +00:00
HarelM
669781ccca deploy: 7fc334ad85 2025-09-09 14:33:27 +00:00
HarelM
18da95d2a6 deploy: 55a487d0c8 2025-09-08 19:09:30 +00:00
HarelM
b47d105e1f deploy: abe6230932 2025-09-08 12:23:28 +00:00
HarelM
5f9c21cf2b deploy: 6f4c34b29a 2025-09-07 14:43:38 +00:00
HarelM
956d24c524 deploy: 54c1b761fd 2025-09-07 14:30:51 +00:00
HarelM
4e02c6e12d deploy: 4d5c74f4ee 2025-09-07 10:06:13 +00:00
HarelM
e9966e5a20 deploy: e2e29d7f5e 2025-08-17 08:07:25 +00:00
birkskyum
3deb491306 deploy: 727bc7dfae 2025-08-07 18:35:15 +00:00
louwers
34572dc3f0 deploy: b2fa703ceb 2025-07-10 04:07:56 +00:00
louwers
6bf79c2121 deploy: 3ddb55aec7 2025-07-05 18:31:48 +00:00
louwers
a925995f89 deploy: 2fef0467b6 2025-07-05 12:47:58 +00:00
louwers
c674575fbc deploy: eb985f4d95 2025-07-05 11:59:23 +00:00
louwers
b030a2a707 deploy: c486aa2139 2025-07-05 11:56:00 +00:00
louwers
74aa3b48db deploy: 533f647c71 2025-07-05 09:33:30 +00:00
louwers
a399df0adc deploy: 4b977fd33e 2025-07-04 21:50:06 +00:00
louwers
1282062b32 deploy: e58b92b0cd 2025-07-04 20:22:52 +00:00
louwers
bb243db63c deploy: 599240033a 2025-07-04 08:28:00 +00:00
HarelM
45c1281490 deploy: 851e4bad21 2025-06-13 04:20:07 +00:00
nyurik
40e452d547 deploy: 19389ca3d3 2025-05-28 18:39:19 +00:00
HarelM
2e62b1802a deploy: 4f52df7c3b 2025-05-04 04:40:44 +00:00
HarelM
ea4c3f4e3e deploy: b6afbb0321 2025-04-21 19:45:41 +00:00
HarelM
19c538a29e deploy: d691d49538 2025-03-28 13:34:48 +00:00
HarelM
d379d462f2 deploy: 699241b691 2025-03-14 20:35:35 +00:00
nyurik
8eb9fe062f deploy: 9540686b40 2025-02-25 10:02:16 +00:00
HarelM
9ca274805c deploy: da361509d2 2025-02-04 06:36:51 +00:00
birkskyum
fc507c7e79 deploy: abf3bd1fa0 2025-01-28 12:58:40 +00:00
HarelM
66453a46ca deploy: b87c8fb5c3 2025-01-25 07:25:28 +00:00
HarelM
96b0c53fd2 deploy: 5af2cc2f9e 2025-01-23 09:00:05 +00:00
birkskyum
663034b749 deploy: dcdbac35ff 2025-01-22 11:43:15 +00:00
birkskyum
c82696d268 deploy: 2852fa62ff 2025-01-22 10:39:26 +00:00
birkskyum
7d987cf68b deploy: 87cd79e86f 2025-01-21 21:39:06 +00:00
birkskyum
075437555a deploy: cd7d607f13 2025-01-21 15:22:33 +00:00
HarelM
654dc9c31b deploy: b429bb16d7 2025-01-21 14:26:07 +00:00
birkskyum
2c8bc5aa04 deploy: a21efcc4d5 2025-01-21 13:48:26 +00:00
HarelM
07bee66764 deploy: 3e6994084c 2025-01-21 13:25:16 +00:00
HarelM
f675c7ff7b deploy: 69e4888d71 2025-01-21 12:25:50 +00:00
HarelM
ad85fd8f12 deploy: 2a3e7ea4bb 2025-01-21 12:09:54 +00:00
HarelM
634d664e46 deploy: a97287a66e 2025-01-21 12:09:04 +00:00
HarelM
0046122c87 deploy: 9a866179b7 2025-01-21 12:01:16 +00:00
HarelM
c0f798a6f6 deploy: 84e9a73d86 2025-01-21 12:00:58 +00:00
HarelM
47941e3738 deploy: 53fbc1ffe9 2025-01-21 11:57:51 +00:00
HarelM
237457a159 deploy: b357e1f352 2025-01-21 11:57:31 +00:00
HarelM
d9dad5614e deploy: c5f6d51ea1 2025-01-21 11:54:31 +00:00
HarelM
f18a594131 deploy: 117f37139e 2025-01-21 11:53:25 +00:00
HarelM
fde3d8fc18 deploy: 39aef39b72 2025-01-21 11:52:48 +00:00
HarelM
a493d6df52 deploy: 1657aa4676 2025-01-21 11:50:24 +00:00
HarelM
7e8eca6f97 deploy: b09d41e41d 2025-01-21 11:49:20 +00:00
HarelM
c3764b65d9 deploy: f17f529ede 2025-01-21 11:48:57 +00:00
HarelM
56e151329d deploy: f12af91a55 2025-01-21 11:48:16 +00:00
HarelM
28d6589928 deploy: e87e122067 2025-01-21 11:47:53 +00:00
birkskyum
7adf516383 deploy: 6af3165418 2025-01-21 10:23:43 +00:00
birkskyum
9a1385823e deploy: b1d4b53548 2025-01-21 09:51:57 +00:00
HarelM
2fc5ab4509 deploy: 69124d0752 2025-01-21 08:49:52 +00:00
HarelM
3108b88e59 deploy: 405b8aa951 2025-01-16 21:54:38 +00:00
HarelM
b910e4fdb6 deploy: d50ea76347 2025-01-09 16:55:36 +00:00
louwers
907c09a927 deploy: c6174a57d9 2024-11-25 11:41:18 +00:00
HarelM
16fb99d9b1 deploy: af01346279 2024-11-14 21:54:28 +00:00
HarelM
a364176a3e deploy: 687f9abaf2 2024-10-31 19:04:51 +00:00
HarelM
48164f5a9d deploy: 172d4d5278 2024-10-15 07:13:06 +00:00
HarelM
046b1b3bb2 deploy: b03af2c039 2024-09-29 13:10:39 +00:00
HarelM
f33e09df62 deploy: 6089cde302 2024-09-29 12:02:38 +00:00
HarelM
7333eb6378 deploy: 25cf61a825 2024-09-25 07:23:54 +00:00
HarelM
ffdc04b3aa deploy: 0f1000c5b0 2024-09-18 04:02:23 +00:00
louwers
223809dda5 deploy: fa4ece22cf 2024-09-16 23:39:50 +00:00
HarelM
744ad0f917 deploy: 00f431c50e 2024-09-03 05:25:36 +00:00
HarelM
2f324c695b deploy: 60785f53bc 2024-09-02 09:45:51 +00:00
HarelM
dd4cbb1b3d deploy: 32fa02d289 2024-09-01 06:59:24 +00:00
HarelM
e61aa393c6 deploy: 482e6ec545 2024-08-29 19:15:18 +00:00
HarelM
e9c24d5ac9 deploy: 4dd34e99fd 2024-08-29 17:23:36 +00:00
HarelM
a7ed7cdb45 deploy: 66c5a5c953 2024-08-29 14:08:36 +00:00
HarelM
dea98ad7b6 deploy: 8184ac8393 2024-08-21 05:58:07 +00:00
HarelM
987c3cd31e deploy: 6a0d2e8ee5 2024-08-21 04:18:41 +00:00
HarelM
6d970fe73f deploy: 58edd262b0 2024-08-19 09:44:12 +00:00
nyurik
9cfd0ced73 deploy: 35840409b8 2024-07-26 02:51:28 +00:00
HarelM
1464a337e3 deploy: d0f6e0fadb 2024-07-12 18:19:25 +00:00
HarelM
1eaba084ed deploy: 0de304ca3e 2024-06-24 05:10:30 +00:00
HarelM
37ba7457d5 deploy: c82c6158e6 2024-05-29 07:48:23 +00:00
HarelM
43a7c058fd deploy: 41cd7dfad1 2024-05-20 11:44:06 +00:00
HarelM
87c7c7ff93 deploy: 7591b031ce 2024-04-16 05:27:23 +00:00
HarelM
8c8241b13b deploy: f34529ef06 2024-04-03 17:27:41 +00:00
HarelM
c187f02c27 deploy: a73b11805d 2024-03-26 23:13:29 +00:00
HarelM
617adcdc48 deploy: ff15b77b7f 2024-03-21 20:52:33 +00:00
HarelM
e71c49e38c deploy: 355b663e7e 2024-03-14 19:03:54 +00:00
HarelM
9572eefd48 deploy: 3c043fd5e0 2024-03-13 20:49:07 +00:00
HarelM
3303a25737 deploy: 5f54dd0ccf 2024-03-09 21:04:29 +00:00
HarelM
49f91a69f1 deploy: 3727f5da48 2024-02-21 05:18:43 +00:00
HarelM
c853c754b1 deploy: bc5ecfade6 2024-02-07 08:33:35 +00:00
HarelM
1b5596052c deploy: c84c7a7b96 2024-02-04 14:39:01 +00:00
HarelM
1c1b5cd208 deploy: cb77c6b4e2 2024-02-04 09:38:30 +00:00
HarelM
c948814efc deploy: ea42f434eb 2024-02-04 08:02:30 +00:00
HarelM
7b9d3512c6 deploy: 6f82c12861 2024-02-04 07:19:30 +00:00
HarelM
edf3a58ea6 deploy: 3b95b25777 2024-01-13 12:40:08 +00:00
HarelM
5a4b5fb9e9 deploy: 1da65f2116 2024-01-12 16:47:24 +00:00
HarelM
6f9f53add6 deploy: a62db148cd 2024-01-12 09:01:06 +00:00
HarelM
e0199c9ce7 deploy: 6ed10a862f 2024-01-11 20:58:23 +00:00
Harel M
e1ed42f16f Fix static files reference 2024-01-11 20:00:59 +00:00
Harel M
104c3c0c10 another attempt to fix base-href 2024-01-11 19:53:30 +00:00
Harel M
f4a1aa4729 Add base tag to html 2024-01-11 19:48:33 +00:00
Harel M
fda7fac260 Publish the current version of maputnik 2024-01-11 19:45:24 +00:00
222 changed files with 990 additions and 28564 deletions

View File

@@ -1,45 +0,0 @@
.git
.gitignore
Dockerfile
#
#
# COPIED FROM .gitignore , please keep it in sync
#
#
# Logs
logs
*.log
*.swp
*.swo
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
# Ignore build files
public
/errorShots
/old
/build

View File

@@ -1,14 +0,0 @@
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,jsx,html,sass}]
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

View File

@@ -1,46 +0,0 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
"ignorePatterns": [
"dist"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"settings": {
"react": { "version": "16.4" }
},
"plugins": [
"@typescript-eslint",
"react-refresh"],
"rules": {
"react-refresh/only-export-components": [
"warn",
{ "allowConstantExport": true }
],
"@typescript-eslint/no-unused-vars": [
"warn",
{ "argsIgnorePattern": "^_" }
],
"no-unused-vars": "off",
"react/prop-types": ["off"],
// Disable no-undef. It's covered by @typescript-eslint
"no-undef": "off",
"indent": ["error", 2],
"no-var": ["error"]
},
"globals": {
"global": "readonly"
}
}

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +0,0 @@
github: [maplibre]
open_collective: maplibre

View File

@@ -1,27 +0,0 @@
---
name: Bug report
about: Create a report to help us improve Maputnik
title: ''
labels: ''
assignees: ''
---
<!-- Thanks for your feedback! Please complete the following information: -->
**Maputnik version**:<!-- e.g v1.7.0, main -->
**Browser**:
**OS**:<!-- (Windows, macOS, Linux) -->
**Description of the bug**:
**Steps to reproduce the behavior**:
1.
2.
3.
**Style file or style URL**:
<!-- If applicable, attach a style file (zip) or provide a style URL. -->
**Screenshots**:
<!-- If applicable, add screenshots to help explain your problem. -->

View File

@@ -1,11 +0,0 @@
---
name: Other issue
about: Feature request or other issue which is no bug report
title: ''
labels: ''
assignees: ''
---
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->

View File

@@ -1,11 +0,0 @@
## Launch Checklist
<!-- Thanks for the PR! Feel free to add or remove items from the checklist. -->
- [ ] Briefly describe the changes in this PR.
- [ ] Link to related issues.
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
- [ ] Write tests for all new functionality.
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.

View File

@@ -1,14 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 2
versioning-strategy: increase
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 2
versioning-strategy: increase

View File

@@ -1,113 +0,0 @@
name: ci
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
build-docker:
name: build docker
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@v4
- run: docker build -t test-docker-image-build .
# build the editor
build-node:
name: "build on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- run: npm run lint
- run: npm run lint-css
build-artifacts:
name: "build artifacts"
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- name: artifacts/maputnik
uses: actions/upload-artifact@v4
with:
name: maputnik
path: dist
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Build desktop artifacts
run: npm run build-desktop
- name: Artifacts/linux
uses: actions/upload-artifact@v4
with:
name: maputnik-linux
path: ./desktop/bin/linux/
- name: Artifacts/darwin
uses: actions/upload-artifact@v4
with:
name: maputnik-darwin
path: ./desktop/bin/darwin/
- name: Artifacts/windows
uses: actions/upload-artifact@v4
with:
name: maputnik-windows
path: ./desktop/bin/windows/
e2e-tests:
name: "E2E tests using ${{ matrix.browser }}"
strategy:
fail-fast: false
matrix:
browser: [chrome, firefox]
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- run: npm ci
- name: Cypress run
uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm run start
browser: ${{ matrix.browser }}
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
files: ${{ github.workspace }}/.nyc_output/out.json
verbose: true

View File

@@ -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 }}

View File

@@ -1,51 +0,0 @@
name: deploy
on:
push:
branches: [ main ]
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
if: ${{ github.event_name == 'push' }}
strategy:
fail-fast: false
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

View File

@@ -1,104 +0,0 @@
name: Release
on:
push:
branches: [main]
workflow_dispatch:
jobs:
release-check:
name: Check if version changed
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Check if version has been updated
id: check
uses: EndBug/version-check@v2
outputs:
publish: ${{ steps.check.outputs.changed }}
release:
name: Release
needs: release-check
if: ${{ needs.release-check.outputs.publish == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build
uses: actions/setup-go@v5
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.3.1
- name: Install
run: npm ci
- name: Build
run: |
npm run build
npm run build-desktop
- name: Tag commit and push
id: tag_version
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ steps.package-version.outputs.current-version }}
- name: Create Archives
run: |
zip -r dist dist
zip -r desktop desktop/bin/
- name: Build Release Notes
id: release_notes
run: |
RELEASE_NOTES_PATH="${PWD}/release_notes.txt"
./build/release-notes.js > ${RELEASE_NOTES_PATH}
echo "release_notes=${RELEASE_NOTES_PATH}" >> $GITHUB_OUTPUT
- name: Create GitHub Release
id: create_regular_release
uses: ncipollo/release-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: ${{ steps.tag_version.outputs.new_tag }}
bodyFile: ${{ steps.release_notes.outputs.release_notes }}
artifacts: "dist.zip,desktop.zip"
allowUpdates: true
draft: false
prerelease: false

38
.gitignore vendored
View File

@@ -1,38 +0,0 @@
# Logs
logs
*.log
*.swp
*.swo
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
# Ignore build files
public
/errorShots
/old
/cypress/screenshots
/dist/
/desktop/version.go

0
.nojekyll Normal file
View File

1
.npmrc
View File

@@ -1 +0,0 @@
legacy-peer-deps = true

1
.nvmrc
View File

@@ -1 +0,0 @@
18.19

View File

@@ -1,18 +0,0 @@
{
"all": true,
"extends": "@istanbuljs/nyc-config-typescript",
"check-coverage": false,
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": [
"cypress/**/*.*",
"**/*.d.ts",
"**/*.cy.tsx",
"**/*.cy.ts",
"./coverage/**",
"./cypress/**",
"./dist/**",
"node_modules"
],
"report-dir": "coverage",
"reporter": ["json", "lcov", "json-summary"]
}

View File

@@ -1,15 +0,0 @@
{
"labels": {
"bug": 5,
"maintenance": 3,
"mentioned in the 1st survey": 2
},
"reactions": {
"+1": 2,
"-1": -1,
"laugh": 1,
"hooray": 2,
"confused": 1,
"heart": 2
}
}

View File

@@ -1,24 +0,0 @@
## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
## 2.1.1
### ✨ Features and improvements
- Add GitHub workflows for releasing new versions
- Update desktop build to pull from this repo (#922)
## 2.0.0
- Update MapLibre to version 4 (#872)
- Start continuous deployment of maputnik website
## 1.7.0
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0

View File

@@ -1,2 +0,0 @@
# Contributor Covenant
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)

View File

@@ -1,16 +0,0 @@
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
# Build maputnik
COPY . .
RUN npx vite build
#---------------------------------------------------------------------------
# Create a clean nginx-alpine slim image with just the build results
FROM nginx:alpine-slim
COPY --from=builder /maputnik/dist /usr/share/nginx/html/

23
LICENSE
View File

@@ -1,23 +0,0 @@
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
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.

116
README.md
View File

@@ -1,116 +0,0 @@
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
# Maputnik
[![GitHub CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci]
[![License](https://img.shields.io/badge/license-MIT-blue.svg)][license]
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci
[license]: https://tldrlegal.com/license/mit-license
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
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
- 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
```
## Documentation
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)**
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
[![Design Map from Scratch](https://j.gifs.com/g5XMgl.gif)](https://youtu.be/XoDh0gEnBQo)
## 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/).
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
# install dependencies
npm install
# start dev server
npm run start
```
If you want Maputnik to be accessible externally use the [`--host` option](https://vitejs.dev/config/server-options.html#server-host):
```bash
# start externally accessible dev server
npm run start -- --host 0.0.0.0
```
The build process will watch for changes to the filesystem, rebuild and autoreload the editor.
```
npm run build
```
Lint the JavaScript code.
```
# run linter
npm run lint
npm run lint-css
npm run sort-styles
```
## Tests
For E2E testing we use [Cypress](https://www.cypress.io/)
[Cypress](https://www.cypress.io/) doesn't starts a server so you'll need to start one manually by running `npm run start`.
Now open a terminal and run the following using *chrome*:
```
npm run test
```
or *firefox*:
```
npm run test -- --browser firefox
```
See the following docs for more info: (Launching Browsers)[https://docs.cypress.io/guides/guides/launching-browsers]
You can also see the tests as they run or select which suites to run by executing:
```
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.
## 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/.
## 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.

View File

@@ -1,2 +0,0 @@
For an up-to-date policy refer to
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt

View File

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

962
assets/index-BvteS-mA.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"translation-BhJ-ufwk.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"translation-BrzYPxJn.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"translation-CQD4fuPu.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"translation-CZ64AJ8H.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"translation-DvW-3CJ8.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"translation-XoriI0W-.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"translation-aD1CAGoy.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

View File

@@ -1,11 +0,0 @@
# Build Scripts
This folder holds common build scripts used by some of the Github workflows.
The scripts are borrowed from [maplibre/maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/tree/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build).
## Generate Release Notes
`bump-version-changelog.js` Used to update the changelog with the current notes, and set up a space for new notes
`release-notes.js` Used to generate release notes when releasing a new version

View File

@@ -1,29 +0,0 @@
#!/usr/bin/env node
/**
* This script updates the changelog.md file with the version given in the arguments
* It replaces ## main with ## <version>
* Removes _...Add new stuff here..._
* And adds on top a ## main with add stuff here.
*
* Copied from maplibre/maplibre-gl-js
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
*/
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
let changelog = fs.readFileSync(changelogPath, 'utf8');
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
changelog = `## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
` + changelog;
fs.writeFileSync(changelogPath, changelog, 'utf8');

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env node
// Copied from maplibre/maplibre-gl-js
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
const changelog = fs.readFileSync(changelogPath, 'utf8');
/*
Parse the raw changelog text and split it into individual releases.
This regular expression:
- Matches lines starting with "## x.x.x".
- Groups the version number.
- Skips the (optional) release date.
- Groups the changelog content.
- Ends when another "## x.x.x" is found.
*/
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
let releaseNotes = [];
let match;
// eslint-disable-next-line no-cond-assign
while (match = regex.exec(changelog)) {
releaseNotes.push({
'version': match[1],
'changelog': match[2].trim(),
});
}
const latest = releaseNotes[0];
const previous = releaseNotes[1];
// Print the release notes template.
let header = 'Changes since previous version'
if (previous) {
header = `https://github.com/maplibre/maputnik
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`
}
const templatedReleaseNotes = `${header}
${latest.changelog}`;
// eslint-disable-next-line eol-last
process.stdout.write(templatedReleaseNotes.trimEnd());

View File

@@ -1,23 +0,0 @@
import { defineConfig } from "cypress";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
export default defineConfig({
env: {
codeCoverage: {
exclude: "cypress/**/*.*",
},
},
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
require("@cypress/code-coverage/task")(on, config);
return config;
},
baseUrl: "http://localhost:8888",
retries: {
runMode: 2,
openMode: 0,
},
},
});

View File

@@ -1,40 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("accessibility", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("skip links", () => {
beforeEach(() => {
when.setStyle("layer");
});
it("skip link to layer list", () => {
const selector = "root:skip:layer-list";
then(get.elementByTestId(selector)).shouldExist();
when.tab();
then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector);
then(get.skipTargetLayerList()).shouldBeFocused();
});
// 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();
when.tab().tab();
then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector);
then(get.skipTargetLayerEditor()).shouldBeFocused();
});
it("skip link to map view", () => {
const selector = "root:skip:map-view";
then(get.elementByTestId(selector)).shouldExist();
when.tab().tab().tab();
then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector);
then(get.canvas()).shouldBeFocused();
});
});
});

View File

@@ -1,125 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("history", () => {
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter();
let undoKeyCombo: string;
let redoKeyCombo: string;
before(() => {
const isMac = get.isMac();
undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
});
it("undo/redo", () => {
when.setStyle("geojson");
when.modal.open();
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.modal.fillLayers({
id: "step 1",
type: "background",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
],
});
when.modal.open();
when.modal.fillLayers({
id: "step 2",
type: "background",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
{
id: "step 2",
type: "background",
},
],
});
when.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
],
});
when.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.typeKeys(redoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
],
});
when.typeKeys(redoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
{
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",
},
],
});
});
});

View File

@@ -1,35 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("i18n", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("language detector", () => {
it("English", () => {
const url = "?lng=en";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
});
it("Japanese", () => {
const url = "?lng=ja";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
describe("language switcher", () => {
beforeEach(() => {
when.setStyle("layer");
});
it("the language switcher switches to Japanese", () => {
const selector = "maputnik-lang-select";
then(get.elementByTestId(selector)).shouldExist();
when.select(selector, "ja");
then(get.elementByTestId(selector)).shouldHaveValue("ja");
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
});
});
});

View File

@@ -1,60 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("keyboard", () => {
let { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
beforeAndAfter();
describe("shortcuts", () => {
beforeEach(() => {
given.setupMockBackedResponses();
when.setStyle("");
});
it("ESC should unfocus", () => {
const targetSelector = "maputnik-select";
when.focus(targetSelector);
then(get.elementByTestId(targetSelector)).shouldBeFocused();
when.typeKeys("{esc}");
then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
});
it("'?' should show shortcuts modal", () => {
when.typeKeys("?");
then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
});
it("'o' should show open modal", () => {
when.typeKeys("o");
then(get.elementByTestId("modal:open")).shouldBeVisible();
});
it("'e' should show export modal", () => {
when.typeKeys("e");
then(get.elementByTestId("modal:export")).shouldBeVisible();
});
it("'d' should show sources modal", () => {
when.typeKeys("d");
then(get.elementByTestId("modal:sources")).shouldBeVisible();
});
it("'s' should show settings modal", () => {
when.typeKeys("s");
then(get.elementByTestId("modal:settings")).shouldBeVisible();
});
it("'i' should change map to inspect mode", () => {
when.typeKeys("i");
then(get.inputValue("maputnik-select")).shouldEqual("inspect");
});
it("'m' should focus map", () => {
when.typeKeys("m");
then(get.canvas()).shouldBeFocused();
});
it("'!' should show debug modal", () => {
when.typeKeys("!");
then(get.elementByTestId("modal:debug")).shouldBeVisible();
});
});
});

View File

@@ -1,497 +0,0 @@
import { v1 as uuid } from "uuid";
import { MaputnikDriver } from "./maputnik-driver";
describe("layers", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => {
when.setStyle("both");
when.modal.open();
});
describe("ops", () => {
let id: string;
beforeEach(() => {
id = when.modal.fillLayers({
type: "background",
});
});
it("should update layers in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
},
],
});
});
describe("when clicking delete", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":delete");
});
it("should empty layers in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [],
});
});
});
describe("when clicking duplicate", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":copy");
});
it("should add copy layer in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id + "-copy",
type: "background",
},
{
id: id,
type: "background",
},
],
});
});
});
describe("when clicking hide", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility");
});
it("should update visibility to none in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
layout: {
visibility: "none",
},
},
],
});
});
describe("when clicking show", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility");
});
it("should update visibility to visible in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
layout: {
visibility: "visible",
},
},
],
});
});
});
});
});
describe("background", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "background",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
},
],
});
});
describe("modify", () => {
function createBackground() {
// Setup
let id = uuid();
when.selectWithin("add-layer.layer-type", "background");
when.setValue("add-layer.layer-id.input", "background:" + id);
when.click("add-layer");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + id,
type: "background",
},
],
});
return id;
}
// ====> THESE SHOULD BE FROM THE SPEC
describe("layer", () => {
it("expand/collapse");
it("id", () => {
let bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
let id = uuid();
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
when.click("min-zoom");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "foobar:" + id,
type: "background",
},
],
});
});
describe("min-zoom", () => {
let bgId: string;
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: [
{
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}");
when.click("max-zoom.input-text");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
minzoom: 1,
},
],
});
});
});
describe("max-zoom", () => {
let bgId: string;
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: [
{
id: "background:" + bgId,
type: "background",
maxzoom: 1,
},
],
});
});
});
describe("comments", () => {
let bgId: string;
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: [
{
id: "background:" + bgId,
type: "background",
metadata: {
"maputnik:comment": comment,
},
},
],
});
});
describe("when unsetting", () => {
beforeEach(() => {
when.clear("layer-comment.input");
when.click("min-zoom.input-text");
});
it("should update style in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
},
],
});
});
});
});
describe("color", () => {
let bgId: string;
beforeEach(() => {
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: [
{
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");
it("compound filter");
});
describe("paint", () => {
it("expand/collapse");
it("color");
it("pattern");
it("opacity");
});
// <=====
describe("json-editor", () => {
it("expand/collapse");
it("modify");
// TODO
it.skip("parse error", () => {
let bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
let errorSelector = ".CodeMirror-lint-marker-error";
then(get.elementByTestId(errorSelector)).shouldNotExist();
when.click(".CodeMirror");
when.typeKeys(
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
);
then(get.elementByTestId(errorSelector)).shouldExist();
});
});
});
});
describe("fill", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "fill",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "fill",
source: "example",
},
],
});
});
// TODO: Change source
it("change source");
});
describe("line", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "line",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "line",
source: "example",
},
],
});
});
it("groups", () => {
// TODO
// Click each of the layer groups.
});
});
describe("symbol", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "symbol",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "symbol",
source: "example",
},
],
});
});
});
describe("raster", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "raster",
layer: "raster",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "raster",
source: "raster",
},
],
});
});
});
describe("circle", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "circle",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "circle",
source: "example",
},
],
});
});
});
describe("fill extrusion", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "fill-extrusion",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "fill-extrusion",
source: "example",
},
],
});
});
});
describe("groups", () => {
it("simple", () => {
when.setStyle("geojson");
when.modal.open();
when.modal.fillLayers({
id: "foo",
type: "background",
});
when.modal.open();
when.modal.fillLayers({
id: "foo_bar",
type: "background",
});
when.modal.open();
when.modal.fillLayers({
id: "foo_bar_baz",
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();
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();
});
});
});

View File

@@ -1,32 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("map", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("zoom level", () => {
it("via url", () => {
let zoomLevel = 12.37;
when.setStyle("geojson", zoomLevel);
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
"Zoom: " + zoomLevel
);
});
it("via map controls", () => {
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();
});
});
});

View File

@@ -1,19 +0,0 @@
import { CypressHelper } from "@shellygo/cypress-test-utils";
export default class MaputnikCypressHelper {
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
public given = {
...this.helper.given,
};
public get = {
...this.helper.get,
};
public when = {
...this.helper.when,
};
public beforeAndAfter = this.helper.beforeAndAfter;
}

View File

@@ -1,186 +0,0 @@
/// <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 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();
private modalDriver = new ModalDriver();
public beforeAndAfter = () => {
beforeEach(() => {
this.given.setupMockBackedResponses();
this.when.setStyle("both");
});
};
public then = (chainable: Cypress.Chainable<any>) =>
new MaputnikAssertable(chainable);
public given = {
...this.helper.given,
setupMockBackedResponses: () => {
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "example-style.json",
response: {
fixture: "example-style.json",
},
alias: "example-style.json",
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "example-layer-style.json",
response: {
fixture: "example-layer-style.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "geojson-style.json",
response: {
fixture: "geojson-style.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "raster-style.json",
response: {
fixture: "raster-style.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "geojson-raster-style.json",
response: {
fixture: "geojson-raster-style.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: "*example.local/*",
response: [],
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: "*example.com/*",
response: [],
});
},
};
public when = {
...this.helper.when,
modal: this.modalDriver.when,
within: (selector: string, fn: () => void) => {
this.helper.when.within(fn, selector);
},
tab: () => this.helper.get.element("body").tab(),
waitForExampleFileResponse: () => {
this.helper.when.waitForResponse("example-style.json");
},
chooseExampleFile: () => {
this.helper.get
.bySelector("type", "file")
.selectFile("cypress/fixtures/example-style.json", { force: true });
},
setStyle: (
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
zoom?: number
) => {
let url = "?debug";
switch (styleProperties) {
case "geojson":
url += `&style=${baseUrl}geojson-style.json`;
break;
case "raster":
url += `&style=${baseUrl}raster-style.json`;
break;
case "both":
url += `&style=${baseUrl}geojson-raster-style.json`;
break;
case "layer":
url += `&style=${baseUrl}/example-layer-style.json`;
break;
}
if (zoom) {
url += `#${zoom}/41.3805/2.1635`;
}
this.helper.when.visit(baseUrl + url);
if (styleProperties) {
this.helper.when.acceptConfirm();
}
// when methods should not include assertions
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
toolbarLink.scrollIntoView();
toolbarLink.should("be.visible");
},
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
clickZoomIn: () => {
this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
},
selectWithin: (selector: string, value: string) => {
this.when.within(selector, () => {
this.helper.get.element("select").select(value);
});
},
select: (selector: string, value: string) => {
this.helper.get.elementByTestId(selector).select(value);
},
focus: (selector: string) => {
this.helper.when.focus(selector);
},
setValue: (selector: string, text: string) => {
this.helper.get
.elementByTestId(selector)
.clear()
.type(text, { parseSpecialCharSequences: false });
},
};
public get = {
...this.helper.get,
isMac: () => {
return Cypress.platform === "darwin";
},
styleFromLocalStorage: () =>
this.helper.get.window().then((win) => styleFromWindow(win)),
exampleFileUrl: () => {
return baseUrl + "example-style.json";
},
skipTargetLayerList: () =>
this.helper.get.elementByTestId("skip-target-layer-list"),
skipTargetLayerEditor: () =>
this.helper.get.elementByTestId("skip-target-layer-editor"),
canvas: () => this.helper.get.element("canvas"),
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
};
}

View File

@@ -1,40 +0,0 @@
import { v1 as uuid } from "uuid";
import MaputnikCypressHelper from "./maputnik-cypress-helper";
export default class ModalDriver {
private helper = new MaputnikCypressHelper();
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;
if (opts.id) {
id = opts.id;
} else {
id = `${type}:${uuid()}`;
}
this.helper.when.selectOption("add-layer.layer-type.select", type);
this.helper.when.type("add-layer.layer-id.input", id);
if (layer) {
this.helper.when.within(() => {
this.helper.get.element("input").type(layer!);
}, "add-layer.layer-source-block");
}
this.helper.when.click("add-layer");
return id;
},
open: () => {
this.helper.when.click("layer-list:add-layer");
},
close: (key: string) => {
this.helper.when.click(key + ".close-modal");
},
};
}

View File

@@ -1,180 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("modals", () => {
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => {
when.setStyle("");
});
describe("open", () => {
beforeEach(() => {
when.click("nav:open");
});
it("close", () => {
when.modal.close("modal:open");
then(get.elementByTestId("modal:open")).shouldNotExist();
});
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();
});
describe("when click open url", () => {
beforeEach(() => {
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();
});
});
});
describe("shortcuts", () => {
it("open/close", () => {
when.setStyle("");
when.typeKeys("?");
when.modal.close("modal:shortcuts");
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
});
});
describe("export", () => {
beforeEach(() => {
when.click("nav:export");
});
it("close", () => {
when.modal.close("modal:export");
then(get.elementByTestId("modal:export")).shouldNotExist();
});
// TODO: Work out how to download a file and check the contents
it("download");
});
describe("sources", () => {
it("active sources");
it("public source");
it("add new source");
});
describe("inspect", () => {
it("toggle", () => {
// There is no assertion in this test
when.setStyle("geojson");
when.select("maputnik-select", "inspect");
});
});
describe("style settings", () => {
beforeEach(() => {
when.click("nav:settings");
});
describe("when click name filed spec information", () => {
beforeEach(() => {
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);
});
it("show name specifications", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
name: "foobar",
});
});
});
describe("when set owner and click name", () => {
beforeEach(() => {
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",
});
});
});
it("sprite url", () => {
when.setValue("modal:settings.sprite", "http://example.com");
when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
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,
});
});
it("maptiler access token", () => {
let apiKey = "testing123";
when.setValue(
"modal:settings.maputnik:openmaptiles_access_token",
apiKey
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({
"maputnik:openmaptiles_access_token": apiKey,
});
});
it("thunderforest access token", () => {
let apiKey = "testing123";
when.setValue(
"modal:settings.maputnik:thunderforest_access_token",
apiKey
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({ "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"
);
when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
metadata: { "maputnik:renderer": "ol" },
});
});
});
describe("sources", () => {
it("toggle");
});
});

View File

@@ -1,18 +0,0 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background"
}
]
}

View File

@@ -1,12 +0,0 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}

View File

@@ -1,34 +0,0 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {
"example": {
"type": "vector",
"data": {
"type": "FeatureCollection",
"features":[{
"type": "Feature",
"properties": {
"name": "Dinagat Islands"
},
"geometry":{
"type": "Point",
"coordinates": [125.6, 10.1]
}
}]
}
},
"raster": {
"tileSize": 256,
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
"type": "raster"
}
},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}

View File

@@ -1,29 +0,0 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {
"example": {
"type": "vector",
"data": {
"type": "FeatureCollection",
"features":[{
"type": "Feature",
"properties": {
"name": "Dinagat Islands"
},
"geometry":{
"type": "Point",
"coordinates": [125.6, 10.1]
}
}]
}
}
},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}

View File

@@ -1,18 +0,0 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {
"raster": {
"tileSize": 256,
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
"type": "raster"
}
},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}

View File

@@ -1,37 +0,0 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

View File

@@ -1,22 +0,0 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "@cypress/code-coverage/support";
import "cypress-plugin-tab";
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

31
desktop/.gitignore vendored
View File

@@ -1,31 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
editor
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Binary version of pubilic/editor
rice-box.go
# Built binary
maputnik

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2016 Maputnik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,39 +0,0 @@
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
BINARY=maputnik
DESKTOP_VERSION := 1.1.1
EDITOR_VERSION := $(shell node -p "require('../package.json').version")
GOPATH := $(if $(GOPATH),$(GOPATH),$(HOME)/go)
GOBIN := $(if $(GOBIN),$(GOBIN),$(HOME)/go/bin)
all: $(BINARY)
$(BINARY): $(GOBIN)/gox $(SOURCES) version.go rice-box.go
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
# Copy the current release into ./editor/maputnik so it can be
# embedded in the binary
editor/pull_release:
mkdir -p editor
cp -r ../dist/* editor
$(GOBIN)/gox:
go install github.com/mitchellh/gox@v1.0.1
$(GOBIN)/rice:
go install github.com/GeertJohan/go.rice/rice@v1.0.3
# Embed the current version numbers in the executable by writing version.go
.PHONY: version.go
version.go:
@echo "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
@echo "package main\n" >> version.go
@echo "const DesktopVersion = \"$(DESKTOP_VERSION)\"" >> version.go
@echo "const EditorVersion = \"$(EDITOR_VERSION)\"" >> version.go
rice-box.go: $(GOBIN)/rice editor/pull_release
$(GOBIN)/rice embed-go
.PHONY: clean
clean:
rm -rf editor && rm -f rice-box.go && rm -rf bin

View File

@@ -1,72 +0,0 @@
# Maputnik Desktop [![GitHub CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci]
---
A Golang based cross platform executable for integrating Maputnik locally.
This binary packages up the JavaScript and CSS bundle produced by maputnik
and embeds it in the program for easy distribution. It also allows
exposing a local style file and work on it both in Maputnik and with your favorite
editor.
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
## Install
You can download a single binary for Linux, OSX or Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/editor/releases/latest).
### Usage
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
```bash
maputnik
```
Expose a local style file to Maputnik allowing the web based editor
to save to the local filesystem.
```bash
maputnik --file basic-v9.json
```
Watch the local style for changes and inform the editor via web socket.
This makes it possible to edit the style with a local text editor and still
use Maputnik.
```bash
maputnik --watch --file basic-v9.json
```
Choose a local port to listen on, instead of using the default port 8000.
```bash
maputnik --port 8001
```
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
Could be used to serve sprites and glyphs.
```bash
maputnik --static ./localFolder
```
### API
`maputnik` exposes the configured styles via a HTTP API.
| Method | Description
|---------------------------------|---------------------------------------
| `GET /styles` | List the ID of all configured style files
| `GET /styles/{filename}` | Get contents of a single style file
| `PUT /styles/{filename}` | Update contents of a style file
| `WEBSOCKET /ws` | Listen to change events for the configured style files
### Build
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
```
npm install
npm run build-desktop
```
You should now find the `maputnik` binary in your `bin` directory.

View File

@@ -1,81 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/gorilla/mux"
)
func StyleFileAccessor(filename string) styleFileAccessor {
return styleFileAccessor{filename, styleId(filename)}
}
func styleId(filename string) string {
raw, err := ioutil.ReadFile(filename)
if err != nil {
log.Panicln(err)
}
var spec styleSpec
err = json.Unmarshal(raw, &spec)
if err != nil {
log.Panicln(err)
}
if spec.Id == "" {
fmt.Println("No id in style")
}
return spec.Id
}
type styleSpec struct {
Id string `json:"id"`
}
// Allows access to a single style file
type styleFileAccessor struct {
filename string
id string
}
func (fa styleFileAccessor) ListFiles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.Encode([]string{fa.id})
}
func (fa styleFileAccessor) ReadFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Choose right file
// right now we just return the single file we know of
w.Header().Set("Content-Type", "application/json")
raw, err := ioutil.ReadFile(fa.filename)
if err != nil {
log.Panicln(err)
}
w.Write(raw)
}
func (fa styleFileAccessor) SaveFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Save to right file
w.Header().Set("Content-Type", "application/json")
body, _ := ioutil.ReadAll(r.Body)
var out bytes.Buffer
json.Indent(&out, body, "", " ")
if err := ioutil.WriteFile(fa.filename, out.Bytes(), 0666); err != nil {
log.Fatalf("Can not copy from request to file: %s", err.Error())
}
}

View File

@@ -1,69 +0,0 @@
package filewatch
import (
"io/ioutil"
"log"
"net/http"
"github.com/fsnotify/fsnotify"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func writer(ws *websocket.Conn, filename string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("Modified file:", event.Name)
var p []byte
var err error
p, err = ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
if p != nil {
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
return
}
}
}
case err := <-watcher.Errors:
log.Println("Watch error:", err)
}
}
}()
if err = watcher.Add(filename); err != nil {
log.Fatal(err)
}
<-done
}
func ServeWebsocketFileWatcher(filename string, w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Println(err)
}
return
}
writer(ws, filename)
defer ws.Close()
}

View File

@@ -1,27 +0,0 @@
module maputnik/desktop
go 1.19
require (
github.com/GeertJohan/go.rice v1.0.3
github.com/fsnotify/fsnotify v1.6.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/maputnik/desktop v1.0.7
github.com/urfave/cli v1.22.12
)
require (
github.com/GeertJohan/go.incremental v1.0.0 // indirect
github.com/akavel/rsrc v0.8.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/daaku/go.zipexe v1.0.2 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/nkovacs/streamquote v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.0.1 // indirect
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
)

View File

@@ -1,54 +0,0 @@
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4=
github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ=
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,80 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/GeertJohan/go.rice"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/maputnik/desktop/filewatch"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "maputnik"
app.Usage = "Server for integrating Maputnik locally"
app.Version = "Editor: " + EditorVersion + "; Desktop: " + DesktopVersion
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "file, f",
Usage: "Allow access to JSON style from web client",
},
&cli.BoolFlag{
Name: "watch",
Usage: "Notify web client about JSON style file changes",
},
&cli.IntFlag{
Name: "port",
Value: 8000,
Usage: "TCP port to listen on",
},
&cli.StringFlag{
Name: "static",
Usage: "Serve directory under /static/",
},
}
app.Action = func(c *cli.Context) error {
gui := http.FileServer(rice.MustFindBox("editor").HTTPBox())
router := mux.NewRouter().StrictSlash(true)
filename := c.String("file")
if filename != "" {
fmt.Printf("%s is accessible via Maputnik\n", filename)
// Allow access to reading and writing file on the local system
path, _ := filepath.Abs(filename)
accessor := StyleFileAccessor(path)
router.Path("/styles").Methods("GET").HandlerFunc(accessor.ListFiles)
router.Path("/styles/{styleId}").Methods("GET").HandlerFunc(accessor.ReadFile)
router.Path("/styles/{styleId}").Methods("PUT").HandlerFunc(accessor.SaveFile)
// Register websocket to notify we clients about file changes
if c.Bool("watch") {
router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
filewatch.ServeWebsocketFileWatcher(filename, w, r)
})
}
}
staticDir := c.String("static")
if staticDir != "" {
h := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
router.PathPrefix("/static/").Handler(h)
}
router.PathPrefix("/").Handler(http.StripPrefix("/", gui))
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter)
fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port"))
return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter)
}
app.Run(os.Args)
}

View File

@@ -1,17 +0,0 @@
export default {
output: 'src/locales/$LOCALE/$NAMESPACE.json',
locales: [ 'ja', 'he','zh' ],
// Because some keys are dynamically generated, i18next-parser can't detect them.
// We add these keys manually, so we don't want to remove them.
keepRemoved: true,
// We use plain English keys, so we disable key and namespace separators.
keySeparator: false,
namespaceSeparator: false,
defaultValue: (locale, ns, key) => {
// The default value is a string that indicates that the string is not translated.
return '__STRING_NOT_TRANSLATED__';
}
}

View File

@@ -4,8 +4,8 @@
<meta charset="utf-8">
<title>Maputnik</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="manifest" href="src/manifest.json">
<link rel="icon" href="src/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="/maputnik/assets/manifest-BrZzkYP9.json">
<link rel="icon" href="/maputnik/assets/favicon-DBn6BKLx.ico" type="image/x-icon" />
<style>
html {
background-color: rgb(28, 31, 36);
@@ -37,6 +37,8 @@
}
</style>
<script type="module" crossorigin src="/maputnik/assets/index-BvteS-mA.js"></script>
<link rel="stylesheet" crossorigin href="/maputnik/assets/index-CuVViU0P.css">
</head>
<body>
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
@@ -123,10 +125,9 @@
<div id="app"></div>
<div class="loading">
<div class="loading__logo">
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
<img inline src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%20width='1200'%20height='1200'%20viewBox='0%200%20100%20100'%3e%3cstyle%3e@keyframes%20circle-anim{0%25,40%25{fill-opacity:0}60%25,to{fill-opacity:1}}.circle0,.circle1,.circle2,.circle3,.circle4,.circle5{stroke-opacity:0;animation-name:circle-anim;will-change:transform;animation-timing-function:east-in-out;animation-duration:800ms;animation-iteration-count:infinite;animation-direction:alternate}.circle0{animation-delay:100ms}.circle1{animation-delay:200ms}.circle2{animation-delay:300ms}.circle3{animation-delay:400ms}.circle4{animation-delay:500ms}.circle5{animation-delay:600ms}%3c/style%3e%3cg%20class='map'%20stroke='%23000'%3e%3cuse%20xlink:href='%23ref-1--map__main'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line1'%20fill='none'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line2'%20fill='none'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line3'%20fill='none'%3e%3c/use%3e%3c/g%3e%3cg%20class='palette'%3e%3cuse%20xlink:href='%23ref-1--palette__main'%20fill='%23fff'%20stroke='%23000'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--palette__inner'%20fill='none'%20stroke='%23000'%3e%3c/use%3e%3cuse%20class='circle5'%20xlink:href='%23ref-1--palette__circle5'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle4'%20xlink:href='%23ref-1--palette__circle4'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20class='circle3'%20xlink:href='%23ref-1--palette__circle3'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle2'%20xlink:href='%23ref-1--palette__circle2'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20class='circle1'%20xlink:href='%23ref-1--palette__circle1'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle0'%20xlink:href='%23ref-1--palette__circle0'%20fill='%234eba6f'%3e%3c/use%3e%3c/g%3e%3cg%20class='brush'%20stroke='%23000'%3e%3cuse%20xlink:href='%23ref-1--brush__bottom'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--brush__top'%20fill='%23fff'%3e%3c/use%3e%3c/g%3e%3cdefs%3e%3cpath%20id='ref-1--map__main'%20stroke-width='2.366'%20stroke-linejoin='round'%20d='M18.84%207.717l15.44%207.542%2015.75-7.762%2015.7%207.857L81.005%207.67%2096.31%2054.052%2073.598%2062.12%2050.93%2053.872l-25.1%208.066-22.668-8.066z'%3e%3c/path%3e%3cpath%20id='ref-1--map__line1'%20d='M65.556%2015.07l7.647%2046.838'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--map__line2'%20d='M50.261%207.422l.717%2046.6'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--map__line3'%20d='M34.011%2015.07l-8.603%2046.6'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--palette__main'%20stroke-width='2.3'%20d='M47.352%2030.887c7.993.226%2016.934%209.725%2017.954%2015.25%201.02%205.527-.743%2011.125-4.298%2013.875-3.554%202.75-8.6%202.905-8.723%208.302-.097%204.237%208.457%208.5%208.088%2015.653-.406%207.857-15.508%2013.15-30.943%206.102-8.556-3.906-14.249-13.653-13.385-26.238C16.833%2052.334%2022.32%2043.658%2027.382%2039c5.977-5.503%2011.977-8.337%2019.97-8.112z'%3e%3c/path%3e%3ccircle%20id='ref-1--palette__inner'%20stroke-width='2.3'%20cx='41.873'%20cy='61.901'%20r='6.389'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle5'%20cy='44.56'%20cx='54.347'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle4'%20cx='40.443'%20cy='41.555'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle3'%20r='4.336'%20cy='51.102'%20cx='29.651'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle2'%20cx='25.293'%20cy='65.836'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle1'%20r='4.336'%20cy='79.326'%20cx='32.764'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle0'%20cx='46.669'%20cy='80.571'%20r='4.336'%3e%3c/circle%3e%3cpath%20id='ref-1--brush__bottom'%20d='M76.333%2089.333c-1.645-9.794-4.375-35.26-4.32-37.887.056-2.627%202.52-4.34%205.36-4.317%202.842.022%205.098%201.87%205.314%204.27.107%201.2-1.576%2028.06-2.318%2037.844-.332%204.374-3.31%204.413-4.036.09z'%20stroke-width='2.3'%20stroke-linejoin='round'%3e%3c/path%3e%3cpath%20id='ref-1--brush__top'%20stroke-linejoin='round'%20stroke-width='2.3'%20d='M77.184%2026.428s-5.621%207.02-5.621%2011.978c0%204.957%202.206%206.878%205.81%206.878%203.606%200%205.148-1.708%205.29-6.736.142-5.028-5.479-12.12-5.479-12.12z'%3e%3c/path%3e%3c/defs%3e%3c/svg%3e" />
</div>
<div class="loading__text">Loading&hellip;</div>
</div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>

11824
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,149 +0,0 @@
{
"name": "maputnik",
"version": "2.1.1",
"description": "A MapLibre GL visual style editor",
"type": "module",
"main": "''",
"scripts": {
"start": "vite",
"build": "tsc && vite build --base=/maputnik/",
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "cypress run",
"cy:open": "cypress open",
"lint-css": "stylelint \"src/styles/*.scss\"",
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
},
"repository": {
"type": "git",
"url": "https://github.com/maplibre/maputnik"
},
"author": "Lukas Martinelli",
"license": "MIT",
"homepage": "https://github.com/maplibre/maputnik#readme",
"dependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@maplibre/maplibre-gl-geocoder": "^1.6.0",
"@maplibre/maplibre-gl-inspect": "^1.6.3",
"@maplibre/maplibre-gl-style-spec": "^20.1.1",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"array-move": "^4.0.0",
"buffer": "^6.0.3",
"classnames": "^2.5.1",
"codemirror": "^5.65.2",
"color": "^4.2.3",
"cypress-plugin-tab": "^1.0.5",
"detect-browser": "^5.3.0",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
"json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.21",
"lodash.capitalize": "^4.2.1",
"lodash.clamp": "^4.0.3",
"lodash.clonedeep": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"maplibre-gl": "^4.1.2",
"maputnik-design": "github:maputnik/design#172b06c",
"ol": "^6.14.1",
"ol-mapbox-style": "^7.1.1",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-accessible-accordion": "^5.0.0",
"react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^5.0.2",
"react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^18.2.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-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0",
"sass": "^1.72.0",
"slugify": "^1.6.6",
"string-hash": "^1.1.3",
"url": "^0.11.3"
},
"jshintConfig": {
"esversion": 6
},
"stylelint": {
"extends": "stylelint-config-recommended-scss",
"rules": {
"no-descending-specificity": null,
"media-feature-name-no-unknown": [
true,
{
"ignoreMediaFeatureNames": [
"prefers-reduced-motion"
]
}
]
}
},
"devDependencies": {
"@cypress/code-coverage": "^3.12.30",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^5.0.5",
"@shellygo/cypress-test-utils": "^2.1.9",
"@types/codemirror": "^5.60.15",
"@types/color": "^3.0.6",
"@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7",
"@types/geojson": "^7946.0.14",
"@types/json-to-ast": "^2.1.4",
"@types/lodash.capitalize": "^4.2.9",
"@types/lodash.clamp": "^4.0.9",
"@types/lodash.clonedeep": "^4.5.9",
"@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-collapse": "^5.0.4",
"@types/react-color": "^3.0.12",
"@types/react-dom": "^18.2.22",
"@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6",
"@types/string-hash": "^1.1.3",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react": "^4.2.1",
"cors": "^2.8.5",
"cypress": "^13.13.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"i18next-parser": "^9.0.1",
"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"
}
}

View File

@@ -1,975 +0,0 @@
// @ts-ignore - this can be easily replaced with arrow functions
import autoBind from 'react-autobind';
import React from 'react'
import cloneDeep from 'lodash.clonedeep'
import clamp from 'lodash.clamp'
import buffer from 'buffer'
import get from 'lodash.get'
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 MapMaplibreGl from './MapMaplibreGl'
import MapOpenLayers from './MapOpenLayers'
import LayerList from './LayerList'
import LayerEditor from './LayerEditor'
import AppToolbar, { MapState } from './AppToolbar'
import AppLayout from './AppLayout'
import MessagePanel from './AppMessagePanel'
import ModalSettings from './ModalSettings'
import ModalExport from './ModalExport'
import ModalSources from './ModalSources'
import ModalOpen from './ModalOpen'
import ModalShortcuts from './ModalShortcuts'
import ModalDebug from './ModalDebug'
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
import style from '../libs/style'
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage'
import { StyleStore } from '../libs/stylestore'
import { ApiStyleStore } from '../libs/apistore'
import { RevisionStore } from '../libs/revisions'
import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal'
import Debug from '../libs/debug'
import { SortEnd } from 'react-sortable-hoc';
import { MapOptions } from 'maplibre-gl';
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
window.Buffer = buffer.Buffer;
function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
const matchesTilehosting = url.match(/\.tilehosting\.com/);
const matchesMaptiler = url.match(/\.maptiler\.com/);
const matchesThunderforest = url.match(/\.thunderforest\.com/);
if (matchesTilehosting || matchesMaptiler) {
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
if (accessToken) {
return url.replace('{key}', accessToken)
}
}
else if (matchesThunderforest) {
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true})
if (accessToken) {
return url.replace('{key}', accessToken)
}
}
else {
return url;
}
}
function updateRootSpec(spec: any, fieldName: string, newValues: any) {
return {
...spec,
$root: {
...spec.$root,
[fieldName]: {
...spec.$root[fieldName],
values: newValues
}
}
}
}
type OnStyleChangedOpts = {
save?: boolean
addRevision?: boolean
initialLoad?: boolean
}
type MappedErrors = {
message: string
parsed?: {
type: string
data: {
index: number
key: string
message: string
}
}
}
type AppState = {
errors: MappedErrors[],
infos: string[],
mapStyle: StyleSpecification & {id: string},
dirtyMapStyle?: StyleSpecification,
selectedLayerIndex: number,
selectedLayerOriginalId?: string,
sources: {[key: string]: SourceSpecification},
vectorLayers: {},
spec: any,
mapView: {
zoom: number,
center: {
lng: number,
lat: number,
},
},
maplibreGlDebugOptions: Partial<MapOptions> & {
showTileBoundaries: boolean,
showCollisionBoxes: boolean,
showOverdrawInspector: boolean,
},
openlayersDebugOptions: {
debugToolbox: boolean,
},
mapState: MapState
isOpen: {
settings: boolean
sources: boolean
open: boolean
shortcuts: boolean
export: boolean
debug: boolean
}
}
export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore;
styleStore: StyleStore | ApiStyleStore;
layerWatcher: LayerWatcher;
constructor(props: any) {
super(props)
autoBind(this);
this.revisionStore = new RevisionStore()
const params = new URLSearchParams(window.location.search.substring(1))
let port = params.get("localport")
if (port == null && (window.location.port !== "80" && window.location.port !== "443")) {
port = window.location.port
}
this.styleStore = new ApiStyleStore({
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
port: port,
host: params.get("localhost")
})
const shortcuts = [
{
key: "?",
handler: () => {
this.toggleModal("shortcuts");
}
},
{
key: "o",
handler: () => {
this.toggleModal("open");
}
},
{
key: "e",
handler: () => {
this.toggleModal("export");
}
},
{
key: "d",
handler: () => {
this.toggleModal("sources");
}
},
{
key: "s",
handler: () => {
this.toggleModal("settings");
}
},
{
key: "i",
handler: () => {
this.setMapState(
this.state.mapState === "map" ? "inspect" : "map"
);
}
},
{
key: "m",
handler: () => {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
}
},
{
key: "!",
handler: () => {
this.toggleModal("debug");
}
},
]
document.body.addEventListener("keyup", (e) => {
if(e.key === "Escape") {
(e.target as HTMLElement).blur();
document.body.focus();
}
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
const shortcut = shortcuts.find((shortcut) => {
return (shortcut.key === e.key)
})
if(shortcut) {
this.setModal("shortcuts", false);
shortcut.handler();
}
}
})
const styleUrl = initialStyleUrl()
if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) {
this.styleStore = new StyleStore()
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
removeStyleQuerystring()
} else {
if(styleUrl) {
removeStyleQuerystring()
}
this.styleStore.init(err => {
if(err) {
console.log('Falling back to local storage for storing styles')
this.styleStore = new StyleStore()
}
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle, {initialLoad: true}))
if(Debug.enabled()) {
Debug.set("maputnik", "styleStore", this.styleStore);
Debug.set("maputnik", "revisionStore", this.revisionStore);
}
})
}
if(Debug.enabled()) {
Debug.set("maputnik", "revisionStore", this.revisionStore);
Debug.set("maputnik", "styleStore", this.styleStore);
}
this.state = {
errors: [],
infos: [],
mapStyle: style.emptyStyle,
selectedLayerIndex: 0,
sources: {},
vectorLayers: {},
mapState: "map",
spec: latest,
mapView: {
zoom: 0,
center: {
lng: 0,
lat: 0,
},
},
isOpen: {
settings: false,
sources: false,
open: false,
shortcuts: false,
export: false,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
debug: false,
},
maplibreGlDebugOptions: {
showTileBoundaries: false,
showCollisionBoxes: false,
showOverdrawInspector: false,
},
openlayersDebugOptions: {
debugToolbox: false,
},
}
this.layerWatcher = new LayerWatcher({
onVectorLayersChange: v => this.setState({ vectorLayers: v })
})
}
handleKeyPress = (e: KeyboardEvent) => {
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
e.preventDefault();
this.onRedo();
}
else if(e.metaKey && e.keyCode === 90) {
e.preventDefault();
this.onUndo();
}
}
else {
if(e.ctrlKey && e.keyCode === 90) {
e.preventDefault();
this.onUndo();
}
else if(e.ctrlKey && e.keyCode === 89) {
e.preventDefault();
this.onRedo();
}
}
}
componentDidMount() {
window.addEventListener("keydown", this.handleKeyPress);
}
componentWillUnmount() {
window.removeEventListener("keydown", this.handleKeyPress);
}
saveStyle(snapshotStyle: StyleSpecification & {id: string}) {
this.styleStore.save(snapshotStyle)
}
updateFonts(urlTemplate: string) {
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
const glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
downloadGlyphsMetadata(glyphUrl, fonts => {
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
})
}
updateIcons(baseUrl: string) {
downloadSpriteMetadata(baseUrl, icons => {
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
})
}
onChangeMetadataProperty = (property: string, value: any) => {
// If we're changing renderer reset the map state.
if (
property === 'maputnik:renderer' &&
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mlgljs')
) {
this.setState({
mapState: 'map'
});
}
const changedStyle = {
...this.state.mapStyle,
metadata: {
...(this.state.mapStyle as any).metadata,
[property]: value
}
}
this.onStyleChanged(changedStyle)
}
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => {
opts = {
save: true,
addRevision: true,
initialLoad: false,
...opts,
};
if (opts.initialLoad) {
this.getInitialStateFromUrl(newStyle);
}
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
// The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here.
const layerErrors: (Error | ValidationError)[] = [];
if (newStyle && newStyle.layers) {
const foundLayers = new global.Map();
newStyle.layers.forEach((layer, index) => {
if (layer.id === "" && foundLayers.has(layer.id)) {
const error = new Error(
`layers[${index}]: duplicate layer id [empty_string], previously used`
);
layerErrors.push(error);
}
foundLayers.set(layer.id, true);
});
}
const mappedErrors = layerErrors.concat(errors).map(error => {
// Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) {
const [, index, message] = dupMatch;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index: parseInt(index, 10),
key: "id",
message,
}
}
}
}
// Special case: Invalid source
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
if (invalidSourceMatch) {
const [, index, message] = invalidSourceMatch;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index: parseInt(index, 10),
key: "source",
message,
}
}
}
}
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) {
const [, index, group, property, message] = layerMatch;
const key = (group && property) ? [group, property].join(".") : property;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index: parseInt(index, 10),
key,
message
}
}
}
}
else {
return {
message: error.message,
};
}
});
let dirtyMapStyle: StyleSpecification | undefined = undefined;
if (errors.length > 0) {
dirtyMapStyle = cloneDeep(newStyle);
errors.forEach(error => {
const {message} = error;
if (message) {
try {
const objPath = message.split(":")[0];
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^[]+/)![0];
unset(dirtyMapStyle, unsetPath);
}
catch (err) {
console.warn(err);
}
}
});
}
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs as string)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite as string)
}
if (opts.addRevision) {
this.revisionStore.addRevision(newStyle);
}
if (opts.save) {
this.saveStyle(newStyle as StyleSpecification & {id: string});
}
this.setState({
mapStyle: newStyle,
dirtyMapStyle: dirtyMapStyle,
errors: mappedErrors,
}, () => {
this.fetchSources();
this.setStateInUrl();
})
}
onUndo = () => {
const activeStyle = this.revisionStore.undo()
const messages = undoMessages(this.state.mapStyle, activeStyle)
this.onStyleChanged(activeStyle, {addRevision: false});
this.setState({
infos: messages,
})
}
onRedo = () => {
const activeStyle = this.revisionStore.redo()
const messages = redoMessages(this.state.mapStyle, activeStyle)
this.onStyleChanged(activeStyle, {addRevision: false});
this.setState({
infos: messages,
})
}
onMoveLayer = (move: SortEnd) => {
let { oldIndex, newIndex } = move;
let layers = this.state.mapStyle.layers;
oldIndex = clamp(oldIndex, 0, layers.length-1);
newIndex = clamp(newIndex, 0, layers.length-1);
if(oldIndex === newIndex) return;
if (oldIndex === this.state.selectedLayerIndex) {
this.setState({
selectedLayerIndex: newIndex
});
}
layers = layers.slice(0);
arrayMoveMutable(layers, oldIndex, newIndex);
this.onLayersChange(layers);
}
onLayersChange = (changedLayers: LayerSpecification[]) => {
const changedStyle = {
...this.state.mapStyle,
layers: changedLayers
}
this.onStyleChanged(changedStyle)
}
onLayerDestroy = (index: number) => {
const layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0);
remainingLayers.splice(index, 1);
this.onLayersChange(remainingLayers);
}
onLayerCopy = (index: number) => {
const layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const clonedLayer = cloneDeep(changedLayers[index])
clonedLayer.id = clonedLayer.id + "-copy"
changedLayers.splice(index, 0, clonedLayer)
this.onLayersChange(changedLayers)
}
onLayerVisibilityToggle = (index: number) => {
const layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const layer = { ...changedLayers[index] }
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
layer.layout = changedLayout
changedLayers[index] = layer
this.onLayersChange(changedLayers)
}
onLayerIdChange = (index: number, _oldId: string, newId: string) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = {
...changedLayers[index],
id: newId
}
this.onLayersChange(changedLayers)
}
onLayerChanged = (index: number, layer: LayerSpecification) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = layer
this.onLayersChange(changedLayers)
}
setMapState = (newState: MapState) => {
this.setState({
mapState: newState
}, this.setStateInUrl);
}
setDefaultValues = (styleObj: StyleSpecification & {id: string}) => {
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
if(metadata['maputnik:renderer'] === undefined) {
const changedStyle = {
...styleObj,
metadata: {
...styleObj.metadata as any,
'maputnik:renderer': 'mlgljs'
}
}
return changedStyle
} else {
return styleObj
}
}
openStyle = (styleObj: StyleSpecification & {id: string}) => {
styleObj = this.setDefaultValues(styleObj)
this.onStyleChanged(styleObj)
}
fetchSources() {
const sourceList: {[key: string]: any} = {};
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) {
if(
!Object.prototype.hasOwnProperty.call(this.state.sources, key) &&
val.type === "vector" &&
Object.prototype.hasOwnProperty.call(val, "url")
) {
sourceList[key] = {
type: val.type,
layers: []
};
let url = val.url;
try {
url = setFetchAccessToken(url!, this.state.mapStyle)
} catch(err) {
console.warn("Failed to setFetchAccessToken: ", err);
}
fetch(url!, {
mode: 'cors',
})
.then(response => response.json())
.then(json => {
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
return;
}
// Create new objects before setState
const sources = Object.assign({}, {
[key]: this.state.sources[key],
});
for(const layer of json.vector_layers) {
(sources[key] as any).layers.push(layer.id)
}
console.debug("Updating source: "+key);
this.setState({
sources: sources
});
})
.catch(err => {
console.error("Failed to process sources for '%s'", url, err);
});
}
else {
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
}
}
if(!isEqual(this.state.sources, sourceList)) {
console.debug("Setting sources");
this.setState({
sources: sourceList
})
}
}
_getRenderer () {
const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
return metadata['maputnik:renderer'] || 'mlgljs';
}
onMapChange = (mapView: {
zoom: number,
center: {
lng: number,
lat: number,
},
}) => {
this.setState({
mapView,
});
}
mapRenderer() {
const {mapStyle, dirtyMapStyle} = this.state;
const mapProps = {
mapStyle: (dirtyMapStyle || mapStyle),
replaceAccessTokens: (mapStyle: StyleSpecification) => {
return style.replaceAccessTokens(mapStyle, {
allowFallback: true
});
},
onDataChange: (e: {map: Map}) => {
this.layerWatcher.analyzeMap(e.map)
this.fetchSources();
},
}
const renderer = this._getRenderer();
let mapElement;
// Check if OL code has been loaded?
if(renderer === 'ol') {
mapElement = <MapOpenLayers
{...mapProps}
onChange={this.onMapChange}
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
onLayerSelect={this.onLayerSelect}
/>
} else {
mapElement = <MapMaplibreGl {...mapProps}
onChange={this.onMapChange}
options={this.state.maplibreGlDebugOptions}
inspectModeEnabled={this.state.mapState === "inspect"}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
onLayerSelect={this.onLayerSelect} />
}
let filterName;
if(this.state.mapState.match(/^filter-/)) {
filterName = this.state.mapState.replace(/^filter-/, "");
}
const elementStyle: {filter?: string} = {};
if (filterName) {
elementStyle.filter = `url('#${filterName}')`;
}
return <div style={elementStyle} className="maputnik-map__container" data-wd-key="maplibre:container">
{mapElement}
</div>
}
setStateInUrl = () => {
const {mapState, mapStyle, isOpen} = this.state;
const {selectedLayerIndex} = this.state;
const url = new URL(location.href);
const hashVal = hash(JSON.stringify(mapStyle));
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
const openModals = Object.entries(isOpen)
.map(([key, val]) => (val === true ? key : null))
.filter(val => val !== null);
if (openModals.length > 0) {
url.searchParams.set("modal", openModals.join(","));
}
else {
url.searchParams.delete("modal");
}
if (mapState === "map") {
url.searchParams.delete("view");
}
else if (mapState === "inspect") {
url.searchParams.set("view", "inspect");
}
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
}
getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
const url = new URL(location.href);
const modalParam = url.searchParams.get("modal");
if (modalParam && modalParam !== "") {
const modals = modalParam.split(",");
const modalObj: {[key: string]: boolean} = {};
modals.forEach(modalName => {
modalObj[modalName] = true;
});
this.setState({
isOpen: {
...this.state.isOpen,
...modalObj,
}
});
}
const view = url.searchParams.get("view");
if (view && view !== "") {
this.setMapState(view as MapState);
}
const path = url.searchParams.get("layer");
if (path) {
try {
const parts = path.split("~");
const [hashVal, selectedLayerIndex] = [
parts[0],
parseInt(parts[1], 10),
];
let valid = true;
if (hashVal !== "-") {
const currentHashVal = hash(JSON.stringify(mapStyle));
if (currentHashVal !== parseInt(hashVal, 10)) {
valid = false;
}
}
if (valid) {
this.setState({
selectedLayerIndex,
selectedLayerOriginalId: mapStyle.layers[selectedLayerIndex].id,
});
}
}
catch (err) {
console.warn(err);
}
}
}
onLayerSelect = (index: number) => {
this.setState({
selectedLayerIndex: index,
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
}, this.setStateInUrl);
}
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
this.setState({
isOpen: {
...this.state.isOpen,
[modalName]: value
}
}, this.setStateInUrl)
}
toggleModal(modalName: keyof AppState["isOpen"]) {
this.setModal(modalName, !this.state.isOpen[modalName]);
}
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
this.setState({
openlayersDebugOptions: {
...this.state.openlayersDebugOptions,
[key]: value,
}
});
}
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
this.setState({
maplibreGlDebugOptions: {
...this.state.maplibreGlDebugOptions,
[key]: value,
}
});
}
render() {
const layers = this.state.mapStyle.layers || []
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined
const toolbar = <AppToolbar
renderer={this._getRenderer()}
mapState={this.state.mapState}
mapStyle={this.state.mapStyle}
inspectModeEnabled={this.state.mapState === "inspect"}
sources={this.state.sources}
onStyleChanged={this.onStyleChanged}
onStyleOpen={this.onStyleChanged}
onSetMapState={this.setMapState}
onToggleModal={this.toggleModal.bind(this)}
/>
const layerList = <LayerList
onMoveLayer={this.onMoveLayer}
onLayerDestroy={this.onLayerDestroy}
onLayerCopy={this.onLayerCopy}
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayersChange={this.onLayersChange}
onLayerSelect={this.onLayerSelect}
selectedLayerIndex={this.state.selectedLayerIndex}
layers={layers}
sources={this.state.sources}
errors={this.state.errors}
/>
const layerEditor = selectedLayer ? <LayerEditor
key={this.state.selectedLayerOriginalId}
layer={selectedLayer}
layerIndex={this.state.selectedLayerIndex}
isFirstLayer={this.state.selectedLayerIndex < 1}
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
sources={this.state.sources}
vectorLayers={this.state.vectorLayers}
spec={this.state.spec}
onMoveLayer={this.onMoveLayer}
onLayerChanged={this.onLayerChanged}
onLayerDestroy={this.onLayerDestroy}
onLayerCopy={this.onLayerCopy}
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors}
/> : undefined
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
currentLayer={selectedLayer}
selectedLayerIndex={this.state.selectedLayerIndex}
onLayerSelect={this.onLayerSelect}
mapStyle={this.state.mapStyle}
errors={this.state.errors}
infos={this.state.infos}
/> : undefined
const modals = <div>
<ModalDebug
renderer={this._getRenderer()}
maplibreGlDebugOptions={this.state.maplibreGlDebugOptions}
openlayersDebugOptions={this.state.openlayersDebugOptions}
onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug}
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
isOpen={this.state.isOpen.debug}
onOpenToggle={this.toggleModal.bind(this, 'debug')}
mapView={this.state.mapView}
/>
<ModalShortcuts
isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>
<ModalSettings
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged}
onChangeMetadataProperty={this.onChangeMetadataProperty}
isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')}
/>
<ModalExport
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, 'export')}
/>
<ModalOpen
isOpen={this.state.isOpen.open}
onStyleOpen={this.openStyle}
onOpenToggle={this.toggleModal.bind(this, 'open')}
/>
<ModalSources
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
</div>
return <AppLayout
toolbar={toolbar}
layerList={layerList}
layerEditor={layerEditor}
map={this.mapRenderer()}
bottom={bottomPanel}
modals={modals}
/>
}
}

View File

@@ -1,52 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
import { WithTranslation, withTranslation } from 'react-i18next';
type AppLayoutInternalProps = {
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> {
static childContextTypes = {
reactIconBase: PropTypes.object
}
getChildContext() {
return {
reactIconBase: { size: 14 }
}
}
render() {
document.body.dir = this.props.i18n.dir();
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-main">
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
</div>
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}
{this.props.modals}
</div>
}
}
const AppLayout = withTranslation()(AppLayoutInternal);
export default AppLayout;

View File

@@ -1,66 +0,0 @@
import React from 'react'
import {formatLayerId} from '../libs/format';
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
type AppMessagePanelInternalProps = {
errors?: unknown[]
infos?: string[]
mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown
currentLayer?: LayerSpecification
selectedLayerIndex?: number
} & WithTranslation;
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const {t, selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const {parsed} = error;
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 &&
<>
&nbsp;&mdash;&nbsp;
<button
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
>
{t("switch to layer")}
</button>
</>
}
</>
);
}
else {
content = error.message;
}
return <p key={"error-"+idx} className="maputnik-message-panel-error">
{content}
</p>
})
const infos = this.props.infos?.map((m, i) => {
return <p key={"info-"+i}>{m}</p>
})
return <div className="maputnik-message-panel">
{errors}
{infos}
</div>
}
}
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
export default AppMessagePanel;

View File

@@ -1,291 +0,0 @@
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 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();
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser!.name) > -1;
type IconTextProps = {
children?: React.ReactNode
};
class IconText extends React.Component<IconTextProps> {
render() {
return <span className="maputnik-icon-text">{this.props.children}</span>
}
}
type ToolbarLinkProps = {
className?: string
children?: React.ReactNode
href?: string
onToggleModal?(...args: unknown[]): unknown
};
class ToolbarLink extends React.Component<ToolbarLinkProps> {
render() {
return <a
className={classnames('maputnik-toolbar-link', this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
data-wd-key="toolbar:link"
>
{this.props.children}
</a>
}
}
type ToolbarSelectProps = {
children?: React.ReactNode
wdKey?: string
};
class ToolbarSelect extends React.Component<ToolbarSelectProps> {
render() {
return <div
className='maputnik-toolbar-select'
data-wd-key={this.props.wdKey}
>
{this.props.children}
</div>
}
}
type ToolbarActionProps = {
children?: React.ReactNode
onClick?(...args: unknown[]): unknown
wdKey?: string
};
class ToolbarAction extends React.Component<ToolbarActionProps> {
render() {
return <button
className='maputnik-toolbar-action'
data-wd-key={this.props.wdKey}
onClick={this.props.onClick}
>
{this.props.children}
</button>
}
}
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
type AppToolbarInternalProps = {
mapStyle: object
inspectModeEnabled: boolean
onStyleChanged(...args: unknown[]): unknown
// A new style has been uploaded
onStyleOpen(...args: unknown[]): unknown
// A dict of source id's and the available source layers
sources: object
children?: React.ReactNode
onToggleModal(...args: unknown[]): unknown
onSetMapState(mapState: MapState): unknown
mapState?: MapState
renderer?: string
} & WithTranslation;
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
state = {
isOpen: {
settings: false,
sources: false,
open: false,
add: false,
export: false,
}
}
handleSelection(val: MapState) {
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();
}
else {
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
el.focus();
}
}
render() {
const t = this.props.t;
const views = [
{
id: "map",
group: "general",
title: t("Map"),
},
{
id: "inspect",
group: "general",
title: t("Inspect"),
disabled: this.props.renderer === 'ol',
},
{
id: "filter-deuteranopia",
group: "color-accessibility",
title: t("Deuteranopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-protanopia",
group: "color-accessibility",
title: t("Protanopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-tritanopia",
group: "color-accessibility",
title: t("Tritanopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-achromatopsia",
group: "color-accessibility",
title: t("Achromatopsia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
];
const currentView = views.find((view) => {
return view.id === this.props.mapState;
});
return <nav className='maputnik-toolbar'>
<div className="maputnik-toolbar__inner">
<div
className="maputnik-toolbar-logo-container"
>
{/* Keyboard accessible quick links */}
<button
data-wd-key="root:skip:layer-list"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-list")}
>
{t("Layers list")}
</button>
<button
data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-editor")}
>
{t("Layer editor")}
</button>
<button
data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("map")}
>
{t("Map view")}
</button>
<a
className="maputnik-toolbar-logo"
target="blank"
rel="noreferrer noopener"
href="https://github.com/maplibre/maputnik"
>
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
<h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
</h1>
</a>
</div>
<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>
</ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>{t("Export")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers />
<IconText>{t("Data Sources")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings />
<IconText>{t("Style Settings")}</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect">
<MdFindInPage />
<label>{t("View")}
<select
className="maputnik-select"
data-wd-key="maputnik-select"
onChange={(e) => this.handleSelection(e.target.value as MapState)}
value={currentView?.id}
>
{views.filter(v => v.group === "general").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled} data-wd-key={item.id}>
{item.title}
</option>
);
})}
<optgroup label={t("Color accessibility")}>
{views.filter(v => v.group === "color-accessibility").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled}>
{item.title}
</option>
);
})}
</optgroup>
</select>
</label>
</ToolbarSelect>
<ToolbarSelect wdKey="nav:language">
<MdLanguage />
<label>{t("Language")}
<select
className="maputnik-select"
data-wd-key="maputnik-lang-select"
onChange={(e) => this.handleLanguageChange(e.target.value)}
value={this.props.i18n.language}
>
{Object.entries(supportedLanguages).map(([code, name]) => {
return (
<option key={code} value={code}>
{name}
</option>
);
})}
</select>
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
<MdHelpOutline />
<IconText>{t("Help")}</IconText>
</ToolbarLink>
</div>
</div>
</nav>
}
}
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;

View File

@@ -1,103 +0,0 @@
import React, {PropsWithChildren, SyntheticEvent} from 'react'
import classnames from 'classnames'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
type BlockProps = PropsWithChildren & {
"data-wd-key"?: string
label?: string
action?: React.ReactElement
style?: object
onChange?(...args: unknown[]): unknown
fieldSpec?: object
wideMode?: boolean
error?: {message: string}
};
type BlockState = {
showDoc: boolean
};
/** Wrap a component with a label */
export default class Block extends React.Component<BlockProps, BlockState> {
_blockEl: HTMLDivElement | null = null;
constructor (props: BlockProps) {
super(props);
this.state = {
showDoc: false,
}
}
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
const value = e.target.value
if (this.props.onChange) {
return this.props.onChange(value === "" ? undefined : value)
}
}
onToggleDoc = (val: boolean) => {
this.setState({
showDoc: val
});
}
/**
* Some fields for example <InputColor/> bind click events inside the element
* to close the picker. This in turn propagates to the <label/> element
* causing the picker to reopen. This causes a scenario where the picker can
* never be closed once open.
*/
onLabelClick = (event: SyntheticEvent<any, any>) => {
const el = event.nativeEvent.target;
const contains = this._blockEl?.contains(el);
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
event.stopPropagation();
}
event.preventDefault();
}
render() {
return <label style={this.props.style}
data-wd-key={this.props["data-wd-key"]}
className={classnames({
"maputnik-input-block": true,
"maputnik-input-block--wide": this.props.wideMode,
"maputnik-action-block": this.props.action
})}
onClick={this.onLabelClick}
>
{this.props.fieldSpec &&
<div className="maputnik-input-block-label">
<FieldDocLabel
label={this.props.label}
onToggleDoc={this.onToggleDoc}
fieldSpec={this.props.fieldSpec}
/>
</div>
}
{!this.props.fieldSpec &&
<div className="maputnik-input-block-label">
{this.props.label}
</div>
}
<div className="maputnik-input-block-action">
{this.props.action}
</div>
<div className="maputnik-input-block-content" ref={el => this._blockEl = el}>
{this.props.children}
</div>
{this.props.fieldSpec &&
<div
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}}
>
<Doc fieldSpec={this.props.fieldSpec} />
</div>
}
</label>
}
}

View File

@@ -1,34 +0,0 @@
import React from 'react'
import { Collapse as ReactCollapse } from 'react-collapse'
import {reducedMotionEnabled} from '../libs/accessibility'
type CollapseProps = {
isActive: boolean
children: React.ReactElement
};
export default class Collapse extends React.Component<CollapseProps> {
static defaultProps = {
isActive: true
}
render() {
if (reducedMotionEnabled()) {
return (
<div style={{display: this.props.isActive ? "block" : "none"}}>
{this.props.children}
</div>
)
}
else {
return (
<ReactCollapse isOpened={this.props.isActive}>
{this.props.children}
</ReactCollapse>
)
}
}
}

View File

@@ -1,19 +0,0 @@
import React from 'react'
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
type CollapserProps = {
isCollapsed: boolean
style?: object
};
export default class Collapser extends React.Component<CollapserProps> {
render() {
const iconStyle = {
width: 20,
height: 20,
...this.props.style,
}
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
}
}

View File

@@ -1,91 +0,0 @@
import React from 'react'
const headers = {
js: "JS",
android: "Android",
ios: "iOS",
macos: "macOS",
};
type DocProps = {
fieldSpec: {
doc?: string
values?: {
[key: string]: {
doc?: string
}
}
'sdk-support'?: {
[key: string]: typeof headers
}
}
};
export default class Doc extends React.Component<DocProps> {
render () {
const {fieldSpec} = this.props;
const {doc, values} = fieldSpec;
const sdkSupport = fieldSpec['sdk-support'];
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>
!Array.isArray(values)
);
return (
<>
{doc &&
<div className="SpecDoc">
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
{renderValues &&
<ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => {
return (
<li key={key}>
<code>{JSON.stringify(key)}</code>
<div>{value.doc}</div>
</li>
);
})}
</ul>
}
</div>
}
{sdkSupport &&
<div className="SpecDoc__sdk-support">
<table className="SpecDoc__sdk-support__table">
<thead>
<tr>
<th></th>
{Object.values(headers).map(header => {
return <th key={header}>{header}</th>;
})}
</tr>
</thead>
<tbody>
{Object.entries(sdkSupport).map(([key, supportObj]) => {
return (
<tr key={key}>
<td>{key}</td>
{Object.keys(headers).map((k) => {
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
}
else {
return <td key={k}>no</td>;
}
})}
</tr>
);
})}
</tbody>
</table>
</div>
}
</>
);
}
}

View File

@@ -1,19 +0,0 @@
import React from 'react'
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
import Fieldset from './Fieldset'
type FieldArrayProps = InputArrayProps & {
name?: string
fieldSpec?: {
doc: string
}
};
export default class FieldArray extends React.Component<FieldArrayProps> {
render() {
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputArray {...this.props} />
</Fieldset>
}
}

View File

@@ -1,18 +0,0 @@
import React from 'react'
import Block from './Block'
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
type FieldAutocompleteProps = InputAutocompleteProps & {
label?: string;
};
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
render() {
return <Block label={this.props.label}>
<InputAutocomplete {...this.props} />
</Block>
}
}

View File

@@ -1,18 +0,0 @@
import React from 'react'
import Block from './Block'
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
type FieldCheckboxProps = InputCheckboxProps & {
label?: string;
};
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
render() {
return <Block label={this.props.label}>
<InputCheckbox {...this.props} />
</Block>
}
}

View File

@@ -1,21 +0,0 @@
import React from 'react'
import Block from './Block'
import InputColor, {InputColorProps} from './InputColor'
type FieldColorProps = InputColorProps & {
label?: string
fieldSpec?: {
doc: string
}
};
export default class FieldColor extends React.Component<FieldColorProps> {
render() {
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputColor {...this.props} />
</Block>
}
}

View File

@@ -1,38 +0,0 @@
import React from 'react'
import Block from './Block'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldCommentInternalProps = {
value?: string
onChange(value: string | undefined): unknown
error: {message: string}
} & WithTranslation;
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
render() {
const t = this.props.t;
const fieldSpec = {
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
};
return <Block
label={t("Comments")}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
error={this.props.error}
>
<InputString
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default={t("Comment...")}
data-wd-key="layer-comment.input"
/>
</Block>
}
}
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;

View File

@@ -1,65 +0,0 @@
import React from 'react'
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
type FieldDocLabelProps = {
label: JSX.Element | string | undefined
fieldSpec?: {
doc?: string
}
onToggleDoc?(...args: unknown[]): unknown
};
type FieldDocLabelState = {
open: boolean
};
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
constructor (props: FieldDocLabelProps) {
super(props);
this.state = {
open: false,
}
}
onToggleDoc = (open: boolean) => {
this.setState({
open,
}, () => {
if (this.props.onToggleDoc) {
this.props.onToggleDoc(this.state.open);
}
});
}
render() {
const {label, fieldSpec} = this.props;
const {doc} = fieldSpec || {};
if (doc) {
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">
{label}
{'\xa0'}
<button
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
onClick={() => this.onToggleDoc(!this.state.open)}
data-wd-key={'field-doc-button-'+label}
>
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
</button>
</div>
</label>
}
else if (label) {
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">
{label}
</div>
</label>
}
else {
<div />
}
}
}

View File

@@ -1,16 +0,0 @@
import React from 'react'
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
import Fieldset from './Fieldset'
type FieldDynamicArrayProps = InputDynamicArrayProps & {
name?: string
};
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
render() {
return <Fieldset label={this.props.label}>
<InputDynamicArray {...this.props} />
</Fieldset>
}
}

View File

@@ -1,20 +0,0 @@
import React from 'react'
import InputEnum, {InputEnumProps} from './InputEnum'
import Fieldset from './Fieldset';
type FieldEnumProps = InputEnumProps & {
label?: string;
fieldSpec?: {
doc: string
}
};
export default class FieldEnum extends React.Component<FieldEnumProps> {
render() {
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputEnum {...this.props} />
</Fieldset>
}
}

View File

@@ -1,407 +0,0 @@
import React from 'react'
import SpecProperty from './_SpecProperty'
import DataProperty, { Stop } from './_DataProperty'
import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from './_ExpressionProperty'
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
import {findDefaultFromSpec} from '../libs/spec-helper';
function isLiteralExpression(value: any) {
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
}
function isGetExpression(value: any) {
return (
Array.isArray(value) &&
value.length === 2 &&
value[0] === "get"
);
}
function isZoomField(value: any) {
return (
typeof(value) === 'object' &&
value.stops &&
typeof(value.property) === 'undefined' &&
Array.isArray(value.stops) &&
value.stops.length > 1 &&
value.stops.every((stop: Stop) => {
return (
Array.isArray(stop) &&
stop.length === 2
);
})
);
}
function isIdentityProperty(value: any) {
return (
typeof(value) === 'object' &&
value.type === "identity" &&
Object.prototype.hasOwnProperty.call(value, "property")
);
}
function isDataStopProperty(value: any) {
return (
typeof(value) === 'object' &&
value.stops &&
typeof(value.property) !== 'undefined' &&
value.stops.length > 1 &&
Array.isArray(value.stops) &&
value.stops.every((stop: Stop) => {
return (
Array.isArray(stop) &&
stop.length === 2 &&
typeof(stop[0]) === 'object'
);
})
);
}
function isDataField(value: any) {
return (
isIdentityProperty(value) ||
isDataStopProperty(value)
);
}
function isPrimative(value: any): value is string | boolean | number {
const valid = ["string", "boolean", "number"];
return valid.includes(typeof(value));
}
function isArrayOfPrimatives(values: any): values is Array<string | boolean | number> {
if (Array.isArray(values)) {
return values.every(isPrimative);
}
return false;
}
function getDataType(value: any, fieldSpec={} as any) {
if (value === undefined) {
return "value";
}
else if (isPrimative(value)) {
return "value";
}
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
return "value";
}
else if (isZoomField(value)) {
return "zoom_function";
}
else if (isDataField(value)) {
return "data_function";
}
else {
return "expression";
}
}
type FieldFunctionProps = {
onChange(fieldName: string, value: any): unknown
fieldName: string
fieldType: string
fieldSpec: any
errors?: {[key: string]: {message: string}}
value?: any
};
type FieldFunctionState = {
dataType: string
isEditing: boolean
}
/** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
constructor (props: FieldFunctionProps) {
super(props);
this.state = {
dataType: getDataType(props.value, props.fieldSpec),
isEditing: false,
}
}
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
// Because otherwise when editing values we end up accidentally changing field type.
if (state.isEditing) {
return {};
}
else {
return {
isEditing: false,
dataType: getDataType(props.value, props.fieldSpec)
};
}
}
getFieldFunctionType(fieldSpec: any) {
if (fieldSpec.expression.interpolated) {
return "exponential"
}
if (fieldSpec.type === "number") {
return "interval"
}
return "categorical"
}
addStop = () => {
const stops = this.props.value.stops.slice(0)
const lastStop = stops[stops.length - 1]
if (typeof lastStop[0] === "object") {
stops.push([
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
lastStop[1]
])
}
else {
stops.push([lastStop[0] + 1, lastStop[1]])
}
const changedValue = {
...this.props.value,
stops: stops,
}
this.props.onChange(this.props.fieldName, changedValue)
}
deleteExpression = () => {
const {fieldSpec, fieldName} = this.props;
this.props.onChange(fieldName, fieldSpec.default);
this.setState({
dataType: "value",
});
}
deleteStop = (stopIdx: number) => {
const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1)
let changedValue = {
...this.props.value,
stops: stops,
}
if(stops.length === 1) {
changedValue = stops[0][1]
}
this.props.onChange(this.props.fieldName, changedValue)
}
makeZoomFunction = () => {
const {value} = this.props;
let zoomFunc;
if (typeof(value) === "object") {
if (value.stops) {
zoomFunc = {
base: value.base,
stops: value.stops.map((stop: Stop) => {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
}
else {
zoomFunc = {
base: value.base,
stops: [
[6, findDefaultFromSpec(this.props.fieldSpec)],
[10, findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
}
else {
zoomFunc = {
stops: [
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
this.props.onChange(this.props.fieldName, zoomFunc)
}
undoExpression = () => {
const {value, fieldName} = this.props;
if (isGetExpression(value)) {
this.props.onChange(fieldName, {
"type": "identity",
"property": value[1]
});
this.setState({
dataType: "value",
});
}
else if (isLiteralExpression(value)) {
this.props.onChange(fieldName, value[1]);
this.setState({
dataType: "value",
});
}
}
canUndo = () => {
const {value, fieldSpec} = this.props;
return (
isGetExpression(value) ||
isLiteralExpression(value) ||
isPrimative(value) ||
(Array.isArray(value) && fieldSpec.type === "array")
);
}
makeExpression = () => {
const {value, fieldSpec} = this.props;
let expression;
if (typeof(value) === "object" && 'stops' in value) {
expression = styleFunction.convertFunction(value, fieldSpec);
}
else if (isIdentityProperty(value)) {
expression = ["get", value.property];
}
else {
expression = ["literal", value || this.props.fieldSpec.default];
}
this.props.onChange(this.props.fieldName, expression);
}
makeDataFunction = () => {
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0;
const {value} = this.props;
let dataFunc;
if (typeof(value) === "object") {
if (value.stops) {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: value.stops.map((stop: Stop) => {
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
}
else {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: [
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
}
else {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: [
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
this.props.onChange(this.props.fieldName, dataFunc)
}
onMarkEditing = () => {
this.setState({isEditing: true});
}
onUnmarkEditing = () => {
this.setState({isEditing: false});
}
render() {
const {dataType} = this.state;
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
let specField;
if (dataType === "expression") {
specField = (
<ExpressionProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this, this.props.fieldName)}
canUndo={this.canUndo}
onUndo={this.undoExpression}
onDelete={this.deleteExpression}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onFocus={this.onMarkEditing}
onBlur={this.onUnmarkEditing}
/>
);
}
else if (dataType === "zoom_function") {
specField = (
<ZoomProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop}
onAddStop={this.addStop}
onChangeToDataFunction={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/>
)
}
else if (dataType === "data_function") {
// TODO: Rename to FieldFunction **this file** shouldn't be called that
specField = (
<DataProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop}
onAddStop={this.addStop}
onChangeToZoomFunction={this.makeZoomFunction}
onExpressionClick={this.makeExpression}
/>
)
}
else {
specField = (
<SpecProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onZoomClick={this.makeZoomFunction}
onDataClick={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/>
)
}
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}>
{specField}
</div>
}
}

View File

@@ -1,28 +0,0 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputString from './InputString'
type FieldIdProps = {
value: string
wdKey: string
onChange(value: string | undefined): unknown
error?: {message: string}
};
export default class FieldId extends React.Component<FieldIdProps> {
render() {
return <Block label="ID" fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
<InputString
value={this.props.value}
onInput={this.props.onChange}
data-wd-key={this.props.wdKey + ".input"}
/>
</Block>
}
}

View File

@@ -1,13 +0,0 @@
import React from 'react'
import InputJson, {InputJsonProps} from './InputJson'
type FieldJsonProps = InputJsonProps & {};
export default class FieldJson extends React.Component<FieldJsonProps> {
render() {
return <InputJson {...this.props} />
}
}

View File

@@ -1,35 +0,0 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMaxZoomInternalProps = {
value?: number
onChange(value: number | undefined): unknown
error?: {message: string}
} & WithTranslation;
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
render() {
const t = this.props.t;
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
<InputNumber
allowRange={true}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.maxzoom.minimum}
max={latest.layer.maxzoom.maximum}
default={latest.layer.maxzoom.maximum}
data-wd-key="max-zoom.input"
/>
</Block>
}
}
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
export default FieldMaxZoom;

View File

@@ -1,35 +0,0 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMinZoomInternalProps = {
value?: number
onChange(...args: unknown[]): unknown
error?: {message: string}
} & WithTranslation;
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
render() {
const t = this.props.t;
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
<InputNumber
allowRange={true}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.minzoom.minimum}
max={latest.layer.minzoom.maximum}
default={latest.layer.minzoom.minimum}
data-wd-key='min-zoom.input'
/>
</Block>
}
}
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
export default FieldMinZoom;

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