Compare commits

..

141 Commits

Author SHA1 Message Date
github-actions[bot] 482e6ec545 Bump version to 2.1.1 (#927)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

---------

Co-authored-by: HarelM <3269297+HarelM@users.noreply.github.com>
Co-authored-by: Harel M <harel.mazor@gmail.com>
2024-08-29 22:14:08 +03:00
Kevin Schaul ca45bcce86 Fix release-notes.js (#926)
Was missing trailing `, causing [this
failure](https://github.com/maplibre/maputnik/actions/runs/10619718712/job/29437920164)
2024-08-29 22:11:14 +03:00
github-actions[bot] 4dd34e99fd Bump version to 2.1.0 (#925)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

---------

Co-authored-by: HarelM <3269297+HarelM@users.noreply.github.com>
Co-authored-by: Harel M <harel.mazor@gmail.com>
2024-08-29 20:22:26 +03:00
dependabot[bot] 3773acd5be Bump micromatch from 4.0.5 to 4.0.8 (#923)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5
to 4.0.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/micromatch/micromatch/releases">micromatch's
releases</a>.</em></p>
<blockquote>
<h2>4.0.8</h2>
<p>Ultimate release that fixes both CVE-2024-4067 and CVE-2024-4068. We
consider the issues low-priority, so even if you see automated scanners
saying otherwise, don't be scared.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md">micromatch's
changelog</a>.</em></p>
<blockquote>
<h2>[4.0.8] - 2024-08-22</h2>
<ul>
<li>backported CVE-2024-4067 fix (from v4.0.6) over to 4.x branch</li>
</ul>
<h2>[4.0.7] - 2024-05-22</h2>
<ul>
<li>this is basically v4.0.5, with some README updates</li>
<li><strong>it is vulnerable to CVE-2024-4067</strong></li>
<li>Updated braces to v3.0.3 to avoid CVE-2024-4068</li>
<li>does NOT break API compatibility</li>
</ul>
<h2>[4.0.6] - 2024-05-21</h2>
<ul>
<li>Added <code>hasBraces</code> to check if a pattern contains
braces.</li>
<li>Fixes CVE-2024-4067</li>
<li><strong>BREAKS API COMPATIBILITY</strong></li>
<li>Should be labeled as a major release, but it's not.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/micromatch/micromatch/commit/8bd704ec0d9894693d35da425d827819916be920"><code>8bd704e</code></a>
4.0.8</li>
<li><a
href="https://github.com/micromatch/micromatch/commit/a0e68416a44da10f3e4e30845ab95af4fd286d5a"><code>a0e6841</code></a>
run verb to generate README documentation</li>
<li><a
href="https://github.com/micromatch/micromatch/commit/4ec288484f6e8cccf597ad3d43529c31d0f7a02a"><code>4ec2884</code></a>
Merge branch 'v4' into hauserkristof-feature/v4.0.8</li>
<li><a
href="https://github.com/micromatch/micromatch/commit/03aa8052171e878897eee5d7bb2ae0ae83ec2ade"><code>03aa805</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/micromatch/issues/266">#266</a>
from hauserkristof/feature/v4.0.8</li>
<li><a
href="https://github.com/micromatch/micromatch/commit/814f5f70efcd100ca9d29198867812a3d6ab91a8"><code>814f5f7</code></a>
lint</li>
<li><a
href="https://github.com/micromatch/micromatch/commit/67fcce6a1077c2faf5ad0c5f998fa70202cc5dae"><code>67fcce6</code></a>
fix: CHANGELOG about braces &amp; CVE-2024-4068, v4.0.5</li>
<li><a
href="https://github.com/micromatch/micromatch/commit/113f2e3fa7cb30b429eda7c4c38475a8e8ba1b30"><code>113f2e3</code></a>
fix: CVE numbers in CHANGELOG</li>
<li><a
href="https://github.com/micromatch/micromatch/commit/d9dbd9a266686f44afb38da26fe016f96d1ec04f"><code>d9dbd9a</code></a>
feat: updated CHANGELOG</li>
<li><a
href="https://github.com/micromatch/micromatch/commit/2ab13157f416679f54e3a32b1425e184bd16749e"><code>2ab1315</code></a>
fix: use actions/setup-node@v4</li>
<li><a
href="https://github.com/micromatch/micromatch/commit/1406ea38f3e24b29f4d4f46908d5cffcb3e6c4ce"><code>1406ea3</code></a>
feat: rework test to work on macos with node 10,12 and 14</li>
<li>Additional commits viewable in <a
href="https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=micromatch&package-manager=npm_and_yarn&previous-version=4.0.5&new-version=4.0.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maplibre/maputnik/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-29 20:13:19 +03:00
Kevin Schaul 6887d70194 Make build scripts executable (#924)
Hopefully will fix CI issue merged in #922

cc @HarelM
2024-08-29 19:52:56 +03:00
Kevin Schaul 66c5a5c953 Update desktop build to pull from this repo (#922)
Previously the desktop build lived in a separate repo and had to
download a released version of the maputnik editor source code. Now that
both live in the same repo, the desktop version can simply run the
maputnik build command and use those generated files.

This commit also removes the ci-desktop workflow, which is not needed.
The regular ci workflow already built the desktop version (this commit
also fixes that build).

Fixes #919

If this works for you all, it would be lovely to create a new tag or
release on GitHub for two reasons:
1. So the latest binaries are easier to locate, and
2. So I can update my [submission to
homebrew](https://github.com/Homebrew/homebrew-core/commit/6e536ff007ef0bea5a3591af5678eb3152908cc9)
to make installation easier (for os x users at least)
2024-08-29 17:07:24 +03:00
Harel M 8184ac8393 Update geocoder to 1.6.0 (#920)
This is to update geocoder to latest version which now have types and is
fully modernized.
2024-08-21 08:56:59 +03:00
Joleen Knepp 6a0d2e8ee5 Added Simplified Chinese language support (#921)
Added Simplified Chinese language support
2024-08-21 07:17:31 +03:00
Keitaroh Kobayashi 58edd262b0 Add react-i18next for multi-language support (#917)
This is a rough start on adding react-i18next. I'll be working on adding
more translatable strings and translations in the coming days. I'm going
to need to wrap class components in HOCs, so let me know if there's
something I should be fixing before doing that. I'm thinking now to keep
the exported class names exactly the same, and rename the existing
classes by prefixing an `I` (for internal). For example:

```
export default class AppToolbar ...
```

becomes

```
class IAppToolbar ...
const AppToolbar = withTranslation()(IAppToolbar);
export default AppToolbar;
```

I'll be able to contribute Japanese strings (I've talked to a couple
people on my team and they'll be happy to help as well), so that's the
language I decided to go with in this PR.

Closes #746

---------

Co-authored-by: Ko Nagase <nagase@georepublic.co.jp>
Co-authored-by: Harel M <harel.mazor@gmail.com>
2024-08-19 12:43:04 +03:00
Ian Wagner 35840409b8 Add Stadia Outdoors style (#913)
* Adds the Stadia Outdoors style (anticipating the question: the
MapLibre domain and localhost development are allowlisted)
* Minor fixes to README as the npm commands seem to have changed
slightly
* Reorder the empty style to the beginning; seems the logical place for
it to me, but open for discussion
2024-07-25 22:50:24 -04:00
Kacper Golinski d0f6e0fadb Update feature id label to $id in feature properties popup (#912)
Current label, "Feature ID", creates a little bit of confusion. It's not
clear that this label isn't really part of the feature properties.

For example, when we want to filter by feature id, we need to use `$id`

```
[==, "$id," 123] 
```
2024-07-12 21:18:23 +03:00
Bart Louwers 0de304ca3e Add Versatiles Colorful as example style (#908)
These are great looking permissively licensed styles maintained by a
passionate open source communities. I think they should be included in
the example styles. 🙂

The thumbnails were uploaded to GitHub and should be available pretty
much forever, I chose to triple the pixel density because the other
thumbnails look bad on my high res monitor.

<img width="573" alt="Screenshot 2024-06-22 at 12 25 56"
src="https://github.com/maplibre/maputnik/assets/649392/e3138a0f-bfca-4949-915d-f9fc3e9bb346">
2024-06-24 08:09:25 +03:00
dependabot[bot] 48bf25c1b0 Bump braces from 3.0.2 to 3.0.3 (#904)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to
3.0.3.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/micromatch/braces/commit/74b2db2938fad48a2ea54a9c8bf27a37a62c350d"><code>74b2db2</code></a>
3.0.3</li>
<li><a
href="https://github.com/micromatch/braces/commit/88f1429a0f47e1dd3813de35211fc97ffda27f9e"><code>88f1429</code></a>
update eslint. lint, fix unit tests.</li>
<li><a
href="https://github.com/micromatch/braces/commit/415d660c3002d1ab7e63dbf490c9851da80596ff"><code>415d660</code></a>
Snyk js braces 6838727 (<a
href="https://redirect.github.com/micromatch/braces/issues/40">#40</a>)</li>
<li><a
href="https://github.com/micromatch/braces/commit/190510f79db1adf21d92798b0bb6fccc1f72c9d6"><code>190510f</code></a>
fix tests, skip 1 test in test/braces.expand</li>
<li><a
href="https://github.com/micromatch/braces/commit/716eb9f12d820b145a831ad678618731927e8856"><code>716eb9f</code></a>
readme bump</li>
<li><a
href="https://github.com/micromatch/braces/commit/a5851e57f45c3431a94d83fc565754bc10f5bbc3"><code>a5851e5</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/braces/issues/37">#37</a>
from coderaiser/fix/vulnerability</li>
<li><a
href="https://github.com/micromatch/braces/commit/2092bd1fb108d2c59bd62e243b70ad98db961538"><code>2092bd1</code></a>
feature: braces: add maxSymbols (<a
href="https://github.com/micromatch/braces/issues/">https://github.com/micromatch/braces/issues/</a>...</li>
<li><a
href="https://github.com/micromatch/braces/commit/9f5b4cf47329351bcb64287223ffb6ecc9a5e6d3"><code>9f5b4cf</code></a>
fix: vulnerability (<a
href="https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727">https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727</a>)</li>
<li><a
href="https://github.com/micromatch/braces/commit/98414f9f1fabe021736e26836d8306d5de747e0d"><code>98414f9</code></a>
remove funding file</li>
<li><a
href="https://github.com/micromatch/braces/commit/665ab5d561c017a38ba7aafd92cc6655b91d8c14"><code>665ab5d</code></a>
update keepEscaping doc (<a
href="https://redirect.github.com/micromatch/braces/issues/27">#27</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/micromatch/braces/compare/3.0.2...3.0.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=braces&package-manager=npm_and_yarn&previous-version=3.0.2&new-version=3.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maplibre/maputnik/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-16 13:08:52 +03:00
Harel M c82c6158e6 Update Cypress related packages (#903)
Let's see if this adds the relevant information to the CI run details.
CC: @ShellyDCMS
2024-05-29 10:47:14 +03:00
bolo 41cd7dfad1 Update OpenMapTiles styles to the latest version fixes #901 (#902)
Update version numbers of OpenMapTiles styles.

These styles are updated with the latest version (v3.15) of
OpenMapTiles.

Fixes #901
2024-05-20 14:43:00 +03:00
Kevin Schaul 7591b031ce Upgrade Cypress to fix CI errors (#897)
This upgrade should fix the error of Firefox tests failing. See this
issue for more details:
https://github.com/cypress-io/cypress/issues/29190
2024-04-16 08:26:13 +03:00
Kevin Schaul 95b5324fd3 Fix desktop ci artifacts path (#896)
The `working-directory` option does not apply to the artifacts steps, so
we have to specify the full paths
2024-04-12 16:02:53 -05:00
dependabot[bot] f34529ef06 Bump vite from 5.2.2 to 5.2.6 (#895)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 5.2.2 to 5.2.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>create-vite@5.2.3</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/create-vite@5.2.3/packages/create-vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->5.2.6 (2024-03-24)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: <code>fs.deny</code> with globs with directories (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16250">#16250</a>)
(<a href="https://github.com/vitejs/vite/commit/ba5269c">ba5269c</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16250">#16250</a></li>
</ul>
<h2><!-- raw HTML omitted -->5.2.5 (2024-03-24)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: avoid SSR requests in waitForRequestIdle (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16246">#16246</a>)
(<a href="https://github.com/vitejs/vite/commit/7093f77">7093f77</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16246">#16246</a></li>
<li>docs: clarify enforce vs hook.order (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16226">#16226</a>)
(<a href="https://github.com/vitejs/vite/commit/3a73e48">3a73e48</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16226">#16226</a></li>
</ul>
<h2><!-- raw HTML omitted -->5.2.4 (2024-03-23)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: dont resolve imports with malformed URI (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16244">#16244</a>)
(<a href="https://github.com/vitejs/vite/commit/fbf69d5">fbf69d5</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16244">#16244</a></li>
</ul>
<h2><!-- raw HTML omitted -->5.2.3 (2024-03-22)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: handle warmup request error correctly (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16223">#16223</a>)
(<a href="https://github.com/vitejs/vite/commit/d7c5256">d7c5256</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16223">#16223</a></li>
<li>fix: skip encode if is data uri (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16233">#16233</a>)
(<a href="https://github.com/vitejs/vite/commit/8617e76">8617e76</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16233">#16233</a></li>
<li>fix(optimizer): fix <code>optimizeDeps.include</code> glob syntax
for <code>./*</code> exports (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16230">#16230</a>)
(<a href="https://github.com/vitejs/vite/commit/f184c80">f184c80</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16230">#16230</a></li>
<li>fix(runtime): fix sourcemap with <code>prepareStackTrace</code> (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16220">#16220</a>)
(<a href="https://github.com/vitejs/vite/commit/dad7f4f">dad7f4f</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16220">#16220</a></li>
<li>chore: <code>utf8</code> replaced with <code>utf-8</code> (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16232">#16232</a>)
(<a href="https://github.com/vitejs/vite/commit/9800c73">9800c73</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16232">#16232</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitejs/vite/commit/7369016d8a0f26ad9200cf7fd0e2045ca9fd1a41"><code>7369016</code></a>
release: v5.2.6</li>
<li><a
href="https://github.com/vitejs/vite/commit/ba5269cca81de3f5fbb0f49d58a1c55688043258"><code>ba5269c</code></a>
fix: <code>fs.deny</code> with globs with directories (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16250">#16250</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/7a2791ce174933ae3afc29f596491cc0c67b9618"><code>7a2791c</code></a>
release: v5.2.5</li>
<li><a
href="https://github.com/vitejs/vite/commit/7093f779b7db2e0fdcb3f41affd76696b783a5fc"><code>7093f77</code></a>
fix: avoid SSR requests in waitForRequestIdle (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16246">#16246</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/3a73e485cd4c08954fcb06698b5f721dea6e9f44"><code>3a73e48</code></a>
docs: clarify enforce vs hook.order (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16226">#16226</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/6a07243a0ecc06a81e87d5b3de7b88023a045e6b"><code>6a07243</code></a>
release: v5.2.4</li>
<li><a
href="https://github.com/vitejs/vite/commit/fbf69d5f6cef335fc18640fed7da16593b13c9b3"><code>fbf69d5</code></a>
fix: dont resolve imports with malformed URI (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16244">#16244</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/a67f9f69456439baa727823ac604a68f7279b3bb"><code>a67f9f6</code></a>
release: v5.2.3</li>
<li><a
href="https://github.com/vitejs/vite/commit/8617e7638ec105c7a6019a7ebac3b3185297b90d"><code>8617e76</code></a>
fix: skip encode if is data uri (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16233">#16233</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/f184c8032bec6e668265a98d254a442e1024b6f3"><code>f184c80</code></a>
fix(optimizer): fix <code>optimizeDeps.include</code> glob syntax for
<code>./*</code> exports (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16">#16</a>...</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v5.2.6/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=5.2.2&new-version=5.2.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maplibre/maputnik/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-03 20:26:34 +03:00
IGUCHI Kanahiro a73b11805d localIdeographFontFamily: false to load CJK fonts (#893)
close #892 

this changes works well for CJK fonts.

<img width="736" alt="Screenshot 2024-03-25 at 22 04 36"
src="https://github.com/maplibre/maputnik/assets/20744195/adb0295e-5428-4773-b216-8f380e2dcb05">
2024-03-27 01:12:17 +02:00
Harel M ff15b77b7f Update React to version 18 and other deps (#890)
This PR aims at updating some packages.
I'll keep this in draft until I'll be more happy with the results.
Current setup seems to work, I'll let the CI run and see how bad this
is.
Packages that needs to be updated/replaces:
- [x] ~autocompete - The only warning left in the console is related to
the autocomplete, which probably needs to be updated since it's a
package that wasn't updated in the last 6 years.~ #611
- [x] ~Codemirror is also something that will need an update, but it
looks too complicated at this point in time, so let's see how this
goes.~ #891
- [ ] react-color
- [x] ~react-aria-menubutton~ #846
- [ ] Others?

Most of the changes here are related to types, which is good.
2024-03-21 22:51:29 +02:00
Harel M 355b663e7e Another attempt to fix the inspect issues (#889)
Fixes #871

- #871 

In case of same sources, calling the render method when the map style
changes, but not right away to let the map load the sources should fix
the issue.
2024-03-14 21:02:49 +02:00
Harel M 3c043fd5e0 Replace mapbox-gl-inspect with maplibre-gl-inspect (#888)
This hopefully fixes #871 

- #871 

I still need to update maplibre-gl-inspect to allow this to be fixed.
2024-03-13 22:48:01 +02:00
Harel M 5f54dd0ccf Temp fix using patch-package for maplibre-inspect issue (#885)
This is a temporary fix for:
 - #871

The fix is using `patch-package` to fix the part of the code that is
causing the issue, I believe.
Initial tests shows that this is fixing the issue, I hope it is not
introducing other issues.
A proper fix and also using the maplibre package will be done once a
repo is migrated as can be expected after the following PR is resolved:
- https://github.com/maplibre/maplibre/issues/359
2024-03-09 23:03:23 +02:00
Harel M 3727f5da48 Improve inspect hover UI (#879)
Fixes #868
- #868

It solves all the block within blocks and all kind of controls that are
not required which creates gaps.
I use a simple table, so the width is dynamic but it is always aligned
for all the properties and the features.

![image](https://github.com/maplibre/maputnik/assets/3269297/75138b00-ec7b-4e8d-b51b-f8ff6abcd5cb)

Vary basic stuff.
There's still the layer popup, which I'm not sure I know what it is and
might need to be fixed as well.
CC: @zstadler
2024-02-21 07:17:28 +02:00
Yuri Astrakhan 079c5f67cc Merge entire history from desktop repo to /desktop
Preserve history of the maputnik/desktop repository,
but move related files to `/desktop` directory,
and adjust the ci script to use it.
2024-02-12 13:52:35 -05:00
Yuri Astrakhan a304d4e060 Renamed ci to ci-desktop 2024-02-12 13:48:37 -05:00
Kevin Schaul 7ac1b03b5a Move into /desktop dir
For eventual merge into maplibre/maputnik repo
2024-02-12 11:06:45 -06:00
glebpinigin b9e32894b3 Readme.md container port changed to 80 (#881)
Activating maputnik with `docker run` requires to bind port 80 from
container and not 8888, because nginx is running with default config and
listening port 80

For `npm run` port 8888 is explicitly set up [in
config](https://github.com/maplibre/maputnik/blob/bc5ecfade6801e16df339a8caba766d28b88de56/vite.config.ts#L8).
2024-02-08 23:35:46 +02:00
Harel M bc5ecfade6 Removes number conversion for dirty input (#878)
Fixes #870

- #870

This regression bug was introduced as part of the refactoring of
typescript.
Here:
- #848

I've added tests to cover the scenario so that it won't happen again,
hopefully.
2024-02-07 10:32:19 +02:00
HarelM c84c7a7b96 Fix redo issue 2024-02-04 16:37:47 +02:00
Harel M cb77c6b4e2 Add nominatim search to maputnik (#873)
This replaces PR:
- #785

Before:

![image](https://github.com/maplibre/maputnik/assets/3269297/95297211-4108-43d8-8a43-42f87e2fbe16)
After:

![image](https://github.com/maplibre/maputnik/assets/3269297/86c3ae58-1bb7-4d2c-8ad9-6b84a21c96f7)

This is based on the geocoder example in maplibre docs:
https://maplibre.org/maplibre-gl-js/docs/examples/geocoder/
2024-02-04 11:37:23 +02:00
Harel M ea42f434eb Update MapLibre to version 4 (#872)
This updates maplibre from a pre-release to an official release.
It also removed the pre.2 from maputnik as maputnik is now published and
there's no reason to keep it in pre-release version name.
This doesn't mean maputnik has an npm package at this point though.
2024-02-04 10:01:22 +02:00
dependabot[bot] 6f82c12861 Bump vite from 5.0.10 to 5.0.12 (#865)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 5.0.10 to 5.0.12.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->5.0.12 (2024-01-19)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: await <code>configResolved</code> hooks of worker plugins (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15597">#15597</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15605">#15605</a>)
(<a href="https://github.com/vitejs/vite/commit/ef89f80">ef89f80</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15597">#15597</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/15605">#15605</a></li>
<li>fix: fs deny for case insensitive systems (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15653">#15653</a>)
(<a href="https://github.com/vitejs/vite/commit/91641c4">91641c4</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15653">#15653</a></li>
</ul>
<h2><!-- raw HTML omitted -->5.0.11 (2024-01-05)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: don't pretransform classic script links (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15361">#15361</a>)
(<a href="https://github.com/vitejs/vite/commit/19e3c9a">19e3c9a</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15361">#15361</a></li>
<li>fix: inject <code>__vite__mapDeps</code> code before sourcemap file
comment (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15483">#15483</a>)
(<a href="https://github.com/vitejs/vite/commit/d2aa096">d2aa096</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15483">#15483</a></li>
<li>fix(assets): avoid splitting <code>,</code> inside base64 value of
<code>srcset</code> attribute (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15422">#15422</a>)
(<a href="https://github.com/vitejs/vite/commit/8de7bd2">8de7bd2</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15422">#15422</a></li>
<li>fix(html): handle offset magic-string slice error (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15435">#15435</a>)
(<a href="https://github.com/vitejs/vite/commit/5ea9edb">5ea9edb</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15435">#15435</a></li>
<li>chore(deps): update dependency strip-literal to v2 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15475">#15475</a>)
(<a href="https://github.com/vitejs/vite/commit/49d21fe">49d21fe</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15475">#15475</a></li>
<li>chore(deps): update tj-actions/changed-files action to v41 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15476">#15476</a>)
(<a href="https://github.com/vitejs/vite/commit/2a540ee">2a540ee</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15476">#15476</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitejs/vite/commit/ee81e196769c102a6b1bf30f8444ccde236e71d5"><code>ee81e19</code></a>
release: v5.0.12</li>
<li><a
href="https://github.com/vitejs/vite/commit/91641c4da0a011d4c5352e88fc68389d4e1289a5"><code>91641c4</code></a>
fix: fs deny for case insensitive systems (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15653">#15653</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/ef89f8092f0eb1d8fd7d21256e6af8c4e64fe9b2"><code>ef89f80</code></a>
fix: await <code>configResolved</code> hooks of worker plugins (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15597">#15597</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15605">#15605</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/b44c49302ffbf0c82f984f6219ed6376d1e4552a"><code>b44c493</code></a>
release: v5.0.11</li>
<li><a
href="https://github.com/vitejs/vite/commit/d2aa0969ee316000d3b957d7e879f001e85e369e"><code>d2aa096</code></a>
fix: inject <code>__vite__mapDeps</code> code before sourcemap file
comment (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15483">#15483</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/2a540eee82f9a31deff8215bdbdccfa46d494a06"><code>2a540ee</code></a>
chore(deps): update tj-actions/changed-files action to v41 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15476">#15476</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/5ea9edbc9ceb991e85f893fe62d68ed028677451"><code>5ea9edb</code></a>
fix(html): handle offset magic-string slice error (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15435">#15435</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/49d21fe1feaac30dee0196bd484480a8000a4363"><code>49d21fe</code></a>
chore(deps): update dependency strip-literal to v2 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15475">#15475</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/8de7bd2b68db27b83d9484cc8d4e26436615168e"><code>8de7bd2</code></a>
fix(assets): avoid splitting <code>,</code> inside base64 value of
<code>srcset</code> attribute (#...</li>
<li><a
href="https://github.com/vitejs/vite/commit/19e3c9a8a16847486fbad8a8cd48fc771b1538bb"><code>19e3c9a</code></a>
fix: don't pretransform classic script links (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15361">#15361</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v5.0.12/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=5.0.10&new-version=5.0.12)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maplibre/maputnik/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 09:18:19 +02:00
HarelM 3b95b25777 Remove unneed console.log 2024-01-13 14:38:56 +02:00
Harel M 1da65f2116 Fix missing logo (#863)
Fixes #861
This should hopefully address the last item in the below issue of the
missing logo.
I tested it locally and it seems to be working as expected, I hope this
will be the case once deployed.
- #861
2024-01-12 18:46:10 +02:00
Harel M a62db148cd Remove Storybook, cleanups (#860)
1. Changed references to point to this new repo
2. Fixed docker image publishing to point to ghcr.io.
3. Remove survey link - the survey is closed and there's no point in
keeping it.
4. Remove storybook - Basically a storybook is the ability to look at
components and see how they look and interact with them.
It's a powerful tool for developing component library with "live"
documentation.
But it's an overkill for this project and I would like to reduce
maintenance costs.
Currently all the "stories" are in javascript and not in typescript and
it feels like a waste of time to try and maintain it, along with
updating the storybook library itself and everything around it.
2024-01-12 10:59:57 +02:00
HarelM 6ed10a862f Update pages deploy on every commit to main 2024-01-11 22:57:07 +02:00
HarelM 123e84f19b login to ghcr.io 2024-01-11 22:29:46 +02:00
HarelM d9b1b6f3ae Fix docker deploy, remove unneeded files 2024-01-11 22:24:47 +02:00
Harel M e0cef99c07 Update LICENSE 2024-01-11 22:11:18 +02:00
Harel M eb48bed32a Update MapLibre dependencies, add terrain editing (#859)
This PR aims at updating MapLibre dependencies.

The main goal of this update is to allow adding terrain specification to
the editor.
This requires version 4 of maplibre so currently it will use the
pre-release.
2024-01-11 22:05:47 +02:00
Harel M 87cf81d1c9 Prepare for on boarding (#858)
I believe this solves most of the missing checkboxes:
- https://github.com/maplibre/maplibre/issues/352

Let me know if there's anything else missing.
Other configuration can be done after the repo has been migrated I
believe.
2024-01-11 17:08:42 +02:00
ShellyDCMS 8e35ed97e6 Improve drivers (#856)
Co-authored-by: shelly_goldblit <shelly_goldblit@dell.com>
Co-authored-by: HarelM <harel.mazor@gmail.com>
2024-01-02 12:12:06 +02:00
Harel M 124ae98bf3 E2E: Improve tests, lint, and add more drivers (#855)
This PR introduces lint to cypress code, adds drivers to try and
abstract the usage of cypress as much as possible.
Nothing very interesting, mainly to try out the driver pattern for the
e2e tests.
2023-12-27 20:58:24 +02:00
Harel M b784bf2b84 Improve tests (#854)
Recover commented out tests, 
Improve usage of helper in driver, 
Added data-wd-key for test to use instead of classes
Moved all non tsx file to a single folder (lib) instead of lib and utils

---------

Co-authored-by: Yuri Astrakhan <yuriastrakhan@gmail.com>
2023-12-27 10:19:59 +02:00
Max 09a1f3f87b Use nginx to serve built files (#808)
Resolves #842
Better suited than python + handles sigterm better

Co-authored-by: Harel M <harel.mazor@gmail.com>
2023-12-26 23:28:17 +02:00
Harel M a22476cab2 Add lint to CI and fix errors (#853)
Adds lint to CI and fixes errors.
I'm not sure I'm fully proud of all the solutions there.
But there's no lint issues and the lint is being checked as part of the
CI.

---------

Co-authored-by: Yuri Astrakhan <yuriastrakhan@gmail.com>
2023-12-26 23:13:22 +02:00
ShellyDCMS a324ddb654 add code coverage (#852)
Add Cypress code coverage using Istanbul nyc

![image](https://github.com/maputnik/editor/assets/60476837/e7c364b7-7b54-4bf1-85aa-ee6fb19cc40c)

---------

Co-authored-by: shelly_goldblit <shelly_goldblit@dell.com>
Co-authored-by: Harel M <harel.mazor@gmail.com>
2023-12-26 12:04:43 +02:00
Harel M 656264f2bc Migration of jsx files to tsx 3 (#851)
This is in continue to:
- #850
- #848

The last files should be converted as part of this PR, there are only a
handful left.
2023-12-25 15:48:46 +02:00
Harel M 974dd7bfd9 Migration of jsx files to tsx 2 (#850)
This is to continue the work of migrating all the jsx files into tsx
files.
The MO is basically described here: #848.

About 7 files to go...
2023-12-22 23:32:25 +02:00
Kevin Schaul 7265bf0aa4 Refactor into a go module, update dependencies (#18)
* Refactor into a go module

`go get` is no longer supported outside of a go module. This commit
converts the project into a go module, moving dependencies into a go.mod
file.

* Use go.rice instead of go-bindata-assetfs

go.rice appears to be maintained and more popular

* Edit dependencies

* Pin to specific versions

* Updating workflows

* Fix syntax, I hope

* Update go

* Increment version number

* Version number

* version number
2023-07-18 16:44:57 +02:00
pathmapper c9504fcaed Merge pull request #16 from pathmapper/static
Add option to serve directory under /static/ (supersedes #2)
2020-07-24 18:31:30 +02:00
pathmapper b7ef0943f4 Fix formatting 2020-07-23 20:25:24 +02:00
pathmapper 4661677387 Add docs 2020-07-23 20:19:33 +02:00
pathmapper 77ed14a340 Update desktop version number 2020-07-23 19:52:38 +02:00
pathmapper e24d390f7c Fix build error 2020-07-23 19:33:23 +02:00
pathmapper 698fdfc958 Fix syntax error 2020-07-23 19:28:34 +02:00
pathmapper 77b3655c3c Add option to serve directory under /static/ (from #2) 2020-07-23 19:22:43 +02:00
pathmapper c264cd1771 Merge pull request #15 from pathmapper/update_desktop_version
Update desktop version number
2020-06-03 15:08:54 +02:00
pathmapper 1495d11462 Update desktop version number
The makefile was updated in https://github.com/maputnik/desktop/pull/14
2020-06-03 15:05:47 +02:00
pathmapper fda11e52e7 Merge pull request #14 from pathmapper/makefile_editor
Update for use in the editor ci workflow
2020-05-31 21:43:57 +02:00
pathmapper d9e3aa6ac4 Update for use in the editor ci workflow 2020-05-31 21:41:02 +02:00
pathmapper aeca95a27f Merge pull request #13 from pathmapper/rename_ci
Rename ci
2020-05-31 21:30:22 +02:00
pathmapper 7dfcdac202 Rename ci 2020-05-31 21:23:52 +02:00
pathmapper 4f156ee3fd Merge pull request #12 from pathmapper/gh-actions
Replace Travis with Github Actions
2020-05-31 21:14:39 +02:00
pathmapper 6d00214f55 Update README.md 2020-05-31 21:09:02 +02:00
pathmapper 1e7b6e809c Remove Travis 2020-05-31 21:05:00 +02:00
pathmapper cdcc61e234 Create go.yml 2020-05-31 21:04:11 +02:00
pathmapper 9c63172d36 Merge pull request #11 from pathmapper/travis
Travis: Update go version and use OSM Liberty
2020-05-28 23:00:06 +02:00
pathmapper 6f21fd8dff Use OSM Liberty style 2020-05-28 22:56:08 +02:00
pathmapper 0e788c5841 Update go version 2020-05-28 22:55:39 +02:00
pathmapper 7229df704a Merge pull request #10 from pathmapper/1.7.0
Update Editor to v1.7.0
2020-05-28 22:08:56 +02:00
pathmapper 686fd27b35 Update Editor to v1.7.0 2020-05-28 22:05:48 +02:00
pathmapper 293342e4fb Update Desktop version number 2020-05-28 22:04:40 +02:00
pathmapper 03d9b946e7 Merge pull request #9 from miya0001/patch
Fixed the error `cannot use cli.StringFlag literal ...`
2020-05-28 21:54:55 +02:00
Takayuki Miyauchi 4a3825fa89 fixed the error cannot use cli.StringFlag literal ... 2020-02-21 07:24:00 +09:00
pathmapper 5371b0f9fb Merge pull request #8 from pathmapper/editor_1.6.1
Update editor to v1.6.1
2019-10-13 17:50:22 +02:00
pathmapper 538cea7f45 Update editor to v1.6.1 2019-10-13 14:28:42 +02:00
pathmapper fdfc470ccc Merge pull request #6 from pathmapper/update_versions
Update version numbers
2019-10-10 14:30:14 +02:00
pathmapper 3153eea1da Update version numbers 2019-10-08 22:29:45 +02:00
pathmapper 8471d0af3d Merge pull request #5 from JesseCrocker/choose-listen-port
Add option to choose a listening port
2019-10-08 22:20:05 +02:00
Jesse Crocker 1ce2d59b9b Add --port option to readme 2019-09-04 09:21:56 -06:00
Jesse Crocker d951256b1c Add option to choose a listening port 2019-04-15 12:29:15 -06:00
pathmapper ec753869d5 Merge pull request #3 from maputnik/update-release-to-v1.5.0
Update release to v1.5.0
2019-02-09 14:07:52 +01:00
Lukas Welte 2e58be1c90 Switch around editor and desktop in version field so that it prints nicer 2019-02-09 13:47:58 +01:00
Lukas Welte 562a4f7322 Remove appveyor status from README 2019-02-09 13:45:45 +01:00
Lukas Welte e52a63e1dd Remove the deprecation warning from the readme 2019-02-09 12:47:47 +01:00
Lukas Welte 4533fd06ed Have Desktop version (changes in Desktop code) and editor version in version field 2019-02-09 12:39:32 +01:00
pathmapper b4fc62632c Merge pull request #4 from pathmapper/pathmapper-update-readme
Update README.md
2019-02-09 10:28:15 +01:00
pathmapper e3a9a8a38c Update README.md 2019-02-09 10:09:05 +01:00
pathmapper 7c4e982fb3 Merge pull request #1 from fawick/master
Fix README.md
2019-02-09 09:51:13 +01:00
Lukas Welte 85dd22b09a Use recent go version 2019-02-07 17:58:11 +01:00
Lukas Welte 18e15eeb5c remove complete bin folder in make clean 2019-02-07 17:35:23 +01:00
Lukas Welte 3a45b8dd41 Move dependencies into Makefile 2019-02-07 17:33:38 +01:00
Lukas Welte 5b8412765b Linux should be enough to build go for linux, windows and mac 2019-02-07 17:29:49 +01:00
Lukas Welte 69519df82f Use folders for different maputnik executables 2019-02-07 17:25:27 +01:00
Lukas Welte 8052701021 Use go cross compile to build once with travis for all platforms 2019-02-07 17:18:48 +01:00
Lukas Welte 35c0150522 Download release instead of building editor from source 2019-02-07 16:05:02 +01:00
Lukas Welte c55278e7da Remove editor submodule 2019-02-07 15:56:29 +01:00
Lukas Martinelli d3ecef3de6 Update README.md 2018-04-15 16:14:22 +05:30
Fabian Wickborn 3ef0a90de4 Fix README.md 2017-10-25 20:33:45 +02:00
Lukas Martinelli 87290889fd Upgrade to v1.0.2 2017-01-25 13:57:27 +01:00
Lukas Martinelli 1997e31b6b Build latest 2017-01-18 13:06:43 +01:00
Lukas Martinelli 5b21a2fa4f Build latest 2017-01-18 13:03:50 +01:00
Lukas Martinelli d314add6a9 Release v1.0.1 of CLI 2017-01-16 10:18:27 +01:00
Lukas Martinelli 50d61cdb0e Move to v1.0.0 2017-01-13 21:56:06 +01:00
Lukas Martinelli 4ffea21c5f Upgrade to latest Maputnik 2017-01-10 14:57:13 +01:00
Lukas Martinelli d29a79e79f Update editor 2017-01-08 17:15:57 +01:00
Lukas Martinelli 004177a3c8 Kill watch ocmmand after 5s 2017-01-08 13:40:38 +01:00
Lukas Martinelli b9e70a943f Update x/sys 2017-01-08 13:29:20 +01:00
Lukas Martinelli de853eb2d7 Use Go 1.7 explicitely in Travis 2017-01-08 13:29:20 +01:00
Lukas Martinelli f242c2c015 Update README.md 2017-01-08 10:45:01 +01:00
Lukas Martinelli d895cf079c Execute maputnik in travis 2017-01-08 10:39:12 +01:00
Lukas Martinelli 147cbc1580 Include subdirectories in embedded assets 2017-01-01 21:12:24 +01:00
Lukas Martinelli 3e46c0d3ba Set version, badge and download instruction 2017-01-01 16:49:18 +01:00
Lukas Martinelli d233e0a14d Create public dir 2017-01-01 16:33:29 +01:00
Lukas Martinelli a73b2fd7e1 Use nvm for travis 2017-01-01 16:24:36 +01:00
Lukas Martinelli 69f63f2844 Use latest node in desktop build 2017-01-01 16:22:01 +01:00
Lukas Martinelli 10136c07db Publish artifacts 2017-01-01 16:16:18 +01:00
Lukas Martinelli 5e3156ab21 Fetch submodule in appveyor 2017-01-01 16:00:59 +01:00
Lukas Martinelli 6be8959951 Update to latest master 2017-01-01 15:52:10 +01:00
Lukas Martinelli d2cd84de2b Switch filewatch implementation to fsnotify 2017-01-01 15:50:58 +01:00
Lukas Martinelli 60bea1777a Ugrade to v0.3.1 2016-12-31 16:12:07 +01:00
Lukas Martinelli ce9216b2d5 Write and read file directly 2016-12-03 23:04:32 +01:00
Lukas Martinelli 35ed202cd0 Support CORS 2016-12-03 23:04:12 +01:00
Lukas Martinelli 0d77518a02 Support saving file 2016-12-03 16:51:47 +01:00
lukasmartinelli 11375008fa Extend README with endpoint desription 2016-11-05 17:40:19 +01:00
lukasmartinelli 009f4e105d Specify path to mingw make 2016-11-03 13:29:14 +01:00
lukasmartinelli b1af4917e5 Add missing bindata deps 2016-11-03 13:26:14 +01:00
lukasmartinelli 5b712d74ae Use MinGW make in AppVeyor 2016-11-03 13:24:02 +01:00
lukasmartinelli ca2df37c79 Fix go get in AppVeyor 2016-11-03 13:19:58 +01:00
lukasmartinelli 7d1890156d Update Travis go get 2016-11-03 13:18:08 +01:00
lukasmartinelli ecab640a9a Add AppVeyor integration 2016-11-02 17:10:40 +01:00
lukasmartinelli f8cb0619f3 Use HTTPS endpoint for submodule 2016-11-02 17:08:48 +01:00
lukasmartinelli d874b2503b Add Travis file 2016-11-02 17:01:46 +01:00
lukasmartinelli 3d09f2a0f3 Add server usage example 2016-09-27 17:31:14 +02:00
lukasmartinelli 1f1580276d Integrate file access into API 2016-09-27 14:28:40 +02:00
lukasmartinelli 0421a7f099 Add websocket filewatcher 2016-09-27 11:45:27 +02:00
lukasmartinelli 8b722fc967 Add Makefile 2016-09-27 10:59:56 +02:00
lukasmartinelli 66e3ce8743 Add server and bindata packaging 2016-09-27 10:27:47 +02:00
Lukas Martinelli 45eb3a01e6 Initial commit 2016-09-27 10:08:08 +02:00
209 changed files with 8791 additions and 13119 deletions
-4
View File
@@ -1,4 +0,0 @@
{
"packages": [],
"sandboxes": ["/"]
}
+46
View File
@@ -0,0 +1,46 @@
{
"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"
}
}
-20
View File
@@ -1,20 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
+2 -1
View File
@@ -1 +1,2 @@
custom: "https://maputnik.github.io/donate"
github: [maplibre]
open_collective: maplibre
+11
View File
@@ -0,0 +1,11 @@
## Launch Checklist
<!-- Thanks for the PR! Feel free to add or remove items from the checklist. -->
- [ ] Briefly describe the changes in this PR.
- [ ] Link to related issues.
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
- [ ] Write tests for all new functionality.
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
+34 -61
View File
@@ -8,23 +8,18 @@ on:
jobs:
build-docker:
name: build/docker
runs-on: ${{ matrix.os }}
name: build docker
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
- run: docker build -t test-docker-image-build .
# build the editor
build-node:
name: "build/node@${{ matrix.node-version }} (${{ matrix.os }})"
name: "build on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
@@ -33,95 +28,68 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18.x]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v1
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
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 (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
name: "build artifacts"
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node-version: [18.x]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v1
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- run: npm run build-storybook
- name: artifacts/editor
uses: actions/upload-artifact@v1
- name: artifacts/maputnik
uses: actions/upload-artifact@v4
with:
name: editor
name: maputnik
path: dist
- name: artifacts/storybook
uses: actions/upload-artifact@v1
with:
name: storybook
path: build/storybook
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: ^1.19.x
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v4
with:
repository: maputnik/desktop
ref: master
path: ./src/github.com/maputnik/desktop/
- name: Make
run: cd src/github.com/maputnik/desktop/ && make
- name: Build desktop artifacts
run: npm run build-desktop
- name: Artifacts/linux
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: maputnik-linux
path: ./src/github.com/maputnik/desktop/bin/linux/
path: ./desktop/bin/linux/
- name: Artifacts/darwin
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: maputnik-darwin
path: ./src/github.com/maputnik/desktop/bin/darwin/
path: ./desktop/bin/darwin/
- name: Artifacts/windows
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: maputnik-windows
path: ./src/github.com/maputnik/desktop/bin/windows/
path: ./desktop/bin/windows/
cypress-run:
e2e-tests:
name: "E2E tests using ${{ matrix.browser }}"
strategy:
fail-fast: false
matrix:
@@ -138,3 +106,8 @@ jobs:
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
@@ -0,0 +1,39 @@
name: Create bump version PR
on:
workflow_dispatch:
inputs:
version:
description: Version to change to.
required: true
type: string
jobs:
bump-version-pr:
name: Bump version PR
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Bump version
run: |
npm version --commit-hooks false --git-tag-version false ${{ inputs.version }}
./build/bump-version-changelog.js ${{ inputs.version }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
commit-message: Bump version to ${{ inputs.version }}
branch: bump-version-to-${{ inputs.version }}
title: Bump version to ${{ inputs.version }}
+33 -8
View File
@@ -3,24 +3,49 @@ name: deploy
on:
push:
branches: [ main ]
tags:
- 'v*'
jobs:
deploy-pages:
name: deploy/pages
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }}
steps:
- uses: actions/checkout@v4
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install
run: npm ci
- name: Build
run: npm run build
- name: Upload to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: dist
# publish docker to GitHub registry
deploy-docker:
name: deploy/docker
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v4
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
- run: docker push docker.pkg.github.com/maputnik/editor/editor:main
- run: docker build -t ghcr.io/maplibre/maputnik:main .
- run: docker push ghcr.io/maplibre/maputnik:main
+104
View File
@@ -0,0 +1,104 @@
name: Release
on:
push:
branches: [main]
workflow_dispatch:
jobs:
release-check:
name: Check if version changed
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Check if version has been updated
id: check
uses: EndBug/version-check@v2
outputs:
publish: ${{ steps.check.outputs.changed }}
release:
name: Release
needs: release-check
if: ${{ needs.release-check.outputs.publish == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build
uses: actions/setup-go@v5
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.3.1
- name: Install
run: npm ci
- name: Build
run: |
npm run build
npm run build-desktop
- name: Tag commit and push
id: tag_version
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ steps.package-version.outputs.current-version }}
- name: Create Archives
run: |
zip -r dist dist
zip -r desktop desktop/bin/
- name: Build Release Notes
id: release_notes
run: |
RELEASE_NOTES_PATH="${PWD}/release_notes.txt"
./build/release-notes.js > ${RELEASE_NOTES_PATH}
echo "release_notes=${RELEASE_NOTES_PATH}" >> $GITHUB_OUTPUT
- name: Create GitHub Release
id: create_regular_release
uses: ncipollo/release-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: ${{ steps.tag_version.outputs.new_tag }}
bodyFile: ${{ steps.release_notes.outputs.release_notes }}
artifacts: "dist.zip,desktop.zip"
allowUpdates: true
draft: false
prerelease: false
+2 -1
View File
@@ -14,6 +14,7 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
@@ -32,6 +33,6 @@ node_modules
public
/errorShots
/old
/build
/cypress/screenshots
/dist/
/desktop/version.go
+1
View File
@@ -0,0 +1 @@
legacy-peer-deps = true
+1
View File
@@ -0,0 +1 @@
18.19
+18
View File
@@ -0,0 +1,18 @@
{
"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"]
}
-9
View File
@@ -1,9 +0,0 @@
const config = {
stories: ['../stories/**/*.stories.jsx'],
addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-a11y/register', '@storybook/addon-storysource'],
framework: {
name: '@storybook/react-vite',
options: {}
}
};
export default config;
-7
View File
@@ -1,7 +0,0 @@
import { addons } from '@storybook/addons';
import { themes } from '@storybook/theming';
import theme from './maputnik.theme';
addons.setConfig({
theme: theme,
});
-8
View File
@@ -1,8 +0,0 @@
import { create } from '@storybook/theming/create';
export default create({
base: 'light',
brandTitle: 'Maputnik',
brandUrl: 'https://github.com/maputnik/editor',
});
+24
View File
@@ -0,0 +1,24 @@
## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
## 2.1.1
### ✨ Features and improvements
- Add GitHub workflows for releasing new versions
- Update desktop build to pull from this repo (#922)
## 2.0.0
- Update MapLibre to version 4 (#872)
- Start continuous deployment of maputnik website
## 1.7.0
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
+2
View File
@@ -0,0 +1,2 @@
# 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)
+6 -12
View File
@@ -2,21 +2,15 @@ FROM node:18 as builder
WORKDIR /maputnik
# Only copy package.json to prevent npm install from running on every build
COPY package.json package-lock.json ./
RUN npm install
COPY package.json package-lock.json .npmrc ./
RUN npm ci
# Build maputnik
# TODO: we should also do a npm run test here (needs more dependencies)
COPY . .
RUN npm run build
RUN npx vite build
#---------------------------------------------------------------------------
# Create a clean nginx-alpine slim image with just the build results
FROM nginx:alpine-slim
# Create a clean python-based image with just the build results
FROM python:3-slim
WORKDIR /maputnik
COPY --from=builder /maputnik/dist .
EXPOSE 8888
CMD python -m http.server 8888
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
+1
View File
@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2015 Lukas Martinelli
Copyright (c) 2024 MapLibre contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+24 -69
View File
@@ -1,10 +1,10 @@
<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/maputnik/editor/workflows/ci/badge.svg)][github-action-ci]
[![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/maputnik/editor/actions?query=workflow%3Aci
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci
[license]: https://tldrlegal.com/license/mit-license
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
@@ -13,33 +13,32 @@ targeted at developers and map designers.
## Usage
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/wiki/Maputnik-CLI) for local style development
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
```bash
docker run -it --rm -p 8888:8888 maputnik/editor
docker run -it --rm -p 8888:80 ghcr.io/maplibre/maputnik:main
```
## Donations
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
## Documentation
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)**
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
[![Design Map from Scratch](https://j.gifs.com/g5XMgl.gif)](https://youtu.be/XoDh0gEnBQo)
## Develop
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
Maputnik is written in typescript and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
### Getting Involved
Join the #maplibre or #maputnik slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/ Read the the below guide in order to get familiar with how we do things around here.
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
```bash
@@ -67,7 +66,8 @@ Lint the JavaScript code.
```
# run linter
npm run lint
npm run lint-styles
npm run lint-css
npm run sort-styles
```
@@ -94,68 +94,23 @@ You can also see the tests as they run or select which suites to run by executin
npm run cy:open
```
## Release process
## Related Projects
1. Review [`CHANGELOG.md`](/CHANGELOG.md)
- Double-check that all changes included in the release are appropriately documented.
- To-be-released changes should be under the "main" header.
- Commit any final changes to the changelog.
2. Run [Create bump version PR](https://github.com/maplibre/maputnik/actions/workflows/create-bump-version-pr.yml) by manual workflow dispatch and set the version number in the input. This will create a PR that changes the changelog and `package.json` file to review and merge.
3. Once merged, an automatic process will kick in and creates a GitHub release and uploads release assets.
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
## Sponsors
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
### Gold
- [Wemap](https://getwemap.com/)
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
- [Terranodo](http://terranodo.io/)
<a href="https://getwemap.com/">
<img width="33%" alt="Wemap" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/wemap.jpg" />
</a>
<a href="http://terranodo.io/">
<img width="33%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/terranodo.png" />
</a>
<a href="https://www.orbiconinformatik.dk/">
<img width="32%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/orbicon_informatik.png" />
</a>
<br/>
### Silver
- [Klokan Technologies](https://www.klokantech.com/)
- [Geofabrik](http://www.geofabrik.de/)
- [Dreipol](https://www.dreipol.ch/)
<a href="https://www.klokantech.com/">
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/klokantech.png" />
</a>
<a href="http://www.geofabrik.de/">
<img width="18%" alt="Geofabrik" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/geofabrik.png" />
</a>
<a href="https://www.dreipol.ch/">
<img width="18%" alt="Dreipol" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/dreipol.png" />
</a>
<br/>
### Individuals
**Influential Stakeholder**
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
**Stakeholder**
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
**Supporter**
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
You can see this file's history for previous sponsors of the original Maputnik repo.
Read more about the MapLibre Sponsorship Program at https://maplibre.org/sponsors/.
## License
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
**Disclaimer** This is an independent style editor.
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and Maplibre contributors.
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by other map studios and make your own decisions for a good style editor.
+2
View File
@@ -0,0 +1,2 @@
For an up-to-date policy refer to
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt
+11
View File
@@ -0,0 +1,11 @@
# Build Scripts
This folder holds common build scripts used by some of the Github workflows.
The scripts are borrowed from [maplibre/maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/tree/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build).
## Generate Release Notes
`bump-version-changelog.js` Used to update the changelog with the current notes, and set up a space for new notes
`release-notes.js` Used to generate release notes when releasing a new version
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env node
/**
* This script updates the changelog.md file with the version given in the arguments
* It replaces ## main with ## <version>
* Removes _...Add new stuff here..._
* And adds on top a ## main with add stuff here.
*
* Copied from maplibre/maplibre-gl-js
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
*/
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
let changelog = fs.readFileSync(changelogPath, 'utf8');
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
changelog = `## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
` + changelog;
fs.writeFileSync(changelogPath, changelog, 'utf8');
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env node
// Copied from maplibre/maplibre-gl-js
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
const changelog = fs.readFileSync(changelogPath, 'utf8');
/*
Parse the raw changelog text and split it into individual releases.
This regular expression:
- Matches lines starting with "## x.x.x".
- Groups the version number.
- Skips the (optional) release date.
- Groups the changelog content.
- Ends when another "## x.x.x" is found.
*/
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
let releaseNotes = [];
let match;
// eslint-disable-next-line no-cond-assign
while (match = regex.exec(changelog)) {
releaseNotes.push({
'version': match[1],
'changelog': match[2].trim(),
});
}
const latest = releaseNotes[0];
const previous = releaseNotes[1];
// Print the release notes template.
let header = 'Changes since previous version'
if (previous) {
header = `https://github.com/maplibre/maputnik
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`
}
const templatedReleaseNotes = `${header}
${latest.changelog}`;
// eslint-disable-next-line eol-last
process.stdout.write(templatedReleaseNotes.trimEnd());
+10
View File
@@ -1,10 +1,20 @@
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,
+11 -11
View File
@@ -1,7 +1,7 @@
import MaputnikDriver from "./driver";
import { MaputnikDriver } from "./maputnik-driver";
describe("accessibility", () => {
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("skip links", () => {
@@ -11,30 +11,30 @@ describe("accessibility", () => {
it("skip link to layer list", () => {
const selector = "root:skip:layer-list";
should.exist(selector);
then(get.elementByTestId(selector)).shouldExist();
when.tab();
should.beFocused(selector);
then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector);
should.beFocused("skip-target-layer-list");
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";
should.exist(selector);
then(get.elementByTestId(selector)).shouldExist();
when.tab().tab();
should.beFocused(selector);
then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector);
should.beFocused("skip-target-layer-editor");
then(get.skipTargetLayerEditor()).shouldBeFocused();
});
it("skip link to map view", () => {
const selector = "root:skip:map-view";
should.exist(selector);
then(get.elementByTestId(selector)).shouldExist();
when.tab().tab().tab();
should.beFocused(selector);
then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector);
should.canvasBeFocused();
then(get.canvas()).shouldBeFocused();
});
});
});
-210
View File
@@ -1,210 +0,0 @@
import { CypressHelper } from "@shellygo/cypress-test-utils";
import { v1 as uuid } from "uuid";
export default class MaputnikDriver {
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
public beforeAndAfter = () => {
beforeEach(() => {
this.given.setupInterception();
this.when.setStyle("both");
});
};
public given = {
setupInterception: () => {
cy.intercept("GET", "http://localhost:8888/example-style.json", {
fixture: "example-style.json",
}).as("example-style.json");
cy.intercept("GET", "http://localhost:8888/example-layer-style.json", {
fixture: "example-layer-style.json",
});
cy.intercept("GET", "http://localhost:8888/geojson-style.json", {
fixture: "geojson-style.json",
});
cy.intercept("GET", "http://localhost:8888/raster-style.json", {
fixture: "raster-style.json",
});
cy.intercept("GET", "http://localhost:8888/geojson-raster-style.json", {
fixture: "geojson-raster-style.json",
});
cy.intercept({ method: "GET", url: "*example.local/*" }, []);
cy.intercept({ method: "GET", url: "*example.com/*" }, []);
},
};
public when = {
within: (selector: string, fn: () => void) => {
this.helper.when.within(fn, selector);
},
tab: () => cy.get("body").tab(),
waitForExampleFileRequset: () => {
this.helper.when.waitForResponse("example-style.json");
},
chooseExampleFile: () => {
cy.get("input[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=http://localhost:8888/geojson-style.json";
break;
case "raster":
url += "&style=http://localhost:8888/raster-style.json";
break;
case "both":
url += "&style=http://localhost:8888/geojson-raster-style.json";
break;
case "layer":
url += "&style=http://localhost:8888/example-layer-style.json";
break;
}
if (zoom) {
url += "#" + zoom + "/41.3805/2.1635";
}
cy.visit("http://localhost:8888/" + url);
if (styleProperties) {
cy.on("window:confirm", () => true);
}
cy.get(".maputnik-toolbar-link").should("be.visible");
},
fillLayersModal: (opts: {type: string, layer?: string, id?: string}) => {
var type = opts.type;
var layer = opts.layer;
var id;
if (opts.id) {
id = opts.id;
} else {
id = `${type}:${uuid()}`;
}
cy.get(
this.get.dataAttribute("add-layer.layer-type", "select")
).select(type);
cy.get(this.get.dataAttribute("add-layer.layer-id", "input")).type(id);
if (layer) {
cy.get(
this.get.dataAttribute("add-layer.layer-source-block", "input")
).type(layer);
}
this.when.click("add-layer");
return id;
},
typeKeys: (keys: string) => {
cy.get("body").type(keys);
},
click: (selector: string) => {
this.helper.when.click(selector);
},
clickZoomin: () => {
cy.get(".maplibregl-ctrl-zoom-in").click();
},
selectWithin: (selector: string, value: string) => {
this.when.within(selector, () => {
cy.get("select").select(value);
});
},
select: (selector: string, value: string) => {
this.helper.get.element(selector).select(value);
},
focus: (selector: string) => {
this.helper.when.focus(selector);
},
setValue: (selector: string, text: string) => {
cy.get(selector).clear().type(text, { parseSpecialCharSequences: false });
},
closeModal: (key: string) => {
this.helper.when.waitUntil(() => this.helper.get.element(key));
this.when.click(key + ".close-modal");
},
openLayersModal: () => {
this.helper.when.click("layer-list:add-layer");
cy.get(this.get.dataAttribute("modal:add-layer")).should("exist");
cy.get(this.get.dataAttribute("modal:add-layer")).should("be.visible");
},
};
public get = {
isMac: () => {
return Cypress.platform === "darwin";
},
styleFromWindow: (win: Window) => {
const styleId = win.localStorage.getItem("maputnik:latest_style");
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
const obj = JSON.parse(styleItem || "");
return obj;
},
exampleFileUrl: () => {
return "http://localhost:8888/example-style.json";
},
dataAttribute: (key: string, selector?: string): string => {
return `*[data-wd-key='${key}'] ${selector || ""}`;
},
};
public should = {
canvasBeFocused: () => {
this.when.within("maplibre:map", () => {
cy.get("canvas").should("be.focused");
});
},
notExist: (selector: string) => {
cy.get(selector).should("not.exist");
},
beFocused: (selector: string) => {
this.helper.get.element(selector).should("have.focus");
},
notBeFocused: (selector: string) => {
this.helper.get.element(selector).should("not.have.focus");
},
beVisible: (selector: string) => {
this.helper.get.element(selector).should("be.visible");
},
notBeVisible: (selector: string) => {
this.helper.get.element(selector).should("not.be.visible");
},
equalStyleStore: (getter: (obj: any) => any, styleObj: any) => {
cy.window().then((win: any) => {
const obj = this.get.styleFromWindow(win);
assert.deepEqual(getter(obj), styleObj);
});
},
styleStoreEqualToExampleFileData: () => {
cy.window().then((win: any) => {
const obj = this.get.styleFromWindow(win);
cy.fixture("example-style.json").should("deep.equal", obj);
});
},
exist: (selector: string) => {
this.helper.get.element(selector).should("exist");
},
beSelected: (selector: string, value: string) => {
this.helper.get.element(selector).find(`option[value="${value}"]`).should("be.selected");
},
containText: (selector: string, text: string) => {
this.helper.get.element(selector).should("contain.text", text);
}
};
}
+63 -35
View File
@@ -1,7 +1,7 @@
import MaputnikDriver from "./driver";
import { MaputnikDriver } from "./maputnik-driver";
describe("history", () => {
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter();
let undoKeyCombo: string;
@@ -15,34 +15,30 @@ describe("history", () => {
it("undo/redo", () => {
when.setStyle("geojson");
when.openLayersModal();
when.modal.open();
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
should.equalStyleStore((a: any) => a.layers, []);
when.fillLayersModal({
when.modal.fillLayers({
id: "step 1",
type: "background",
});
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
]
);
],
});
when.openLayersModal();
when.fillLayersModal({
when.modal.open();
when.modal.fillLayers({
id: "step 2",
type: "background",
});
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
@@ -51,38 +47,35 @@ describe("history", () => {
id: "step 2",
type: "background",
},
]
);
],
});
when.typeKeys(undoKeyCombo);
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
]
);
],
});
when.typeKeys(undoKeyCombo);
should.equalStyleStore((a: any) => a.layers, []);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.typeKeys(redoKeyCombo);
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
]
);
],
});
when.typeKeys(redoKeyCombo);
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
@@ -91,7 +84,42 @@ describe("history", () => {
id: "step 2",
type: "background",
},
]
);
],
});
});
it("should not redo after undo and value change", () => {
when.setStyle("geojson");
when.modal.open();
when.modal.fillLayers({
id: "step 1",
type: "background",
});
when.modal.open();
when.modal.fillLayers({
id: "step 2",
type: "background",
});
when.typeKeys(undoKeyCombo);
when.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.modal.open();
when.modal.fillLayers({
id: "step 3",
type: "background",
});
when.typeKeys(redoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 3",
type: "background",
},
],
});
});
});
+35
View File
@@ -0,0 +1,35 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("i18n", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("language detector", () => {
it("English", () => {
const url = "?lng=en";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
});
it("Japanese", () => {
const url = "?lng=ja";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
describe("language switcher", () => {
beforeEach(() => {
when.setStyle("layer");
});
it("the language switcher switches to Japanese", () => {
const selector = "maputnik-lang-select";
then(get.elementByTestId(selector)).shouldExist();
when.select(selector, "ja");
then(get.elementByTestId(selector)).shouldHaveValue("ja");
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
});
});
});
+13 -14
View File
@@ -1,61 +1,60 @@
import MaputnikDriver from "./driver";
import { MaputnikDriver } from "./maputnik-driver";
describe("keyboard", () => {
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
let { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
beforeAndAfter();
describe("shortcuts", () => {
beforeEach(() => {
given.setupInterception();
given.setupMockBackedResponses();
when.setStyle("");
});
it("ESC should unfocus", () => {
const targetSelector = "maputnik-select";
when.focus(targetSelector);
should.beFocused(targetSelector);
then(get.elementByTestId(targetSelector)).shouldBeFocused();
when.typeKeys("{esc}");
expect(should.notBeFocused(targetSelector));
then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
});
it("'?' should show shortcuts modal", () => {
when.typeKeys("?");
should.beVisible("modal:shortcuts");
then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
});
it("'o' should show open modal", () => {
when.typeKeys("o");
should.beVisible("modal:open");
then(get.elementByTestId("modal:open")).shouldBeVisible();
});
it("'e' should show export modal", () => {
when.typeKeys("e");
should.beVisible("modal:export");
then(get.elementByTestId("modal:export")).shouldBeVisible();
});
it("'d' should show sources modal", () => {
when.typeKeys("d");
should.beVisible("modal:sources");
then(get.elementByTestId("modal:sources")).shouldBeVisible();
});
it("'s' should show settings modal", () => {
when.typeKeys("s");
should.beVisible("modal:settings");
then(get.elementByTestId("modal:settings")).shouldBeVisible();
});
it("'i' should change map to inspect mode", () => {
when.typeKeys("i");
should.beSelected("nav:inspect", "inspect");
then(get.inputValue("maputnik-select")).shouldEqual("inspect");
});
it("'m' should focus map", () => {
when.typeKeys("m");
should.canvasBeFocused();
then(get.canvas()).shouldBeFocused();
});
it("'!' should show debug modal", () => {
when.typeKeys("!");
should.beVisible("modal:debug");
then(get.elementByTestId("modal:debug")).shouldBeVisible();
});
});
});
+279 -273
View File
@@ -1,155 +1,137 @@
import { v1 as uuid } from "uuid";
import MaputnikDriver from "./driver";
import { MaputnikDriver } from "./maputnik-driver";
describe("layers", () => {
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => {
when.setStyle("both");
when.openLayersModal();
when.modal.open();
});
describe("ops", () => {
it("delete", () => {
var id = when.fillLayersModal({
let id: string;
beforeEach(() => {
id = when.modal.fillLayers({
type: "background",
});
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
},
]
);
when.click("layer-list-item:" + id + ":delete");
should.equalStyleStore((a: any) => a.layers, []);
});
it("duplicate", () => {
var styleObj;
var id = when.fillLayersModal({
type: "background",
it("should update layers in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
},
],
});
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
},
]
);
when.click("layer-list-item:" + id + ":copy");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id + "-copy",
type: "background",
},
{
id: id,
type: "background",
},
]
);
});
it("hide", () => {
var styleObj;
var id = when.fillLayersModal({
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");
});
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
},
]
);
when.click("layer-list-item:" + id + ":toggle-visibility");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
layout: {
visibility: "none",
it("should update visibility to none in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
layout: {
visibility: "none",
},
},
},
]
);
],
});
});
when.click("layer-list-item:" + id + ":toggle-visibility");
describe("when clicking show", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility");
});
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
layout: {
visibility: "visible",
},
},
]
);
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", () => {
var id = when.fillLayersModal({
let id = when.modal.fillLayers({
type: "background",
});
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
},
]
);
],
});
});
describe("modify", () => {
function createBackground() {
// Setup
var id = uuid();
let id = uuid();
when.selectWithin("add-layer.layer-type", "background");
when.setValue(
get.dataAttribute("add-layer.layer-id", "input"),
"background:" + id
);
when.setValue("add-layer.layer-id.input", "background:" + id);
when.click("add-layer");
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + id,
type: "background",
},
]
);
],
});
return id;
}
@@ -157,137 +139,166 @@ describe("layers", () => {
describe("layer", () => {
it("expand/collapse");
it("id", () => {
var bgId = createBackground();
let bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
var id = uuid();
when.setValue(
get.dataAttribute("layer-editor.layer-id", "input"),
"foobar:" + id
);
let id = uuid();
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
when.click("min-zoom");
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "foobar:" + id,
type: "background",
},
]
);
],
});
});
it("min-zoom", () => {
var bgId = createBackground();
describe("min-zoom", () => {
let bgId: string;
when.click("layer-list-item:background:" + bgId);
when.setValue(
get.dataAttribute("min-zoom", 'input[type="text"]'),
"1"
);
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.setValue("min-zoom.input-text", "1");
when.click("layer-editor.layer-id");
});
when.click("layer-editor.layer-id");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "background:" + bgId,
type: "background",
minzoom: 1,
},
]
);
// AND RESET!
// driver.setValue(driver.get.dataAttribute("min-zoom", "input"), "")
// driver.click(driver.get.dataAttribute("max-zoom", "input"));
// driver.isStyleStoreEqual((a: any) => a.layers, [
// {
// "id": 'background:'+bgId,
// "type": 'background'
// }
// ]);
});
it("max-zoom", () => {
var bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.setValue(
get.dataAttribute("max-zoom", 'input[type="text"]'),
"1"
);
when.click("layer-editor.layer-id");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "background:" + bgId,
type: "background",
maxzoom: 1,
},
]
);
});
it("comments", () => {
var bgId = createBackground();
var id = uuid();
when.click("layer-list-item:background:" + bgId);
when.setValue(get.dataAttribute("layer-comment", "textarea"), id);
when.click("layer-editor.layer-id");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "background:" + bgId,
type: "background",
metadata: {
"maputnik:comment": id,
it("should update min-zoom in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
minzoom: 1,
},
},
]
);
],
});
});
// Unset it again.
// TODO: This fails
// driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), "");
// driver.click(driver.getDataAttribute("min-zoom", "input"));
// driver.isStyleStoreEqual((a: any) => a.layers, [
// {
// "id": 'background:'+bgId,
// "type": 'background'
// }
// ]);
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,
},
],
});
});
});
it("color", () => {
var bgId = createBackground();
describe("max-zoom", () => {
let bgId: string;
when.click("layer-list-item:background:" + bgId);
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.setValue("max-zoom.input-text", "1");
when.click("layer-editor.layer-id");
});
when.click("spec-field:background-color");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "background:" + bgId,
type: "background",
},
]
);
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", () => {
@@ -309,18 +320,18 @@ describe("layers", () => {
// TODO
it.skip("parse error", () => {
var bgId = createBackground();
let bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
var errorSelector = ".CodeMirror-lint-marker-error";
should.notExist(errorSelector);
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 {"
);
should.exist(errorSelector);
then(get.elementByTestId(errorSelector)).shouldExist();
});
});
});
@@ -328,21 +339,20 @@ describe("layers", () => {
describe("fill", () => {
it("add", () => {
var id = when.fillLayersModal({
let id = when.modal.fillLayers({
type: "fill",
layer: "example",
});
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "fill",
source: "example",
},
]
);
],
});
});
// TODO: Change source
@@ -351,21 +361,20 @@ describe("layers", () => {
describe("line", () => {
it("add", () => {
var id = when.fillLayersModal({
let id = when.modal.fillLayers({
type: "line",
layer: "example",
});
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "line",
source: "example",
},
]
);
],
});
});
it("groups", () => {
@@ -376,81 +385,77 @@ describe("layers", () => {
describe("symbol", () => {
it("add", () => {
var id = when.fillLayersModal({
let id = when.modal.fillLayers({
type: "symbol",
layer: "example",
});
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "symbol",
source: "example",
},
]
);
],
});
});
});
describe("raster", () => {
it("add", () => {
var id = when.fillLayersModal({
let id = when.modal.fillLayers({
type: "raster",
layer: "raster",
});
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "raster",
source: "raster",
},
]
);
],
});
});
});
describe("circle", () => {
it("add", () => {
var id = when.fillLayersModal({
let id = when.modal.fillLayers({
type: "circle",
layer: "example",
});
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "circle",
source: "example",
},
]
);
],
});
});
});
describe("fill extrusion", () => {
it("add", () => {
var id = when.fillLayersModal({
let id = when.modal.fillLayers({
type: "fill-extrusion",
layer: "example",
});
should.equalStyleStore(
(a: any) => a.layers,
[
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "fill-extrusion",
source: "example",
},
]
);
],
});
});
});
@@ -458,34 +463,35 @@ describe("layers", () => {
it("simple", () => {
when.setStyle("geojson");
when.openLayersModal();
when.fillLayersModal({
when.modal.open();
when.modal.fillLayers({
id: "foo",
type: "background",
});
when.openLayersModal();
when.fillLayersModal({
when.modal.open();
when.modal.fillLayers({
id: "foo_bar",
type: "background",
});
when.openLayersModal();
when.fillLayersModal({
when.modal.open();
when.modal.fillLayers({
id: "foo_bar_baz",
type: "background",
});
should.beVisible("layer-list-item:foo");
should.notBeVisible("layer-list-item:foo_bar");
should.notBeVisible("layer-list-item:foo_bar_baz");
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");
should.beVisible("layer-list-item:foo");
should.beVisible("layer-list-item:foo_bar");
should.beVisible("layer-list-item:foo_bar_baz");
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();
});
});
});
+19 -10
View File
@@ -1,23 +1,32 @@
import MaputnikDriver from "./driver";
import { MaputnikDriver } from "./maputnik-driver";
describe("map", () => {
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("zoom level", () => {
it("via url", () => {
var zoomLevel = 12.37;
let zoomLevel = 12.37;
when.setStyle("geojson", zoomLevel);
should.beVisible("maplibre:ctrl-zoom");
should.containText("maplibre:ctrl-zoom", "Zoom: " + zoomLevel);
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
"Zoom: " + zoomLevel
);
});
it("via map controls", () => {
var zoomLevel = 12.37;
let zoomLevel = 12.37;
when.setStyle("geojson", zoomLevel);
should.beVisible("maplibre:ctrl-zoom");
when.clickZoomin();
should.containText("maplibre:ctrl-zoom", "Zoom: "+(zoomLevel + 1));
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();
});
});
});
+19
View File
@@ -0,0 +1,19 @@
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;
}
+186
View File
@@ -0,0 +1,186 @@
/// <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')
};
}
+40
View File
@@ -0,0 +1,40 @@
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");
},
};
}
+81 -62
View File
@@ -1,8 +1,9 @@
import MaputnikDriver from "./driver";
import { MaputnikDriver } from "./maputnik-driver";
describe("modals", () => {
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => {
when.setStyle("");
});
@@ -12,25 +13,27 @@ describe("modals", () => {
});
it("close", () => {
when.closeModal("modal:open");
should.notExist("modal:open");
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();
should.styleStoreEqualToExampleFileData();
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
});
it("load from url", () => {
var styleFileUrl = get.exampleFileUrl();
describe("when click open url", () => {
beforeEach(() => {
let styleFileUrl = get.exampleFileUrl();
when.setValue(get.dataAttribute("modal:open.url.input"), styleFileUrl);
when.click("modal:open.url.button");
when.waitForExampleFileRequset();
should.styleStoreEqualToExampleFileData();
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();
});
});
});
@@ -38,8 +41,8 @@ describe("modals", () => {
it("open/close", () => {
when.setStyle("");
when.typeKeys("?");
when.closeModal("modal:shortcuts");
should.notExist("modal:shortcuts");
when.modal.close("modal:shortcuts");
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
});
});
@@ -49,8 +52,8 @@ describe("modals", () => {
});
it("close", () => {
when.closeModal("modal:export");
should.notExist("modal:export");
when.modal.close("modal:export");
then(get.elementByTestId("modal:export")).shouldNotExist();
});
// TODO: Work out how to download a file and check the contents
@@ -65,9 +68,9 @@ describe("modals", () => {
describe("inspect", () => {
it("toggle", () => {
// There is no assertion in this test
when.setStyle("geojson");
when.selectWithin("nav:inspect", "inspect");
when.select("maputnik-select", "inspect");
});
});
@@ -76,82 +79,98 @@ describe("modals", () => {
when.click("nav:settings");
});
it("name", () => {
when.click("field-doc-button-Name");
describe("when click name filed spec information", () => {
beforeEach(() => {
when.click("field-doc-button-Name");
});
should.containText("spec-field-doc", "name for the style");
it("should show the spec information", () => {
then(get.elementsText("spec-field-doc")).shouldInclude(
"name for the style"
);
});
});
it("show name specifications", () => {
when.setValue(get.dataAttribute("modal:settings.name"), "foobar");
when.click("modal:settings.owner");
describe("when set name and click owner", () => {
beforeEach(() => {
when.setValue("modal:settings.name", "foobar");
when.click("modal:settings.owner");
when.wait(200);
});
should.equalStyleStore((obj) => obj.name, "foobar");
it("show name specifications", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
name: "foobar",
});
});
});
it("owner", () => {
when.setValue(get.dataAttribute("modal:settings.owner"), "foobar");
when.click("modal:settings.name");
should.equalStyleStore((obj) => obj.owner, "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(
get.dataAttribute("modal:settings.sprite"),
"http://example.com"
);
when.setValue("modal:settings.sprite", "http://example.com");
when.click("modal:settings.name");
should.equalStyleStore((obj) => obj.sprite, "http://example.com");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sprite: "http://example.com",
});
});
it("glyphs url", () => {
var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
when.setValue(get.dataAttribute("modal:settings.glyphs"), glyphsUrl);
let glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
when.setValue("modal:settings.glyphs", glyphsUrl);
when.click("modal:settings.name");
should.equalStyleStore((obj) => obj.glyphs, glyphsUrl);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
glyphs: glyphsUrl,
});
});
it("maptiler access token", () => {
var apiKey = "testing123";
let apiKey = "testing123";
when.setValue(
get.dataAttribute(
"modal:settings.maputnik:openmaptiles_access_token"
),
"modal:settings.maputnik:openmaptiles_access_token",
apiKey
);
when.click("modal:settings.name");
should.equalStyleStore(
(obj) => obj.metadata["maputnik:openmaptiles_access_token"],
apiKey
);
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({
"maputnik:openmaptiles_access_token": apiKey,
});
});
it("thunderforest access token", () => {
var apiKey = "testing123";
let apiKey = "testing123";
when.setValue(
get.dataAttribute(
"modal:settings.maputnik:thunderforest_access_token"
),
"modal:settings.maputnik:thunderforest_access_token",
apiKey
);
when.click("modal:settings.name");
should.equalStyleStore(
(obj) => obj.metadata["maputnik:thunderforest_access_token"],
apiKey
);
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");
should.beSelected("modal:settings.maputnik:renderer", "ol");
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
"ol"
);
when.click("modal:settings.name");
should.equalStyleStore((obj) => obj.metadata["maputnik:renderer"], "ol");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
metadata: { "maputnik:renderer": "ol" },
});
});
});
+1
View File
@@ -14,6 +14,7 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "@cypress/code-coverage/support";
import "cypress-plugin-tab";
import "./commands";
+31
View File
@@ -0,0 +1,31 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
editor
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Binary version of pubilic/editor
rice-box.go
# Built binary
maputnik
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Maputnik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+39
View File
@@ -0,0 +1,39 @@
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
BINARY=maputnik
DESKTOP_VERSION := 1.1.1
EDITOR_VERSION := $(shell node -p "require('../package.json').version")
GOPATH := $(if $(GOPATH),$(GOPATH),$(HOME)/go)
GOBIN := $(if $(GOBIN),$(GOBIN),$(HOME)/go/bin)
all: $(BINARY)
$(BINARY): $(GOBIN)/gox $(SOURCES) version.go rice-box.go
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
# Copy the current release into ./editor/maputnik so it can be
# embedded in the binary
editor/pull_release:
mkdir -p editor
cp -r ../dist/* editor
$(GOBIN)/gox:
go install github.com/mitchellh/gox@v1.0.1
$(GOBIN)/rice:
go install github.com/GeertJohan/go.rice/rice@v1.0.3
# Embed the current version numbers in the executable by writing version.go
.PHONY: version.go
version.go:
@echo "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
@echo "package main\n" >> version.go
@echo "const DesktopVersion = \"$(DESKTOP_VERSION)\"" >> version.go
@echo "const EditorVersion = \"$(EDITOR_VERSION)\"" >> version.go
rice-box.go: $(GOBIN)/rice editor/pull_release
$(GOBIN)/rice embed-go
.PHONY: clean
clean:
rm -rf editor && rm -f rice-box.go && rm -rf bin
+72
View File
@@ -0,0 +1,72 @@
# Maputnik Desktop [![GitHub CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci]
---
A Golang based cross platform executable for integrating Maputnik locally.
This binary packages up the JavaScript and CSS bundle produced by maputnik
and embeds it in the program for easy distribution. It also allows
exposing a local style file and work on it both in Maputnik and with your favorite
editor.
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
## Install
You can download a single binary for Linux, OSX or Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/editor/releases/latest).
### Usage
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
```bash
maputnik
```
Expose a local style file to Maputnik allowing the web based editor
to save to the local filesystem.
```bash
maputnik --file basic-v9.json
```
Watch the local style for changes and inform the editor via web socket.
This makes it possible to edit the style with a local text editor and still
use Maputnik.
```bash
maputnik --watch --file basic-v9.json
```
Choose a local port to listen on, instead of using the default port 8000.
```bash
maputnik --port 8001
```
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
Could be used to serve sprites and glyphs.
```bash
maputnik --static ./localFolder
```
### API
`maputnik` exposes the configured styles via a HTTP API.
| Method | Description
|---------------------------------|---------------------------------------
| `GET /styles` | List the ID of all configured style files
| `GET /styles/{filename}` | Get contents of a single style file
| `PUT /styles/{filename}` | Update contents of a style file
| `WEBSOCKET /ws` | Listen to change events for the configured style files
### Build
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
```
npm install
npm run build-desktop
```
You should now find the `maputnik` binary in your `bin` directory.
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/gorilla/mux"
)
func StyleFileAccessor(filename string) styleFileAccessor {
return styleFileAccessor{filename, styleId(filename)}
}
func styleId(filename string) string {
raw, err := ioutil.ReadFile(filename)
if err != nil {
log.Panicln(err)
}
var spec styleSpec
err = json.Unmarshal(raw, &spec)
if err != nil {
log.Panicln(err)
}
if spec.Id == "" {
fmt.Println("No id in style")
}
return spec.Id
}
type styleSpec struct {
Id string `json:"id"`
}
// Allows access to a single style file
type styleFileAccessor struct {
filename string
id string
}
func (fa styleFileAccessor) ListFiles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.Encode([]string{fa.id})
}
func (fa styleFileAccessor) ReadFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Choose right file
// right now we just return the single file we know of
w.Header().Set("Content-Type", "application/json")
raw, err := ioutil.ReadFile(fa.filename)
if err != nil {
log.Panicln(err)
}
w.Write(raw)
}
func (fa styleFileAccessor) SaveFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Save to right file
w.Header().Set("Content-Type", "application/json")
body, _ := ioutil.ReadAll(r.Body)
var out bytes.Buffer
json.Indent(&out, body, "", " ")
if err := ioutil.WriteFile(fa.filename, out.Bytes(), 0666); err != nil {
log.Fatalf("Can not copy from request to file: %s", err.Error())
}
}
+69
View File
@@ -0,0 +1,69 @@
package filewatch
import (
"io/ioutil"
"log"
"net/http"
"github.com/fsnotify/fsnotify"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func writer(ws *websocket.Conn, filename string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("Modified file:", event.Name)
var p []byte
var err error
p, err = ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
if p != nil {
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
return
}
}
}
case err := <-watcher.Errors:
log.Println("Watch error:", err)
}
}
}()
if err = watcher.Add(filename); err != nil {
log.Fatal(err)
}
<-done
}
func ServeWebsocketFileWatcher(filename string, w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Println(err)
}
return
}
writer(ws, filename)
defer ws.Close()
}
+27
View File
@@ -0,0 +1,27 @@
module maputnik/desktop
go 1.19
require (
github.com/GeertJohan/go.rice v1.0.3
github.com/fsnotify/fsnotify v1.6.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/maputnik/desktop v1.0.7
github.com/urfave/cli v1.22.12
)
require (
github.com/GeertJohan/go.incremental v1.0.0 // indirect
github.com/akavel/rsrc v0.8.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/daaku/go.zipexe v1.0.2 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/nkovacs/streamquote v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.0.1 // indirect
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
)
+54
View File
@@ -0,0 +1,54 @@
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4=
github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ=
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+80
View File
@@ -0,0 +1,80 @@
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/GeertJohan/go.rice"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/maputnik/desktop/filewatch"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "maputnik"
app.Usage = "Server for integrating Maputnik locally"
app.Version = "Editor: " + EditorVersion + "; Desktop: " + DesktopVersion
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "file, f",
Usage: "Allow access to JSON style from web client",
},
&cli.BoolFlag{
Name: "watch",
Usage: "Notify web client about JSON style file changes",
},
&cli.IntFlag{
Name: "port",
Value: 8000,
Usage: "TCP port to listen on",
},
&cli.StringFlag{
Name: "static",
Usage: "Serve directory under /static/",
},
}
app.Action = func(c *cli.Context) error {
gui := http.FileServer(rice.MustFindBox("editor").HTTPBox())
router := mux.NewRouter().StrictSlash(true)
filename := c.String("file")
if filename != "" {
fmt.Printf("%s is accessible via Maputnik\n", filename)
// Allow access to reading and writing file on the local system
path, _ := filepath.Abs(filename)
accessor := StyleFileAccessor(path)
router.Path("/styles").Methods("GET").HandlerFunc(accessor.ListFiles)
router.Path("/styles/{styleId}").Methods("GET").HandlerFunc(accessor.ReadFile)
router.Path("/styles/{styleId}").Methods("PUT").HandlerFunc(accessor.SaveFile)
// Register websocket to notify we clients about file changes
if c.Bool("watch") {
router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
filewatch.ServeWebsocketFileWatcher(filename, w, r)
})
}
}
staticDir := c.String("static")
if staticDir != "" {
h := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
router.PathPrefix("/static/").Handler(h)
}
router.PathPrefix("/").Handler(http.StripPrefix("/", gui))
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter)
fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port"))
return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter)
}
app.Run(os.Args)
}
+17
View File
@@ -0,0 +1,17 @@
export default {
output: 'src/locales/$LOCALE/$NAMESPACE.json',
locales: [ 'ja', 'he','zh' ],
// Because some keys are dynamically generated, i18next-parser can't detect them.
// We add these keys manually, so we don't want to remove them.
keepRemoved: true,
// We use plain English keys, so we disable key and namespace separators.
keySeparator: false,
namespaceSeparator: false,
defaultValue: (locale, ns, key) => {
// The default value is a string that indicates that the string is not translated.
return '__STRING_NOT_TRANSLATED__';
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+4068 -8130
View File
File diff suppressed because it is too large Load Diff
+68 -55
View File
@@ -1,38 +1,49 @@
{
"name": "maputnik",
"version": "2.0.0-pre.2",
"version": "2.1.1",
"description": "A MapLibre GL visual style editor",
"type": "module",
"main": "''",
"scripts": {
"start": "vite",
"build": "tsc && vite build",
"lint": "eslint ./src --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0 && npm run lint-css",
"build": "tsc && vite build --base=/maputnik/",
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "cypress run",
"cy:open": "cypress open",
"lint-css": "stylelint \"src/styles/*.scss\"",
"storybook": "storybook dev -h 0.0.0.0 -p 6006",
"build-storybook": "storybook build -o build/storybook"
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
},
"repository": {
"type": "git",
"url": "https://github.com/maputnik/editor"
"url": "https://github.com/maplibre/maputnik"
},
"author": "Lukas Martinelli",
"license": "MIT",
"homepage": "https://github.com/maputnik/editor#readme",
"homepage": "https://github.com/maplibre/maputnik#readme",
"dependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
"@mdi/js": "^6.6.96",
"@mdi/react": "^1.5.0",
"@maplibre/maplibre-gl-geocoder": "^1.6.0",
"@maplibre/maplibre-gl-inspect": "^1.6.3",
"@maplibre/maplibre-gl-style-spec": "^20.1.1",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"array-move": "^4.0.0",
"buffer": "^6.0.3",
"classnames": "^2.3.1",
"classnames": "^2.5.1",
"codemirror": "^5.65.2",
"color": "^4.2.3",
"cypress-plugin-tab": "^1.0.5",
"detect-browser": "^5.3.0",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"json-stringify-pretty-compact": "^3.0.0",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
"json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.21",
@@ -42,30 +53,30 @@
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"mapbox-gl-inspect": "^1.3.1",
"maplibre-gl": "^2.4.0",
"maplibre-gl": "^4.1.2",
"maputnik-design": "github:maputnik/design#172b06c",
"ol": "^6.14.1",
"ol-mapbox-style": "^7.1.1",
"prop-types": "^15.8.1",
"react": "^16.0.0",
"react-accessible-accordion": "^4.0.0",
"react": "^18.2.0",
"react-accessible-accordion": "^5.0.0",
"react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^4.0.1",
"react-aria-modal": "^5.0.2",
"react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^16.0.0",
"react-dom": "^18.2.0",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.0.1",
"react-icon-base": "^2.1.2",
"react-icons": "^4.3.1",
"react-icons": "^5.0.1",
"react-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0",
"sass": "^1.50.0",
"slugify": "^1.6.5",
"sass": "^1.72.0",
"slugify": "^1.6.6",
"string-hash": "^1.1.3",
"url": "^0.11.0"
"url": "^0.11.3"
},
"jshintConfig": {
"esversion": 6
@@ -85,52 +96,54 @@
}
},
"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.0.9",
"@storybook/addon-a11y": "^7.6.5",
"@storybook/addon-actions": "^7.6.5",
"@storybook/addon-links": "^7.6.5",
"@storybook/addon-storysource": "^7.6.5",
"@storybook/addons": "^7.6.5",
"@storybook/builder-vite": "^7.6.5",
"@storybook/react": "^7.6.5",
"@storybook/react-vite": "^7.6.5",
"@storybook/theming": "^7.6.5",
"@shellygo/cypress-test-utils": "^2.1.9",
"@types/codemirror": "^5.60.15",
"@types/color": "^3.0.6",
"@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7",
"@types/geojson": "^7946.0.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/react": "^16.14.52",
"@types/react-aria-modal": "^4.0.9",
"@types/react-autocomplete": "^1.8.9",
"@types/mocha": "^10.0.6",
"@types/randomcolor": "^0.5.9",
"@types/react": "^18.2.67",
"@types/react-aria-menubutton": "^6.2.14",
"@types/react-aria-modal": "^4.0.10",
"@types/react-autocomplete": "^1.8.10",
"@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.10",
"@types/react-dom": "^16.9.24",
"@types/react-color": "^3.0.12",
"@types/react-dom": "^18.2.22",
"@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6",
"@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.0",
"@types/string-hash": "^1.1.3",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react": "^4.2.1",
"cors": "^2.8.5",
"cypress": "^13.6.1",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"cypress": "^13.13.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"express": "^4.17.3",
"eslint-plugin-react-refresh": "^0.4.6",
"i18next-parser": "^9.0.1",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.0",
"mocha": "^9.2.2",
"postcss": "^8.4.12",
"react-hot-loader": "^4.13.0",
"storybook": "^7.6.5",
"stylelint": "^14.6.1",
"stylelint-config-recommended-scss": "^6.0.0",
"stylelint-scss": "^4.2.0",
"typescript": "^5.3.3",
"uuid": "^8.3.2",
"vite": "^5.0.0"
"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"
}
}
+163 -114
View File
@@ -1,3 +1,4 @@
// @ts-ignore - this can be easily replaced with arrow functions
import autoBind from 'react-autobind';
import React from 'react'
import cloneDeep from 'lodash.clonedeep'
@@ -6,14 +7,15 @@ import buffer from 'buffer'
import get from 'lodash.get'
import {unset} from 'lodash'
import {arrayMoveMutable} from 'array-move'
import url from 'url'
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 from './AppToolbar'
import AppToolbar, { MapState } from './AppToolbar'
import AppLayout from './AppLayout'
import MessagePanel from './AppMessagePanel'
@@ -22,11 +24,9 @@ import ModalExport from './ModalExport'
import ModalSources from './ModalSources'
import ModalOpen from './ModalOpen'
import ModalShortcuts from './ModalShortcuts'
import ModalSurvey from './ModalSurvey'
import ModalDebug from './ModalDebug'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
import style from '../libs/style'
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage'
@@ -37,12 +37,13 @@ import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal'
import Debug from '../libs/debug'
import {formatLayerId} from '../util/format';
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, mapStyle) {
function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
const matchesTilehosting = url.match(/\.tilehosting\.com/);
const matchesMaptiler = url.match(/\.maptiler\.com/);
const matchesThunderforest = url.match(/\.thunderforest\.com/);
@@ -63,7 +64,7 @@ function setFetchAccessToken(url, mapStyle) {
}
}
function updateRootSpec(spec, fieldName, newValues) {
function updateRootSpec(spec: any, fieldName: string, newValues: any) {
return {
...spec,
$root: {
@@ -76,15 +77,73 @@ function updateRootSpec(spec, fieldName, newValues) {
}
}
export default class App extends React.Component {
constructor(props) {
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)) {
if (port == null && (window.location.port !== "80" && window.location.port !== "443")) {
port = window.location.port
}
this.styleStore = new ApiStyleStore({
@@ -136,7 +195,7 @@ export default class App extends React.Component {
{
key: "m",
handler: () => {
document.querySelector(".maplibregl-canvas").focus();
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
}
},
{
@@ -149,7 +208,7 @@ export default class App extends React.Component {
document.body.addEventListener("keyup", (e) => {
if(e.key === "Escape") {
e.target.blur();
(e.target as HTMLElement).blur();
document.body.focus();
}
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
@@ -159,7 +218,7 @@ export default class App extends React.Component {
if(shortcut) {
this.setModal("shortcuts", false);
shortcut.handler(e);
shortcut.handler();
}
}
})
@@ -192,8 +251,6 @@ export default class App extends React.Component {
Debug.set("maputnik", "styleStore", this.styleStore);
}
const queryObj = url.parse(window.location.href, true).query;
this.state = {
errors: [],
infos: [],
@@ -217,7 +274,6 @@ export default class App extends React.Component {
shortcuts: false,
export: false,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
survey: false,
debug: false,
},
maplibreGlDebugOptions: {
@@ -235,25 +291,25 @@ export default class App extends React.Component {
})
}
handleKeyPress = (e) => {
handleKeyPress = (e: KeyboardEvent) => {
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
e.preventDefault();
this.onRedo(e);
this.onRedo();
}
else if(e.metaKey && e.keyCode === 90) {
e.preventDefault();
this.onUndo(e);
this.onUndo();
}
}
else {
if(e.ctrlKey && e.keyCode === 90) {
e.preventDefault();
this.onUndo(e);
this.onUndo();
}
else if(e.ctrlKey && e.keyCode === 89) {
e.preventDefault();
this.onRedo(e);
this.onRedo();
}
}
}
@@ -266,27 +322,27 @@ export default class App extends React.Component {
window.removeEventListener("keydown", this.handleKeyPress);
}
saveStyle(snapshotStyle) {
saveStyle(snapshotStyle: StyleSpecification & {id: string}) {
this.styleStore.save(snapshotStyle)
}
updateFonts(urlTemplate) {
const metadata = this.state.mapStyle.metadata || {}
updateFonts(urlTemplate: string) {
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
const glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
downloadGlyphsMetadata(glyphUrl, fonts => {
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
})
}
updateIcons(baseUrl) {
updateIcons(baseUrl: string) {
downloadSpriteMetadata(baseUrl, icons => {
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
})
}
onChangeMetadataProperty = (property, value) => {
onChangeMetadataProperty = (property: string, value: any) => {
// If we're changing renderer reset the map state.
if (
property === 'maputnik:renderer' &&
@@ -300,14 +356,14 @@ export default class App extends React.Component {
const changedStyle = {
...this.state.mapStyle,
metadata: {
...this.state.mapStyle.metadata,
...(this.state.mapStyle as any).metadata,
[property]: value
}
}
this.onStyleChanged(changedStyle)
}
onStyleChanged = (newStyle, opts={}) => {
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => {
opts = {
save: true,
addRevision: true,
@@ -319,16 +375,15 @@ export default class App extends React.Component {
this.getInitialStateFromUrl(newStyle);
}
const errors = validate(newStyle, latest) || [];
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
// The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here.
const layerErrors = [];
const layerErrors: (Error | ValidationError)[] = [];
if (newStyle && newStyle.layers) {
const foundLayers = new Map();
const foundLayers = new global.Map();
newStyle.layers.forEach((layer, index) => {
if (layer.id === "" && foundLayers.has(layer.id)) {
const message = `Duplicate layer: ${formatLayerId(layer.id)}`;
const error = new Error(
`layers[${index}]: duplicate layer id [empty_string], previously used`
);
@@ -342,7 +397,7 @@ export default class App extends React.Component {
// Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) {
const [matchStr, index, message] = dupMatch;
const [, index, message] = dupMatch;
return {
message: error.message,
parsed: {
@@ -359,7 +414,7 @@ export default class App extends React.Component {
// Special case: Invalid source
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
if (invalidSourceMatch) {
const [matchStr, index, message] = invalidSourceMatch;
const [, index, message] = invalidSourceMatch;
return {
message: error.message,
parsed: {
@@ -375,7 +430,7 @@ export default class App extends React.Component {
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) {
const [matchStr, index, group, property, message] = layerMatch;
const [, index, group, property, message] = layerMatch;
const key = (group && property) ? [group, property].join(".") : property;
return {
message: error.message,
@@ -396,7 +451,7 @@ export default class App extends React.Component {
}
});
let dirtyMapStyle = undefined;
let dirtyMapStyle: StyleSpecification | undefined = undefined;
if (errors.length > 0) {
dirtyMapStyle = cloneDeep(newStyle);
@@ -406,7 +461,7 @@ export default class App extends React.Component {
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];
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^[]+/)![0];
unset(dirtyMapStyle, unsetPath);
}
catch (err) {
@@ -417,17 +472,17 @@ export default class App extends React.Component {
}
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
this.updateFonts(newStyle.glyphs as string)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite)
this.updateIcons(newStyle.sprite as string)
}
if (opts.addRevision) {
this.revisionStore.addRevision(newStyle);
}
if (opts.save) {
this.saveStyle(newStyle);
this.saveStyle(newStyle as StyleSpecification & {id: string});
}
this.setState({
@@ -460,7 +515,7 @@ export default class App extends React.Component {
})
}
onMoveLayer = (move) => {
onMoveLayer = (move: SortEnd) => {
let { oldIndex, newIndex } = move;
let layers = this.state.mapStyle.layers;
oldIndex = clamp(oldIndex, 0, layers.length-1);
@@ -478,7 +533,7 @@ export default class App extends React.Component {
this.onLayersChange(layers);
}
onLayersChange = (changedLayers) => {
onLayersChange = (changedLayers: LayerSpecification[]) => {
const changedStyle = {
...this.state.mapStyle,
layers: changedLayers
@@ -486,15 +541,15 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle)
}
onLayerDestroy = (index) => {
let layers = this.state.mapStyle.layers;
onLayerDestroy = (index: number) => {
const layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0);
remainingLayers.splice(index, 1);
this.onLayersChange(remainingLayers);
}
onLayerCopy = (index) => {
let layers = this.state.mapStyle.layers;
onLayerCopy = (index: number) => {
const layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const clonedLayer = cloneDeep(changedLayers[index])
@@ -503,8 +558,8 @@ export default class App extends React.Component {
this.onLayersChange(changedLayers)
}
onLayerVisibilityToggle = (index) => {
let layers = this.state.mapStyle.layers;
onLayerVisibilityToggle = (index: number) => {
const layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const layer = { ...changedLayers[index] }
@@ -517,7 +572,7 @@ export default class App extends React.Component {
}
onLayerIdChange = (index, oldId, newId) => {
onLayerIdChange = (index: number, _oldId: string, newId: string) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = {
...changedLayers[index],
@@ -527,26 +582,26 @@ export default class App extends React.Component {
this.onLayersChange(changedLayers)
}
onLayerChanged = (index, layer) => {
onLayerChanged = (index: number, layer: LayerSpecification) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = layer
this.onLayersChange(changedLayers)
}
setMapState = (newState) => {
setMapState = (newState: MapState) => {
this.setState({
mapState: newState
}, this.setStateInUrl);
}
setDefaultValues = (styleObj) => {
const metadata = styleObj.metadata || {}
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,
...styleObj.metadata as any,
'maputnik:renderer': 'mlgljs'
}
}
@@ -556,19 +611,19 @@ export default class App extends React.Component {
}
}
openStyle = (styleObj) => {
openStyle = (styleObj: StyleSpecification & {id: string}) => {
styleObj = this.setDefaultValues(styleObj)
this.onStyleChanged(styleObj)
}
fetchSources() {
const sourceList = {};
const sourceList: {[key: string]: any} = {};
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) {
if(
!this.state.sources.hasOwnProperty(key) &&
!Object.prototype.hasOwnProperty.call(this.state.sources, key) &&
val.type === "vector" &&
val.hasOwnProperty("url")
Object.prototype.hasOwnProperty.call(val, "url")
) {
sourceList[key] = {
type: val.type,
@@ -578,38 +633,38 @@ export default class App extends React.Component {
let url = val.url;
try {
url = setFetchAccessToken(url, this.state.mapStyle)
url = setFetchAccessToken(url!, this.state.mapStyle)
} catch(err) {
console.warn("Failed to setFetchAccessToken: ", err);
}
fetch(url, {
fetch(url!, {
mode: 'cors',
})
.then(response => response.json())
.then(json => {
.then(response => response.json())
.then(json => {
if(!json.hasOwnProperty("vector_layers")) {
return;
}
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
return;
}
// Create new objects before setState
const sources = Object.assign({}, {
[key]: this.state.sources[key],
// 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);
});
for(let layer of json.vector_layers) {
sources[key].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];
@@ -625,11 +680,17 @@ export default class App extends React.Component {
}
_getRenderer () {
const metadata = this.state.mapStyle.metadata || {};
const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
return metadata['maputnik:renderer'] || 'mlgljs';
}
onMapChange = (mapView) => {
onMapChange = (mapView: {
zoom: number,
center: {
lng: number,
lat: number,
},
}) => {
this.setState({
mapView,
});
@@ -637,16 +698,15 @@ export default class App extends React.Component {
mapRenderer() {
const {mapStyle, dirtyMapStyle} = this.state;
const metadata = this.state.mapStyle.metadata || {};
const mapProps = {
mapStyle: (dirtyMapStyle || mapStyle),
replaceAccessTokens: (mapStyle) => {
replaceAccessTokens: (mapStyle: StyleSpecification) => {
return style.replaceAccessTokens(mapStyle, {
allowFallback: true
});
},
onDataChange: (e) => {
onDataChange: (e: {map: Map}) => {
this.layerWatcher.analyzeMap(e.map)
this.fetchSources();
},
@@ -677,7 +737,7 @@ export default class App extends React.Component {
if(this.state.mapState.match(/^filter-/)) {
filterName = this.state.mapState.replace(/^filter-/, "");
}
const elementStyle = {};
const elementStyle: {filter?: string} = {};
if (filterName) {
elementStyle.filter = `url('#${filterName}')`;
}
@@ -695,8 +755,8 @@ export default class App extends React.Component {
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
const openModals = Object.entries(isOpen)
.map(([key, val]) => (val === true ? key : null))
.filter(val => val !== null);
.map(([key, val]) => (val === true ? key : null))
.filter(val => val !== null);
if (openModals.length > 0) {
url.searchParams.set("modal", openModals.join(","));
@@ -715,12 +775,12 @@ export default class App extends React.Component {
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
}
getInitialStateFromUrl = (mapStyle) => {
getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
const url = new URL(location.href);
const modalParam = url.searchParams.get("modal");
if (modalParam && modalParam !== "") {
const modals = modalParam.split(",");
const modalObj = {};
const modalObj: {[key: string]: boolean} = {};
modals.forEach(modalName => {
modalObj[modalName] = true;
});
@@ -735,7 +795,7 @@ export default class App extends React.Component {
const view = url.searchParams.get("view");
if (view && view !== "") {
this.setMapState(view);
this.setMapState(view as MapState);
}
const path = url.searchParams.get("layer");
@@ -767,18 +827,14 @@ export default class App extends React.Component {
}
}
onLayerSelect = (index) => {
onLayerSelect = (index: number) => {
this.setState({
selectedLayerIndex: index,
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
}, this.setStateInUrl);
}
setModal(modalName, value) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
this.setState({
isOpen: {
...this.state.isOpen,
@@ -787,11 +843,11 @@ export default class App extends React.Component {
}, this.setStateInUrl)
}
toggleModal(modalName) {
toggleModal(modalName: keyof AppState["isOpen"]) {
this.setModal(modalName, !this.state.isOpen[modalName]);
}
onChangeOpenlayersDebug = (key, value) => {
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
this.setState({
openlayersDebugOptions: {
...this.state.openlayersDebugOptions,
@@ -800,7 +856,7 @@ export default class App extends React.Component {
});
}
onChangeMaplibreGlDebug = (key, value) => {
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
this.setState({
maplibreGlDebugOptions: {
...this.state.maplibreGlDebugOptions,
@@ -811,8 +867,7 @@ export default class App extends React.Component {
render() {
const layers = this.state.mapStyle.layers || []
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
const metadata = this.state.mapStyle.metadata || {}
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined
const toolbar = <AppToolbar
renderer={this._getRenderer()}
@@ -855,7 +910,7 @@ export default class App extends React.Component {
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors}
/> : null
/> : undefined
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
currentLayer={selectedLayer}
@@ -864,7 +919,7 @@ export default class App extends React.Component {
mapStyle={this.state.mapStyle}
errors={this.state.errors}
infos={this.state.infos}
/> : null
/> : undefined
const modals = <div>
@@ -879,7 +934,6 @@ export default class App extends React.Component {
mapView={this.state.mapView}
/>
<ModalShortcuts
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>
@@ -889,7 +943,6 @@ export default class App extends React.Component {
onChangeMetadataProperty={this.onChangeMetadataProperty}
isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')}
openlayersDebugOptions={this.state.openlayersDebugOptions}
/>
<ModalExport
mapStyle={this.state.mapStyle}
@@ -908,10 +961,6 @@ export default class App extends React.Component {
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<ModalSurvey
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div>
return <AppLayout
+21 -15
View File
@@ -1,17 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
import { WithTranslation, withTranslation } from 'react-i18next';
type AppLayoutProps = {
type AppLayoutInternalProps = {
toolbar: React.ReactElement
layerList: React.ReactElement
layerEditor?: React.ReactElement
map: React.ReactElement
bottom?: React.ReactElement
modals?: React.ReactNode
};
} & WithTranslation;
class AppLayout extends React.Component<AppLayoutProps> {
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
static childContextTypes = {
reactIconBase: PropTypes.object
}
@@ -23,24 +24,29 @@ class AppLayout extends React.Component<AppLayoutProps> {
}
render() {
document.body.dir = this.props.i18n.dir();
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
<div className="maputnik-layout-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>
}
}
export default AppLayout
const AppLayout = withTranslation()(AppLayoutInternal);
export default AppLayout;
+15 -10
View File
@@ -1,23 +1,24 @@
import React from 'react'
import {formatLayerId} from '../util/format';
import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec';
import {formatLayerId} from '../libs/format';
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
type AppMessagePanelProps = {
type AppMessagePanelInternalProps = {
errors?: unknown[]
infos?: unknown[]
infos?: string[]
mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown
currentLayer?: object
currentLayer?: LayerSpecification
selectedLayerIndex?: number
};
} & WithTranslation;
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const {selectedLayerIndex} = this.props;
const {t, selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
@@ -25,7 +26,9 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
content = (
<>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
<Trans t={t}>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
</Trans>
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
@@ -33,7 +36,7 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
>
switch to layer
{t("switch to layer")}
</button>
</>
}
@@ -59,3 +62,5 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
}
}
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
export default AppMessagePanel;
+62 -54
View File
@@ -2,9 +2,12 @@ import React from 'react'
import classnames from 'classnames'
import {detect} from 'detect-browser';
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdLanguage} from 'react-icons/md'
import pkgJson from '../../package.json'
//@ts-ignore
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
import { withTranslation, WithTranslation } from 'react-i18next';
import { supportedLanguages } from '../i18n';
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
const browser = detect();
@@ -36,34 +39,13 @@ class ToolbarLink extends React.Component<ToolbarLinkProps> {
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
data-wd-key="toolbar:link"
>
{this.props.children}
</a>
}
}
type ToolbarLinkHighlightedProps = {
className?: string
children?: React.ReactNode
href?: string
onToggleModal?(...args: unknown[]): unknown
};
class ToolbarLinkHighlighted extends React.Component<ToolbarLinkHighlightedProps> {
render() {
return <a
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
>
<span className="maputnik-toolbar-link-wrapper">
{this.props.children}
</span>
</a>
}
}
type ToolbarSelectProps = {
children?: React.ReactNode
wdKey?: string
@@ -98,7 +80,9 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
}
}
type AppToolbarProps = {
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
type AppToolbarInternalProps = {
mapStyle: object
inspectModeEnabled: boolean
onStyleChanged(...args: unknown[]): unknown
@@ -108,12 +92,12 @@ type AppToolbarProps = {
sources: object
children?: React.ReactNode
onToggleModal(...args: unknown[]): unknown
onSetMapState(...args: unknown[]): unknown
mapState?: string
onSetMapState(mapState: MapState): unknown
mapState?: MapState
renderer?: string
};
} & WithTranslation;
export default class AppToolbar extends React.Component<AppToolbarProps> {
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
state = {
isOpen: {
settings: false,
@@ -124,10 +108,14 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
}
}
handleSelection(val: string | undefined) {
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();
@@ -139,40 +127,41 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
}
render() {
const t = this.props.t;
const views = [
{
id: "map",
group: "general",
title: "Map",
title: t("Map"),
},
{
id: "inspect",
group: "general",
title: "Inspect",
title: t("Inspect"),
disabled: this.props.renderer === 'ol',
},
{
id: "filter-deuteranopia",
group: "color-accessibility",
title: "Deuteranopia filter",
title: t("Deuteranopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-protanopia",
group: "color-accessibility",
title: "Protanopia filter",
title: t("Protanopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-tritanopia",
group: "color-accessibility",
title: "Tritanopia filter",
title: t("Tritanopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-achromatopsia",
group: "color-accessibility",
title: "Achromatopsia filter",
title: t("Achromatopsia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
];
@@ -192,29 +181,29 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-list")}
>
Layers list
{t("Layers list")}
</button>
<button
data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-editor")}
>
Layer editor
{t("Layer editor")}
</button>
<button
data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("map")}
>
Map view
{t("Map view")}
</button>
<a
className="maputnik-toolbar-logo"
target="blank"
rel="noreferrer noopener"
href="https://github.com/maputnik/editor"
href="https://github.com/maplibre/maputnik"
>
<img src="node_modules/maputnik-design/logos/logo-color.svg" />
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
<h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
@@ -224,28 +213,28 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser />
<IconText>Open</IconText>
<IconText>{t("Open")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>Export</IconText>
<IconText>{t("Export")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers />
<IconText>Data Sources</IconText>
<IconText>{t("Data Sources")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings />
<IconText>Style Settings</IconText>
<IconText>{t("Style Settings")}</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect">
<MdFindInPage />
<label>View
<label>{t("View")}
<select
className="maputnik-select"
data-wd-key="maputnik-select"
onChange={(e) => this.handleSelection(e.target.value)}
onChange={(e) => this.handleSelection(e.target.value as MapState)}
value={currentView?.id}
>
{views.filter(v => v.group === "general").map((item) => {
@@ -255,7 +244,7 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
</option>
);
})}
<optgroup label="Color accessibility">
<optgroup label={t("Color accessibility")}>
{views.filter(v => v.group === "color-accessibility").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled}>
@@ -268,16 +257,35 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<ToolbarSelect wdKey="nav:language">
<MdLanguage />
<label>{t("Language")}
<select
className="maputnik-select"
data-wd-key="maputnik-lang-select"
onChange={(e) => this.handleLanguageChange(e.target.value)}
value={this.props.i18n.language}
>
{Object.entries(supportedLanguages).map(([code, name]) => {
return (
<option key={code} value={code}>
{name}
</option>
);
})}
</select>
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
<MdHelpOutline />
<IconText>Help</IconText>
<IconText>{t("Help")}</IconText>
</ToolbarLink>
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
<MdAssignmentTurnedIn />
<IconText>Take the Maputnik Survey</IconText>
</ToolbarLinkHighlighted>
</div>
</div>
</nav>
}
}
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;
+4 -4
View File
@@ -1,10 +1,10 @@
import React, {SyntheticEvent} from 'react'
import React, {PropsWithChildren, SyntheticEvent} from 'react'
import classnames from 'classnames'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
type BlockProps = {
type BlockProps = PropsWithChildren & {
"data-wd-key"?: string
label?: string
action?: React.ReactElement
@@ -12,7 +12,7 @@ type BlockProps = {
onChange?(...args: unknown[]): unknown
fieldSpec?: object
wideMode?: boolean
error?: unknown[]
error?: {message: string}
};
type BlockState = {
@@ -56,7 +56,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
event.stopPropagation();
}
event.preventDefault();
event.preventDefault();
}
render() {
+2 -2
View File
@@ -31,7 +31,7 @@ export default class Doc extends React.Component<DocProps> {
const renderValues = (
!!values &&
// HACK: Currently we merge additional values into the style spec, so this is required
// See <https://github.com/maputnik/editor/blob/main/src/components/PropertyGroup.jsx#L16>
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16>
!Array.isArray(values)
);
@@ -71,7 +71,7 @@ export default class Doc extends React.Component<DocProps> {
<tr key={key}>
<td>{key}</td>
{Object.keys(headers).map((k) => {
if (supportObj.hasOwnProperty(k)) {
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
}
else {
+15 -7
View File
@@ -2,29 +2,37 @@ import React from 'react'
import Block from './Block'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldCommentProps = {
type FieldCommentInternalProps = {
value?: string
onChange(...args: unknown[]): unknown
};
onChange(value: string | undefined): unknown
error: {message: string}
} & WithTranslation;
export default class FieldComment extends React.Component<FieldCommentProps> {
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
render() {
const t = this.props.t;
const fieldSpec = {
doc: "Comments for the current layer. This is non-standard and not in the spec."
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
};
return <Block
label={"Comments"}
label={t("Comments")}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
error={this.props.error}
>
<InputString
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default="Comment..."
default={t("Comment...")}
data-wd-key="layer-comment.input"
/>
</Block>
}
}
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
type FieldDocLabelProps = {
label: object | string | undefined
label: JSX.Element | string | undefined
fieldSpec?: {
doc?: string
}
+5 -5
View File
@@ -5,7 +5,7 @@ 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 '../util/spec-helper';
import {findDefaultFromSpec} from '../libs/spec-helper';
function isLiteralExpression(value: any) {
@@ -40,7 +40,7 @@ function isIdentityProperty(value: any) {
return (
typeof(value) === 'object' &&
value.type === "identity" &&
value.hasOwnProperty("property")
Object.prototype.hasOwnProperty.call(value, "property")
);
}
@@ -107,7 +107,7 @@ type FieldFunctionProps = {
fieldName: string
fieldType: string
fieldSpec: any
errors?: unknown[]
errors?: {[key: string]: {message: string}}
value?: any
};
@@ -128,7 +128,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
}
}
static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
// Because otherwise when editing values we end up accidentally changing field type.
if (state.isEditing) {
return {};
@@ -399,7 +399,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
/>
)
}
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}>
{specField}
</div>
}
+6 -4
View File
@@ -1,25 +1,27 @@
import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputString from './InputString'
type FieldIdProps = {
value: string
wdKey: string
onChange(...args: unknown[]): unknown
error?: unknown[]
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}
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>
}
+13 -7
View File
@@ -1,18 +1,20 @@
import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMaxZoomProps = {
type FieldMaxZoomInternalProps = {
value?: number
onChange(...args: unknown[]): unknown
error?: unknown[]
};
onChange(value: number | undefined): unknown
error?: {message: string}
} & WithTranslation;
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
render() {
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
const t = this.props.t;
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
@@ -23,7 +25,11 @@ export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
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;
+12 -6
View File
@@ -1,18 +1,20 @@
import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMinZoomProps = {
type FieldMinZoomInternalProps = {
value?: number
onChange(...args: unknown[]): unknown
error?: unknown[]
};
error?: {message: string}
} & WithTranslation;
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
render() {
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
const t = this.props.t;
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
@@ -23,7 +25,11 @@ export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
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;
+12 -7
View File
@@ -1,26 +1,28 @@
import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceProps = {
type FieldSourceInternalProps = {
value?: string
wdKey?: string
onChange?(...args: unknown[]): unknown
onChange?(value: string| undefined): unknown
sourceIds?: unknown[]
error?: unknown[]
};
error?: {message: string}
} & WithTranslation;
export default class FieldSource extends React.Component<FieldSourceProps> {
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
static defaultProps = {
onChange: () => {},
sourceIds: [],
}
render() {
const t = this.props.t;
return <Block
label={"Source"}
label={t("Source")}
fieldSpec={latest.layer.source}
error={this.props.error}
data-wd-key={this.props.wdKey}
@@ -33,3 +35,6 @@ export default class FieldSource extends React.Component<FieldSourceProps> {
</Block>
}
}
const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource;
+13 -4
View File
@@ -3,15 +3,17 @@ import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceLayerProps = {
type FieldSourceLayerInternalProps = {
value?: string
onChange?(...args: unknown[]): unknown
sourceLayerIds?: unknown[]
isFixed?: boolean
};
error?: {message: string}
} & WithTranslation;
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
@@ -19,8 +21,12 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
}
render() {
return <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
const t = this.props.t;
return <Block
label={t("Source Layer")}
fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
error={this.props.error}
>
<InputAutocomplete
keepMenuWithinWindowBounds={!!this.props.isFixed}
@@ -31,3 +37,6 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
</Block>
}
}
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
export default FieldSourceLayer;
+13 -7
View File
@@ -1,25 +1,27 @@
import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputSelect from './InputSelect'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldTypeProps = {
type FieldTypeInternalProps = {
value: string
wdKey?: string
onChange(...args: unknown[]): unknown
error?: unknown[] | undefined
onChange(value: string): unknown
error?: {message: string}
disabled?: boolean
};
} & WithTranslation;
export default class FieldType extends React.Component<FieldTypeProps> {
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
static defaultProps = {
disabled: false,
}
render() {
return <Block label={"Type"} fieldSpec={latest.layer.type}
const t = this.props.t;
return <Block label={t("Type")} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
@@ -44,8 +46,12 @@ export default class FieldType extends React.Component<FieldTypeProps> {
]}
onChange={this.props.onChange}
value={this.props.value}
data-wd-key={this.props.wdKey + ".select"}
/>
}
</Block>
}
}
const FieldType = withTranslation()(FieldTypeInternal);
export default FieldType;
+4 -5
View File
@@ -1,8 +1,9 @@
import React, { ReactElement } from 'react'
import React, { PropsWithChildren, ReactElement } from 'react'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
import generateUniqueId from '../libs/document-uid';
type FieldsetProps = {
type FieldsetProps = PropsWithChildren & {
label?: string,
fieldSpec?: { doc?: string },
action?: ReactElement,
@@ -12,14 +13,12 @@ type FieldsetState = {
showDoc: boolean
};
let IDX = 0;
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
_labelId: string;
constructor (props: FieldsetProps) {
super(props);
this._labelId = `fieldset_label_${(IDX++)}`;
this._labelId = generateUniqueId(`fieldset_label_`);
this.state = {
showDoc: false,
}
@@ -1,10 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
import {combiningFilterOps} from '../libs/filterops'
import {mdiTableRowPlusAfter} from '@mdi/js';
import {isEqual} from 'lodash';
import {ExpressionSpecification, LegacyFilterSpecification, StyleSpecification} from 'maplibre-gl'
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
import {mdiFunctionVariant} from '@mdi/js';
import {combiningFilterOps} from '../libs/filterops'
import InputSelect from './InputSelect'
import Block from './Block'
import SingleFilterEditor from './SingleFilterEditor'
@@ -12,11 +13,11 @@ import FilterEditorBlock from './FilterEditorBlock'
import InputButton from './InputButton'
import Doc from './Doc'
import ExpressionProperty from './_ExpressionProperty';
import {mdiFunctionVariant} from '@mdi/js';
import { WithTranslation, withTranslation } from 'react-i18next';
function combiningFilter (props) {
let filter = props.filter || ['all'];
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
const filter = props.filter || ['all'];
if (!Array.isArray(filter)) {
return filter;
@@ -30,14 +31,15 @@ function combiningFilter (props) {
filters = [filter.slice(0)];
}
return [combiningOp, ...filters];
return [combiningOp, ...filters] as LegacyFilterSpecification | ExpressionSpecification;
}
function migrateFilter (filter) {
return migrate(createStyleFromFilter(filter)).layers[0].filter;
function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
// This "any" can be removed in latest version of maplibre where maplibre re-exported types from style-spec
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
}
function createStyleFromFilter (filter) {
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecification & {id: string} {
return {
"id": "tmp",
"version": 8,
@@ -46,7 +48,7 @@ function createStyleFromFilter (filter) {
"sources": {
"tmp": {
"type": "geojson",
"data": {}
"data": ''
}
},
"sprite": "",
@@ -69,7 +71,7 @@ const FILTER_OPS = [
];
// If we convert a filter that is an expression to an expression it'll remain the same in value
function checkIfSimpleFilter (filter) {
function checkIfSimpleFilter (filter: LegacyFilterSpecification | ExpressionSpecification) {
if (filter.length === 1 && FILTER_OPS.includes(filter[0])) {
return true;
}
@@ -77,33 +79,38 @@ function checkIfSimpleFilter (filter) {
return !isEqual(expression, filter);
}
function hasCombiningFilter(filter) {
function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
return combiningFilterOps.indexOf(filter[0]) >= 0
}
function hasNestedCombiningFilter(filter) {
function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
if(hasCombiningFilter(filter)) {
const combinedFilters = filter.slice(1)
return filter.slice(1).map(f => hasCombiningFilter(f)).filter(f => f == true).length > 0
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0
}
return false
}
export default class FilterEditor extends React.Component {
static propTypes = {
/** Properties of the vector layer and the available fields */
properties: PropTypes.object,
filter: PropTypes.array,
errors: PropTypes.object,
onChange: PropTypes.func.isRequired,
}
type FilterEditorInternalProps = {
/** Properties of the vector layer and the available fields */
properties?: {[key:string]: any}
filter?: any[]
errors?: {[key:string]: any}
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
} & WithTranslation;
type FilterEditorState = {
showDoc: boolean
displaySimpleFilter: boolean
valueIsSimpleFilter?: boolean
};
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
static defaultProps = {
filter: ["all"],
}
constructor (props) {
super();
constructor (props: FilterEditorInternalProps) {
super(props);
this.state = {
showDoc: false,
displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
@@ -111,25 +118,25 @@ export default class FilterEditor extends React.Component {
}
// Convert filter to combining filter
onFilterPartChanged(filterIdx, newPart) {
const newFilter = combiningFilter(this.props).slice(0)
onFilterPartChanged(filterIdx: number, newPart: any[]) {
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
newFilter[filterIdx] = newPart
this.props.onChange(newFilter)
}
deleteFilterItem(filterIdx) {
const newFilter = combiningFilter(this.props).slice(0)
deleteFilterItem(filterIdx: number) {
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
newFilter.splice(filterIdx + 1, 1)
this.props.onChange(newFilter)
}
addFilterItem = () => {
const newFilterItem = combiningFilter(this.props).slice(0)
newFilterItem.push(['==', 'name', ''])
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
(newFilterItem as any[]).push(['==', 'name', ''])
this.props.onChange(newFilterItem)
}
onToggleDoc = (val) => {
onToggleDoc = (val: boolean) => {
this.setState({
showDoc: val
});
@@ -142,25 +149,24 @@ export default class FilterEditor extends React.Component {
}
makeExpression = () => {
let filter = combiningFilter(this.props);
const filter = combiningFilter(this.props);
this.props.onChange(migrateFilter(filter));
this.setState({
displaySimpleFilter: false,
})
}
static getDerivedStateFromProps (props, currentState) {
const {filter} = props;
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
// Upgrade but never downgrade
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
if (!displaySimpleFilter && state.displaySimpleFilter === true) {
return {
displaySimpleFilter: false,
valueIsSimpleFilter: false,
};
}
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
return {
valueIsSimpleFilter: true,
}
@@ -173,41 +179,41 @@ export default class FilterEditor extends React.Component {
}
render() {
const {errors} = this.props;
const {errors, t} = this.props;
const {displaySimpleFilter} = this.state;
const fieldSpec={
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
};
const defaultFilter = ["all"];
const defaultFilter = ["all"] as LegacyFilterSpecification | ExpressionSpecification;
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
if (isNestedCombiningFilter) {
return <div className="maputnik-filter-editor-unsupported">
<p>
Nested filters are not supported.
{t("Nested filters are not supported.")}
</p>
<InputButton
onClick={this.makeExpression}
title="Convert to expression"
title={t("Convert to expression")}
>
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
Upgrade to expression
{t("Upgrade to expression")}
</InputButton>
</div>
}
else if (displaySimpleFilter) {
const filter = combiningFilter(this.props);
let combiningOp = filter[0];
let filters = filter.slice(1)
const combiningOp = filter[0];
const filters = filter.slice(1) as (LegacyFilterSpecification | ExpressionSpecification)[];
const actions = (
<div>
<InputButton
onClick={this.makeExpression}
title="Convert to expression"
title={t("Convert to expression")}
className="maputnik-make-zoom-function"
>
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
@@ -218,7 +224,7 @@ export default class FilterEditor extends React.Component {
);
const editorBlocks = filters.map((f, idx) => {
const error = errors[`filter[${idx+1}]`];
const error = errors![`filter[${idx+1}]`];
return (
<div key={`block-${idx}`}>
@@ -242,13 +248,17 @@ export default class FilterEditor extends React.Component {
<Block
key="top"
fieldSpec={fieldSpec}
label={"Filter"}
label={t("Filter")}
action={actions}
>
<InputSelect
value={combiningOp}
onChange={this.onFilterPartChanged.bind(this, 0)}
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
options={[
["all", t("every filter matches")],
["none", t("no filter matches")],
["any", t("any filter matches")]
]}
/>
</Block>
{editorBlocks}
@@ -263,7 +273,7 @@ export default class FilterEditor extends React.Component {
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add filter
</svg> {t("Add filter")}
</InputButton>
</div>
<div
@@ -277,7 +287,7 @@ export default class FilterEditor extends React.Component {
);
}
else {
let {filter} = this.props;
const {filter} = this.props;
return (
<>
@@ -294,12 +304,13 @@ export default class FilterEditor extends React.Component {
/>
{this.state.valueIsSimpleFilter &&
<div className="maputnik-expr-infobox">
You&apos;ve entered a old style filter,{' '}
{t("You've entered an old style filter.")}
{' '}
<button
onClick={this.makeFilter}
className="maputnik-expr-infobox__button"
>
switch to filter editor
{t("Switch to filter editor.")}
</button>
</div>
}
@@ -308,3 +319,6 @@ export default class FilterEditor extends React.Component {
}
}
}
const FilterEditor = withTranslation()(FilterEditorInternal);
export default FilterEditor;
+9 -5
View File
@@ -1,19 +1,21 @@
import React from 'react'
import React, { PropsWithChildren } from 'react'
import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
type FilterEditorBlockProps = {
type FilterEditorBlockInternalProps = PropsWithChildren & {
onDelete(...args: unknown[]): unknown
};
} & WithTranslation;
export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> {
render() {
const t = this.props.t;
return <div className="maputnik-filter-editor-block">
<div className="maputnik-filter-editor-block-action">
<InputButton
className="maputnik-delete-filter"
onClick={this.props.onDelete}
title="Delete filter block"
title={t("Delete filter block")}
>
<MdDelete />
</InputButton>
@@ -25,3 +27,5 @@ export default class FilterEditorBlock extends React.Component<FilterEditorBlock
}
}
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
export default FilterEditorBlock;
+11 -10
View File
@@ -10,22 +10,23 @@ import IconMissing from './IconMissing'
type IconLayerProps = {
type: string
style?: object
className?: string
};
export default class IconLayer extends React.Component<IconLayerProps> {
render() {
const iconProps = { style: this.props.style }
switch(this.props.type) {
case 'fill-extrusion': return <IconBackground {...iconProps} />
case 'raster': return <IconFill {...iconProps} />
case 'hillshade': return <IconFill {...iconProps} />
case 'heatmap': return <IconFill {...iconProps} />
case 'fill': return <IconFill {...iconProps} />
case 'background': return <IconBackground {...iconProps} />
case 'line': return <IconLine {...iconProps} />
case 'symbol': return <IconSymbol {...iconProps} />
case 'circle': return <IconCircle {...iconProps} />
default: return <IconMissing {...iconProps} />
case 'fill-extrusion': return <IconBackground {...iconProps} />
case 'raster': return <IconFill {...iconProps} />
case 'hillshade': return <IconFill {...iconProps} />
case 'heatmap': return <IconFill {...iconProps} />
case 'fill': return <IconFill {...iconProps} />
case 'background': return <IconBackground {...iconProps} />
case 'line': return <IconLine {...iconProps} />
case 'symbol': return <IconSymbol {...iconProps} />
case 'circle': return <IconCircle {...iconProps} />
default: return <IconMissing {...iconProps} />
}
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
};
}
static getDerivedStateFromProps(props: FieldArrayProps, state: FieldArrayState) {
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) {
const value: any[] = [];
const initialPropsValue = state.initialPropsValue.slice(0);
+2 -2
View File
@@ -8,7 +8,7 @@ const MAX_HEIGHT = 140;
export type InputAutocompleteProps = {
value?: string
options: any[]
onChange(...args: unknown[]): unknown
onChange(value: string | undefined): unknown
keepMenuWithinWindowBounds?: boolean
'aria-label'?: string
};
@@ -91,7 +91,7 @@ export default class InputAutocomplete extends React.Component<InputAutocomplete
"maputnik-autocomplete-menu-item-selected": isHighlighted,
})}
>
{item[1]}
{item[1]}
</div>
)}
/>
+2 -2
View File
@@ -27,8 +27,8 @@ export default class InputCheckbox extends React.Component<InputCheckboxProps> {
/>
<div className="maputnik-checkbox-box">
<svg style={{
display: this.props.value ? 'inline' : 'none'
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
display: this.props.value ? 'inline' : 'none'
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
</svg>
</div>
+3 -3
View File
@@ -87,8 +87,8 @@ export default class InputColor extends React.Component<InputColorProps> {
const picker = <div
className="maputnik-color-picker-offset"
style={{
position: 'fixed',
zIndex: 1,
position: 'fixed',
zIndex: 1,
left: offset.left,
top: offset.top,
}}>
@@ -110,7 +110,7 @@ export default class InputColor extends React.Component<InputColorProps> {
/>
</div>
var swatchStyle = {
const swatchStyle = {
backgroundColor: this.props.value
};
+18 -8
View File
@@ -1,6 +1,7 @@
import React from 'react'
import capitalize from 'lodash.capitalize'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
import InputString from './InputString'
import InputNumber from './InputNumber'
@@ -21,10 +22,11 @@ export type FieldDynamicArrayProps = {
}
'aria-label'?: string
label: string
};
}
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation;
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> {
changeValue(idx: number, newValue: string | number | undefined) {
const values = this.values.slice(0)
values[idx] = newValue
@@ -62,8 +64,13 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
}
render() {
const t = this.props.t;
const i18nProps = { t, i18n: this.props.i18n, tReady: this.props.tReady };
const inputs = this.values.map((v, i) => {
const deleteValueBtn= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
const deleteValueBtn= <DeleteValueInputButton
onClick={this.deleteValue.bind(this, i)}
{...i18nProps}
/>;
let input;
if(this.props.type === 'url') {
input = <InputUrl
@@ -100,7 +107,7 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
style={this.props.style}
key={i}
className="maputnik-array-block"
>
>
<div className="maputnik-array-block-action">
{deleteValueBtn}
</div>
@@ -117,27 +124,30 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
className="maputnik-array-add-value"
onClick={this.addValue}
>
Add value
{t("Add value")}
</InputButton>
</div>
);
}
}
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
export default FieldDynamicArray;
type DeleteValueInputButtonProps = {
onClick?(...args: unknown[]): unknown
};
} & WithTranslation;
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
render() {
const t = this.props.t;
return <InputButton
className="maputnik-delete-stop"
onClick={this.props.onClick}
title="Remove array item"
title={t("Remove array item")}
>
<FieldDocLabel
label={<MdDelete />}
fieldSpec={{doc:" Remove array item."}}
/>
</InputButton>
}
+12 -4
View File
@@ -1,6 +1,7 @@
import React from 'react'
import classnames from 'classnames';
import CodeMirror, { ModeSpec } from 'codemirror';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import 'codemirror/mode/javascript/javascript'
import 'codemirror/addon/lint/lint'
@@ -8,7 +9,7 @@ import 'codemirror/addon/edit/matchbrackets'
import 'codemirror/lib/codemirror.css'
import 'codemirror/addon/lint/lint.css'
import stringifyPretty from 'json-stringify-pretty-compact'
import '../util/codemirror-mgl';
import '../libs/codemirror-mgl';
export type InputJsonProps = {
@@ -27,6 +28,7 @@ export type InputJsonProps = {
mode?: ModeSpec<any>
lint?: boolean | object
};
type InputJsonInternalProps = InputJsonProps & WithTranslation;
type InputJsonState = {
isEditing: boolean
@@ -34,7 +36,7 @@ type InputJsonState = {
prevValue: string
};
export default class InputJson extends React.Component<InputJsonProps, InputJsonState> {
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
static defaultProps = {
lineNumbers: true,
lineWrapping: false,
@@ -52,7 +54,7 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
_el: HTMLDivElement | null = null;
_cancelNextChange: boolean = false;
constructor(props: InputJsonProps) {
constructor(props: InputJsonInternalProps) {
super(props);
this._keyEvent = "keyboard";
this.state = {
@@ -156,6 +158,7 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
}
render() {
const t = this.props.t;
const {showMessage} = this.state;
const style = {} as {maxHeight?: number};
if (this.props.maxHeight) {
@@ -164,7 +167,9 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
Press <kbd>ESC</kbd> to lose focus
<Trans t={t}>
Press <kbd>ESC</kbd> to lose focus
</Trans>
</div>
<div
className={classnames("codemirror-container", this.props.className)}
@@ -174,3 +179,6 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
</div>
}
}
const InputJson = withTranslation()(InputJsonInternal);
export default InputJson;
+17 -10
View File
@@ -1,6 +1,5 @@
import React, { BaseSyntheticEvent } from 'react'
let IDX = 0;
import generateUniqueId from '../libs/document-uid';
export type InputNumberProps = {
value?: number
@@ -10,7 +9,7 @@ export type InputNumberProps = {
onChange?(value: number | undefined): unknown
allowRange?: boolean
rangeStep?: number
wdKey?: string
"data-wd-key"?: string
required?: boolean
"aria-label"?: string
};
@@ -20,7 +19,10 @@ type InputNumberState = {
editing: boolean
editingRange?: boolean
value?: number
dirtyValue?: number
/**
* This is the value that is currently being edited. It can be an invalid value.
*/
dirtyValue?: number | string | undefined
}
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
@@ -32,14 +34,14 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
constructor(props: InputNumberProps) {
super(props)
this.state = {
uuid: IDX++,
uuid: +generateUniqueId(),
editing: false,
value: props.value,
dirtyValue: props.value,
}
}
static getDerivedStateFromProps(props: InputNumberProps, state: InputNumberState) {
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
if (!state.editing && props.value !== state.value) {
return {
value: props.value,
@@ -67,7 +69,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}
this.setState({
dirtyValue: newValue === "" ? undefined : value,
dirtyValue: newValue === "" ? undefined : newValue,
})
}
@@ -126,7 +128,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
// for example we might go from 13 to 13.23, however because we know
// that came from a keyboard event we always want to increase by a
// single step value.
if (value < this.state.dirtyValue!) {
if (value < +this.state.dirtyValue!) {
value = this.state.value! - step;
}
else {
@@ -140,7 +142,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}
else {
value = value + (step - snap);
};
}
}
}
@@ -155,7 +157,8 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
render() {
if(
this.props.hasOwnProperty("min") && this.props.hasOwnProperty("max") &&
Object.prototype.hasOwnProperty.call(this.props, "min") &&
Object.prototype.hasOwnProperty.call(this.props, "max") &&
this.props.min !== undefined && this.props.max !== undefined &&
this.props.allowRange
) {
@@ -197,6 +200,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
dirtyValue: this.state.value,
});
}}
data-wd-key={this.props["data-wd-key"] + "-range"}
/>
<input
key="text"
@@ -215,6 +219,8 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
this.setState({editing: false});
this.resetValue()
}}
data-wd-key={this.props["data-wd-key"] + "-text"}
/>
</div>
}
@@ -233,6 +239,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}}
onBlur={this.resetValue}
required={this.props.required}
data-wd-key={this.props["data-wd-key"]}
/>
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ export type InputSelectProps = {
"data-wd-key"?: string
options: [string, any][] | string[]
style?: object
onChange(value: string): unknown
onChange(value: string | [string, any]): unknown
title?: string
'aria-label'?: string
};
+56 -54
View File
@@ -49,70 +49,72 @@ export default class SpecField extends React.Component<SpecFieldProps> {
value: this.props.value,
default: this.props.fieldSpec?.default,
name: this.props.fieldName,
"data-wd-key": "spec-field-input:" + this.props.fieldName,
onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
'aria-label': this.props['aria-label'],
}
switch(this.props.fieldSpec?.type) {
case 'number': return (
<InputNumber
{...commonProps as InputNumberProps}
min={this.props.fieldSpec.minimum}
max={this.props.fieldSpec.maximum}
/>
)
case 'enum':
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)])
case 'number': return (
<InputNumber
{...commonProps as InputNumberProps}
min={this.props.fieldSpec.minimum}
max={this.props.fieldSpec.maximum}
/>
)
case 'enum': {
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)])
return <InputEnum
{...commonProps as Omit<InputEnumProps, "options">}
options={options}
return <InputEnum
{...commonProps as Omit<InputEnumProps, "options">}
options={options}
/>
}
case 'resolvedImage':
case 'formatted':
case 'string':
if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
const options = this.props.fieldSpec.values || [];
return <InputAutocomplete
{...commonProps as Omit<InputAutocompleteProps, "options">}
options={options.map(f => [f, f])}
/>
case 'resolvedImage':
case 'formatted':
case 'string':
if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
const options = this.props.fieldSpec.values || [];
return <InputAutocomplete
{...commonProps as Omit<InputAutocompleteProps, "options">}
options={options.map(f => [f, f])}
} else {
return <InputString
{...commonProps as InputStringProps}
/>
}
case 'color': return (
<InputColor
{...commonProps as InputColorProps}
/>
)
case 'boolean': return (
<InputCheckbox
{...commonProps as InputCheckboxProps}
/>
)
case 'array':
if(this.props.fieldName === 'text-font') {
return <InputFont
{...commonProps as FieldFontProps}
fonts={this.props.fieldSpec.values}
/>
} else {
if (this.props.fieldSpec.length) {
return <InputArray
{...commonProps as FieldArrayProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>
} else {
return <InputString
{...commonProps as InputStringProps}
return <InputDynamicArray
{...commonProps as FieldDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type={this.props.fieldSpec.value as FieldDynamicArrayProps['type']}
/>
}
case 'color': return (
<InputColor
{...commonProps as InputColorProps}
/>
)
case 'boolean': return (
<InputCheckbox
{...commonProps as InputCheckboxProps}
/>
)
case 'array':
if(this.props.fieldName === 'text-font') {
return <InputFont
{...commonProps as FieldFontProps}
fonts={this.props.fieldSpec.values}
/>
} else {
if (this.props.fieldSpec.length) {
return <InputArray
{...commonProps as FieldArrayProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>
} else {
return <InputDynamicArray
{...commonProps as FieldDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type={this.props.fieldSpec.value as FieldDynamicArrayProps['type']}
/>
}
}
default: return null
}
default: return null
}
}
+4 -4
View File
@@ -33,7 +33,7 @@ export default class InputString extends React.Component<InputStringProps, Input
}
}
static getDerivedStateFromProps(props: InputStringProps, state: InputStringState) {
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
if (!state.editing) {
return {
value: props.value
@@ -46,7 +46,7 @@ export default class InputString extends React.Component<InputStringProps, Input
let tag;
let classes;
if(!!this.props.multi) {
if(this.props.multi) {
tag = "textarea"
classes = [
"maputnik-string",
@@ -60,14 +60,14 @@ export default class InputString extends React.Component<InputStringProps, Input
]
}
if(!!this.props.disabled) {
if(this.props.disabled) {
classes.push("maputnik-string--disabled");
}
return React.createElement(tag, {
"aria-label": this.props["aria-label"],
"data-wd-key": this.props["data-wd-key"],
spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
spellCheck: Object.prototype.hasOwnProperty.call(this.props, "spellCheck") ? this.props.spellCheck : !(tag === "input"),
disabled: this.props.disabled,
className: classes.join(" "),
style: this.props.style,
+28 -17
View File
@@ -1,9 +1,10 @@
import React from 'react'
import InputString from './InputString'
import SmallError from './SmallError'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
function validate(url: string) {
function validate(url: string, t: TFunction): JSX.Element | undefined {
if (url === "") {
return;
}
@@ -22,15 +23,19 @@ function validate(url: string) {
const isSsl = window.location.protocol === "https:";
if (!protocol) {
error = (
<SmallError>
Must provide protocol {
isSsl
? <code>https://</code>
: <><code>http://</code> or <code>https://</code></>
}
</SmallError>
);
if (isSsl) {
error = (
<SmallError>
<Trans t={t}>Must provide protocol: <code>https://</code></Trans>
</SmallError>
);
} else {
error = (
<SmallError>
<Trans t={t}>Must provide protocol: <code>http://</code> or <code>https://</code></Trans>
</SmallError>
);
}
}
else if (
protocol &&
@@ -39,7 +44,9 @@ function validate(url: string) {
) {
error = (
<SmallError>
CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain
<Trans t={t}>
CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain
</Trans>
</SmallError>
);
}
@@ -61,32 +68,34 @@ export type FieldUrlProps = {
className?: string
};
type FieldUrlInternalProps = FieldUrlProps & WithTranslation;
type FieldUrlState = {
error?: React.ReactNode
}
export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlState> {
class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlState> {
static defaultProps = {
onInput: () => {},
}
constructor (props: FieldUrlProps) {
constructor (props: FieldUrlInternalProps) {
super(props);
this.state = {
error: validate(props.value)
error: validate(props.value, props.t),
};
}
onInput = (url: string) => {
this.setState({
error: validate(url)
error: validate(url, this.props.t),
});
if (this.props.onInput) this.props.onInput(url);
}
onChange = (url: string) => {
this.setState({
error: validate(url)
error: validate(url, this.props.t),
});
this.props.onChange(url);
}
@@ -106,3 +115,5 @@ export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlSta
}
}
const FieldUrl = withTranslation()(FieldUrlInternal);
export default FieldUrl;
-337
View File
@@ -1,337 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
import FieldJson from './FieldJson'
import FilterEditor from './FilterEditor'
import PropertyGroup from './PropertyGroup'
import LayerEditorGroup from './LayerEditorGroup'
import FieldType from './FieldType'
import FieldId from './FieldId'
import FieldMinZoom from './FieldMinZoom'
import FieldMaxZoom from './FieldMaxZoom'
import FieldComment from './FieldComment'
import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer'
import {Accordion} from 'react-accessible-accordion';
import {MdMoreVert} from 'react-icons/md'
import { changeType, changeProperty } from '../libs/layer'
import layout from '../config/layout.json'
import {formatLayerId} from '../util/format';
function getLayoutForType (type) {
return layout[type] ? layout[type] : layout.invalid;
}
function layoutGroups(layerType) {
const layerGroup = {
title: 'Layer',
type: 'layer'
}
const filterGroup = {
title: 'Filter',
type: 'filter'
}
const editorGroup = {
title: 'JSON Editor',
type: 'jsoneditor'
}
return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType).groups)
.concat([editorGroup])
}
/** Layer editor supporting multiple types of layers. */
export default class LayerEditor extends React.Component {
static propTypes = {
layer: PropTypes.object.isRequired,
sources: PropTypes.object,
vectorLayers: PropTypes.object,
spec: PropTypes.object.isRequired,
onLayerChanged: PropTypes.func,
onLayerIdChange: PropTypes.func,
onMoveLayer: PropTypes.func,
onLayerDestroy: PropTypes.func,
onLayerCopy: PropTypes.func,
onLayerVisibilityToggle: PropTypes.func,
isFirstLayer: PropTypes.bool,
isLastLayer: PropTypes.bool,
layerIndex: PropTypes.number,
errors: PropTypes.array,
}
static defaultProps = {
onLayerChanged: () => {},
onLayerIdChange: () => {},
onLayerDestroyed: () => {},
}
static childContextTypes = {
reactIconBase: PropTypes.object
}
constructor(props) {
super(props)
//TODO: Clean this up and refactor into function
const editorGroups = {}
layoutGroups(this.props.layer.type).forEach(group => {
editorGroups[group.title] = true
})
this.state = { editorGroups }
}
static getDerivedStateFromProps(props, state) {
const additionalGroups = { ...state.editorGroups }
getLayoutForType(props.layer.type).groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
})
return {
editorGroups: additionalGroups
};
}
getChildContext () {
return {
reactIconBase: {
size: 14,
color: '#8e8e8e',
}
}
}
changeProperty(group, property, newValue) {
this.props.onLayerChanged(
this.props.layerIndex,
changeProperty(this.props.layer, group, property, newValue)
)
}
onGroupToggle(groupTitle, active) {
const changedActiveGroups = {
...this.state.editorGroups,
[groupTitle]: active,
}
this.setState({
editorGroups: changedActiveGroups
})
}
renderGroupType(type, fields) {
let comment = ""
if(this.props.layer.metadata) {
comment = this.props.layer.metadata['maputnik:comment']
}
const {errors, layerIndex} = this.props;
const errorData = {};
errors.forEach(error => {
if (
error.parsed &&
error.parsed.type === "layer" &&
error.parsed.data.index == layerIndex
) {
errorData[error.parsed.data.key] = {
message: error.parsed.data.message
};
}
})
let sourceLayerIds;
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
sourceLayerIds = this.props.sources[this.props.layer.source].layers;
}
switch(type) {
case 'layer': return <div>
<FieldId
value={this.props.layer.id}
wdKey="layer-editor.layer-id"
error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/>
<FieldType
disabled={true}
error={errorData.type}
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(
this.props.layerIndex,
changeType(this.props.layer, newType)
)}
/>
{this.props.layer.type !== 'background' && <FieldSource
error={errorData.source}
sourceIds={Object.keys(this.props.sources)}
value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)}
/>
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
<FieldSourceLayer
error={errorData['source-layer']}
sourceLayerIds={sourceLayerIds}
value={this.props.layer['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/>
}
<FieldMinZoom
error={errorData.minzoom}
value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, 'minzoom', v)}
/>
<FieldMaxZoom
error={errorData.maxzoom}
value={this.props.layer.maxzoom}
onChange={v => this.changeProperty(null, 'maxzoom', v)}
/>
<FieldComment
error={errorData.comment}
value={comment}
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
/>
</div>
case 'filter': return <div>
<div className="maputnik-filter-editor-wrapper">
<FilterEditor
errors={errorData}
filter={this.props.layer.filter}
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)}
/>
</div>
</div>
case 'properties':
return <PropertyGroup
errors={errorData}
layer={this.props.layer}
groupFields={fields}
spec={this.props.spec}
onChange={this.changeProperty.bind(this)}
/>
case 'jsoneditor':
return <FieldJson
layer={this.props.layer}
onChange={(layer) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/>
}
}
moveLayer(offset) {
this.props.onMoveLayer({
oldIndex: this.props.layerIndex,
newIndex: this.props.layerIndex+offset
})
}
render() {
const groupIds = [];
const layerType = this.props.layer.type
const groups = layoutGroups(layerType).filter(group => {
return !(layerType === 'background' && group.type === 'source')
}).map(group => {
const groupId = group.title.replace(/ /g, "_");
groupIds.push(groupId);
return <LayerEditorGroup
data-wd-key={group.title}
id={groupId}
key={group.title}
title={group.title}
isActive={this.state.editorGroups[group.title]}
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
>
{this.renderGroupType(group.type, group.fields)}
</LayerEditorGroup>
})
const layout = this.props.layer.layout || {}
const items = {
delete: {
text: "Delete",
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
},
duplicate: {
text: "Duplicate",
handler: () => this.props.onLayerCopy(this.props.layerIndex)
},
hide: {
text: (layout.visibility === "none") ? "Show" : "Hide",
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
},
moveLayerUp: {
text: "Move layer up",
// Not actually used...
disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1)
},
moveLayerDown: {
text: "Move layer down",
// Not actually used...
disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1)
}
}
function handleSelection(id, event) {
event.stopPropagation;
items[id].handler();
}
return <section className="maputnik-layer-editor"
role="main"
aria-label="Layer editor"
>
<header>
<div className="layer-header">
<h2 className="layer-header__title">
Layer: {formatLayerId(this.props.layer.id)}
</h2>
<div className="layer-header__info">
<Wrapper
className='more-menu'
onSelection={handleSelection}
closeOnSelection={false}
>
<Button id="skip-target-layer-editor" data-wd-key="skip-target-layer-editor" className='more-menu__button' title="Layer options">
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
<ul className="more-menu__menu">
{Object.keys(items).map((id, idx) => {
const item = items[id];
return <li key={id}>
<MenuItem value={id} className='more-menu__menu__item'>
{item.text}
</MenuItem>
</li>
})}
</ul>
</Menu>
</Wrapper>
</div>
</div>
</header>
<Accordion
allowMultipleExpanded={true}
allowZeroExpanded={true}
preExpanded={groupIds}
>
{groups}
</Accordion>
</section>
}
}
+365
View File
@@ -0,0 +1,365 @@
import React, {type JSX} from 'react'
import PropTypes from 'prop-types'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
import {Accordion} from 'react-accessible-accordion';
import {MdMoreVert} from 'react-icons/md'
import {BackgroundLayerSpecification, LayerSpecification, SourceSpecification} from 'maplibre-gl';
import FieldJson from './FieldJson'
import FilterEditor from './FilterEditor'
import PropertyGroup from './PropertyGroup'
import LayerEditorGroup from './LayerEditorGroup'
import FieldType from './FieldType'
import FieldId from './FieldId'
import FieldMinZoom from './FieldMinZoom'
import FieldMaxZoom from './FieldMaxZoom'
import FieldComment from './FieldComment'
import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer'
import { changeType, changeProperty } from '../libs/layer'
import layout from '../config/layout.json'
import {formatLayerId} from '../libs/format';
import { WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
function getLayoutForType(type: LayerSpecification["type"], t: TFunction) {
return layout[type] ? {
...layout[type],
groups: layout[type].groups.map(group => {
return {
...group,
id: group.title.replace(/ /g, "_"),
title: t(group.title)
};
}),
} : layout.invalid;
}
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
const layerGroup = {
id: 'layer',
title: t('Layer'),
type: 'layer'
}
const filterGroup = {
id: 'filter',
title: t('Filter'),
type: 'filter'
}
const editorGroup = {
id: 'jsoneditor',
title: t('JSON Editor'),
type: 'jsoneditor'
}
return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType, t).groups)
.concat([editorGroup])
}
type LayerEditorInternalProps = {
layer: LayerSpecification
sources: {[key: string]: SourceSpecification}
vectorLayers: {[key: string]: any}
spec: object
onLayerChanged(...args: unknown[]): unknown
onLayerIdChange(...args: unknown[]): unknown
onMoveLayer(...args: unknown[]): unknown
onLayerDestroy(...args: unknown[]): unknown
onLayerCopy(...args: unknown[]): unknown
onLayerVisibilityToggle(...args: unknown[]): unknown
isFirstLayer?: boolean
isLastLayer?: boolean
layerIndex: number
errors?: any[]
} & WithTranslation;
type LayerEditorState = {
editorGroups: {[keys:string]: boolean}
};
/** Layer editor supporting multiple types of layers. */
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
static defaultProps = {
onLayerChanged: () => {},
onLayerIdChange: () => {},
onLayerDestroyed: () => {},
}
static childContextTypes = {
reactIconBase: PropTypes.object
}
constructor(props: LayerEditorInternalProps) {
super(props)
//TODO: Clean this up and refactor into function
const editorGroups: {[keys:string]: boolean} = {}
layoutGroups(this.props.layer.type, props.t).forEach(group => {
editorGroups[group.title] = true
})
this.state = { editorGroups }
}
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
const additionalGroups = { ...state.editorGroups }
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
})
return {
editorGroups: additionalGroups
};
}
getChildContext () {
return {
reactIconBase: {
size: 14,
color: '#8e8e8e',
}
}
}
changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) {
this.props.onLayerChanged(
this.props.layerIndex,
changeProperty(this.props.layer, group, property, newValue)
)
}
onGroupToggle(groupTitle: string, active: boolean) {
const changedActiveGroups = {
...this.state.editorGroups,
[groupTitle]: active,
}
this.setState({
editorGroups: changedActiveGroups
})
}
renderGroupType(type: string, fields?: string[]): JSX.Element {
let comment = ""
if(this.props.layer.metadata) {
comment = (this.props.layer.metadata as any)['maputnik:comment']
}
const {errors, layerIndex} = this.props;
const errorData: {[key in LayerSpecification as string]: {message: string}} = {};
errors!.forEach(error => {
if (
error.parsed &&
error.parsed.type === "layer" &&
error.parsed.data.index == layerIndex
) {
errorData[error.parsed.data.key] = {
message: error.parsed.data.message
};
}
})
let sourceLayerIds;
const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
if(Object.prototype.hasOwnProperty.call(this.props.sources, layer.source)) {
sourceLayerIds = (this.props.sources[layer.source] as any).layers;
}
switch(type) {
case 'layer': return <div>
<FieldId
value={this.props.layer.id}
wdKey="layer-editor.layer-id"
error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/>
<FieldType
disabled={true}
error={errorData.type}
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(
this.props.layerIndex,
changeType(this.props.layer, newType)
)}
/>
{this.props.layer.type !== 'background' && <FieldSource
error={errorData.source}
sourceIds={Object.keys(this.props.sources!)}
value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)}
/>
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
<FieldSourceLayer
error={errorData['source-layer']}
sourceLayerIds={sourceLayerIds}
value={(this.props.layer as any)['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/>
}
<FieldMinZoom
error={errorData.minzoom}
value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, 'minzoom', v)}
/>
<FieldMaxZoom
error={errorData.maxzoom}
value={this.props.layer.maxzoom}
onChange={v => this.changeProperty(null, 'maxzoom', v)}
/>
<FieldComment
error={errorData.comment}
value={comment}
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
/>
</div>
case 'filter': return <div>
<div className="maputnik-filter-editor-wrapper">
<FilterEditor
errors={errorData}
filter={(this.props.layer as any).filter}
properties={this.props.vectorLayers[(this.props.layer as any)['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)}
/>
</div>
</div>
case 'properties':
return <PropertyGroup
errors={errorData}
layer={this.props.layer}
groupFields={fields!}
spec={this.props.spec}
onChange={this.changeProperty.bind(this)}
/>
case 'jsoneditor':
return <FieldJson
layer={this.props.layer}
onChange={(layer) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/>
default: return <></>
}
}
moveLayer(offset: number) {
this.props.onMoveLayer({
oldIndex: this.props.layerIndex,
newIndex: this.props.layerIndex+offset
})
}
render() {
const t = this.props.t;
const groupIds: string[] = [];
const layerType = this.props.layer.type
const groups = layoutGroups(layerType, t).filter(group => {
return !(layerType === 'background' && group.type === 'source')
}).map(group => {
const groupId = group.id;
groupIds.push(groupId);
return <LayerEditorGroup
data-wd-key={group.title}
id={groupId}
key={groupId}
title={group.title}
isActive={this.state.editorGroups[group.title]}
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
>
{this.renderGroupType(group.type, group.fields)}
</LayerEditorGroup>
})
const layout = this.props.layer.layout || {}
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
delete: {
text: t("Delete"),
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
},
duplicate: {
text: t("Duplicate"),
handler: () => this.props.onLayerCopy(this.props.layerIndex)
},
hide: {
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
},
moveLayerUp: {
text: t("Move layer up"),
// Not actually used...
disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1)
},
moveLayerDown: {
text: t("Move layer down"),
// Not actually used...
disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1)
}
}
function handleSelection(id: string, event: React.SyntheticEvent) {
event.stopPropagation();
items[id].handler();
}
return <section className="maputnik-layer-editor"
role="main"
aria-label={t("Layer editor")}
>
<header>
<div className="layer-header">
<h2 className="layer-header__title">
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
</h2>
<div className="layer-header__info">
<Wrapper
className='more-menu'
onSelection={handleSelection}
closeOnSelection={false}
>
<Button
id="skip-target-layer-editor"
data-wd-key="skip-target-layer-editor"
className='more-menu__button'
title={"Layer options"}>
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
<ul className="more-menu__menu">
{Object.keys(items).map((id) => {
const item = items[id];
return <li key={id}>
<MenuItem value={id} className='more-menu__menu__item'>
{item.text}
</MenuItem>
</li>
})}
</ul>
</Menu>
</Wrapper>
</div>
</div>
</header>
<Accordion
allowMultipleExpanded={true}
allowZeroExpanded={true}
preExpanded={groupIds}
>
{groups}
</Accordion>
</section>
}
}
const LayerEditor = withTranslation()(LayerEditorInternal);
export default LayerEditor;
+5 -5
View File
@@ -5,10 +5,10 @@ import {
mdiMenuUp
} from '@mdi/js';
import {
AccordionItem,
AccordionItemHeading,
AccordionItemButton,
AccordionItemPanel,
AccordionItem,
AccordionItemHeading,
AccordionItemButton,
AccordionItemPanel,
} from 'react-accessible-accordion';
@@ -18,7 +18,7 @@ type LayerEditorGroupProps = {
title: string
isActive: boolean
children: React.ReactElement
onActiveToggle(...args: unknown[]): unknown
onActiveToggle(active: boolean): unknown
};
@@ -1,5 +1,4 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {type JSX} from 'react'
import classnames from 'classnames'
import lodash from 'lodash';
@@ -7,44 +6,41 @@ import LayerListGroup from './LayerListGroup'
import LayerListItem from './LayerListItem'
import ModalAdd from './ModalAdd'
import {SortableContainer} from 'react-sortable-hoc';
import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
import type {LayerSpecification} from 'maplibre-gl';
import generateUniqueId from '../libs/document-uid';
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
import { WithTranslation, withTranslation } from 'react-i18next';
const layerListPropTypes = {
layers: PropTypes.array.isRequired,
selectedLayerIndex: PropTypes.number.isRequired,
onLayersChange: PropTypes.func.isRequired,
onLayerSelect: PropTypes.func,
sources: PropTypes.object.isRequired,
}
type LayerListContainerProps = {
layers: LayerSpecification[]
selectedLayerIndex: number
onLayersChange(layers: LayerSpecification[]): unknown
onLayerSelect(...args: unknown[]): unknown
onLayerDestroy?(...args: unknown[]): unknown
onLayerCopy(...args: unknown[]): unknown
onLayerVisibilityToggle(...args: unknown[]): unknown
sources: object
errors: any[]
};
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
function layerPrefix(name) {
return name.replace(' ', '-').replace('_', '-').split('-')[0]
}
function findClosestCommonPrefix(layers, idx) {
const currentLayerPrefix = layerPrefix(layers[idx].id)
let closestIdx = idx
for (let i = idx; i > 0; i--) {
const previousLayerPrefix = layerPrefix(layers[i-1].id)
if(previousLayerPrefix === currentLayerPrefix) {
closestIdx = i - 1
} else {
return closestIdx
}
}
return closestIdx
}
let UID = 0;
type LayerListContainerState = {
collapsedGroups: {[ket: string]: boolean}
areAllGroupsExpanded: boolean
keys: {[key: string]: number}
isOpen: {[key: string]: boolean}
};
// List of collapsible layer editors
class LayerListContainer extends React.Component {
static propTypes = {...layerListPropTypes}
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
static defaultProps = {
onLayerSelect: () => {},
}
selectedItemRef: React.RefObject<any>;
scrollContainerRef: React.RefObject<HTMLElement>;
constructor(props) {
constructor(props: LayerListContainerInternalProps) {
super(props);
this.selectedItemRef = React.createRef();
this.scrollContainerRef = React.createRef();
@@ -52,7 +48,7 @@ class LayerListContainer extends React.Component {
collapsedGroups: {},
areAllGroupsExpanded: false,
keys: {
add: UID++,
add: +generateUniqueId(),
},
isOpen: {
add: false,
@@ -60,11 +56,11 @@ class LayerListContainer extends React.Component {
}
}
toggleModal(modalName) {
toggleModal(modalName: string) {
this.setState({
keys: {
...this.state.keys,
[modalName]: UID++,
[modalName]: +generateUniqueId(),
},
isOpen: {
...this.state.isOpen,
@@ -74,9 +70,9 @@ class LayerListContainer extends React.Component {
}
toggleLayers = () => {
let idx=0
let idx = 0
let newGroups=[]
const newGroups: {[key:string]: boolean} = {}
this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id)
@@ -87,7 +83,7 @@ class LayerListContainer extends React.Component {
newGroups[lookupKey] = this.state.areAllGroupsExpanded
}
layers.forEach((layer) => {
layers.forEach((_layer) => {
idx += 1
})
});
@@ -98,7 +94,7 @@ class LayerListContainer extends React.Component {
})
}
groupedLayers() {
groupedLayers(): (LayerSpecification & {key: string})[][] {
const groups = []
const layerIdCount = new Map();
@@ -122,7 +118,7 @@ class LayerListContainer extends React.Component {
return groups
}
toggleLayerGroup(groupPrefix, idx) {
toggleLayerGroup(groupPrefix: string, idx: number) {
const lookupKey = [groupPrefix, idx].join('-')
const newGroups = { ...this.state.collapsedGroups }
if(lookupKey in this.state.collapsedGroups) {
@@ -135,12 +131,12 @@ class LayerListContainer extends React.Component {
})
}
isCollapsed(groupPrefix, idx) {
isCollapsed(groupPrefix: string, idx: number) {
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')]
return collapsed === undefined ? true : collapsed
}
shouldComponentUpdate (nextProps, nextState) {
shouldComponentUpdate (nextProps: LayerListContainerProps, nextState: LayerListContainerState) {
// Always update on state change
if (this.state !== nextState) {
return true;
@@ -148,8 +144,8 @@ class LayerListContainer extends React.Component {
// This component tree only requires id and visibility from the layers
// objects
function getRequiredProps (layer) {
const out = {
function getRequiredProps(layer: LayerSpecification) {
const out: {id: string, layout?: { visibility: any}} = {
id: layer.id,
};
@@ -165,10 +161,10 @@ class LayerListContainer extends React.Component {
this.props.layers.map(getRequiredProps),
);
function withoutLayers (props) {
function withoutLayers(props: LayerListContainerProps) {
const out = {
...props
};
} as LayerListContainerProps & { layers?: any };
delete out['layers'];
return out;
}
@@ -184,7 +180,7 @@ class LayerListContainer extends React.Component {
return propsChanged;
}
componentDidUpdate (prevProps) {
componentDidUpdate (prevProps: LayerListContainerProps) {
if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) {
const selectedItemNode = this.selectedItemRef.current;
if (selectedItemNode && selectedItemNode.node) {
@@ -207,7 +203,7 @@ class LayerListContainer extends React.Component {
render() {
const listItems = []
const listItems: JSX.Element[] = []
let idx = 0
const layersByGroup = this.groupedLayers();
layersByGroup.forEach(layers => {
@@ -235,7 +231,7 @@ class LayerListContainer extends React.Component {
);
});
const additionalProps = {};
const additionalProps: {ref?: React.RefObject<any>} = {};
if (idx === this.props.selectedLayerIndex) {
additionalProps.ref = this.selectedItemRef;
}
@@ -255,7 +251,7 @@ class LayerListContainer extends React.Component {
visibility={(layer.layout || {}).visibility}
isSelected={idx === this.props.selectedLayerIndex}
onLayerSelect={this.props.onLayerSelect}
onLayerDestroy={this.props.onLayerDestroy.bind(this)}
onLayerDestroy={this.props.onLayerDestroy?.bind(this)}
onLayerCopy={this.props.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
{...additionalProps}
@@ -265,22 +261,24 @@ class LayerListContainer extends React.Component {
})
})
const t = this.props.t;
return <section
className="maputnik-layer-list"
role="complementary"
aria-label="Layers list"
aria-label={t("Layers list")}
ref={this.scrollContainerRef}
>
<ModalAdd
key={this.state.keys.add}
layers={this.props.layers}
sources={this.props.sources}
isOpen={this.state.isOpen.add}
onOpenToggle={this.toggleModal.bind(this, 'add')}
onLayersChange={this.props.onLayersChange}
key={this.state.keys.add}
layers={this.props.layers}
sources={this.props.sources}
isOpen={this.state.isOpen.add}
onOpenToggle={this.toggleModal.bind(this, 'add')}
onLayersChange={this.props.onLayersChange}
/>
<header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">Layers</span>
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
<span className="maputnik-space" />
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
@@ -289,7 +287,11 @@ class LayerListContainer extends React.Component {
data-wd-key="skip-target-layer-list"
onClick={this.toggleLayers}
className="maputnik-button">
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
{this.state.areAllGroupsExpanded === true ?
t("Collapse")
:
t("Expand")
}
</button>
</div>
</div>
@@ -299,14 +301,14 @@ class LayerListContainer extends React.Component {
onClick={this.toggleModal.bind(this, 'add')}
data-wd-key="layer-list:add-layer"
className="maputnik-button maputnik-button-selected">
Add Layer
{t("Add Layer")}
</button>
</div>
</div>
</header>
<div
role="navigation"
aria-label="Layers list"
aria-label={t("Layers list")}
>
<ul className="maputnik-layer-list-container">
{listItems}
@@ -316,11 +318,21 @@ class LayerListContainer extends React.Component {
}
}
const LayerListContainerSortable = SortableContainer((props) => <LayerListContainer {...props} />)
// The next two lines have react-refresh/only-export-components disabled because they are
// internal components that are not intended to be used outside of this file.
// For some reason, the linter is not recognizing these components correctly.
// When these components are migrated to functional components, the HOCs will no longer be needed
// and the comments can be removed.
// eslint-disable-next-line react-refresh/only-export-components
const LayerListContainer = withTranslation()(LayerListContainerInternal);
// eslint-disable-next-line react-refresh/only-export-components
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
export default class LayerList extends React.Component {
static propTypes = {...layerListPropTypes}
type LayerListProps = LayerListContainerProps & {
onMoveLayer: SortEndHandler
};
export default class LayerList extends React.Component<LayerListProps> {
render() {
return <LayerListContainerSortable
{...this.props}
@@ -8,7 +8,12 @@ import IconLayer from './IconLayer'
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
const DraggableLabel = SortableHandle((props) => {
type DraggableLabelProps = {
layerId: string
layerType: string
};
const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
return <div className="maputnik-layer-list-item-handle">
<IconLayer
className="layer-handle__icon"
@@ -20,21 +25,21 @@ const DraggableLabel = SortableHandle((props) => {
</div>
});
class IconAction extends React.Component {
static propTypes = {
action: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
wdKey: PropTypes.string,
classBlockName: PropTypes.string,
classBlockModifier: PropTypes.string,
}
type IconActionProps = {
action: string
onClick(...args: unknown[]): unknown
wdKey?: string
classBlockName?: string
classBlockModifier?: string
};
class IconAction extends React.Component<IconActionProps> {
renderIcon() {
switch(this.props.action) {
case 'duplicate': return <MdContentCopy />
case 'show': return <MdVisibility />
case 'hide': return <MdVisibilityOff />
case 'delete': return <MdDelete />
case 'duplicate': return <MdContentCopy />
case 'show': return <MdVisibility />
case 'hide': return <MdVisibilityOff />
case 'delete': return <MdDelete />
}
}
@@ -51,7 +56,7 @@ class IconAction extends React.Component {
}
return <button
tabIndex="-1"
tabIndex={-1}
title={this.props.action}
className={`maputnik-layer-list-icon-action ${classAdditions}`}
data-wd-key={this.props.wdKey}
@@ -63,21 +68,21 @@ class IconAction extends React.Component {
}
}
class LayerListItem extends React.Component {
static propTypes = {
layerIndex: PropTypes.number.isRequired,
layerId: PropTypes.string.isRequired,
layerType: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
visibility: PropTypes.string,
className: PropTypes.string,
onLayerSelect: PropTypes.func.isRequired,
onLayerCopy: PropTypes.func,
onLayerDestroy: PropTypes.func,
onLayerVisibilityToggle: PropTypes.func,
}
type LayerListItemProps = {
id?: string
layerIndex: number
layerId: string
layerType: string
isSelected?: boolean
visibility?: string
className?: string
onLayerSelect(...args: unknown[]): unknown
onLayerCopy?(...args: unknown[]): unknown
onLayerDestroy?(...args: unknown[]): unknown
onLayerVisibilityToggle?(...args: unknown[]): unknown
};
class LayerListItem extends React.Component<LayerListItemProps> {
static defaultProps = {
isSelected: false,
visibility: 'visible',
@@ -102,38 +107,38 @@ class LayerListItem extends React.Component {
return <li
id={this.props.id}
key={this.props.layerId}
onClick={e => this.props.onLayerSelect(this.props.layerIndex)}
onClick={_e => this.props.onLayerSelect(this.props.layerIndex)}
data-wd-key={"layer-list-item:"+this.props.layerId}
className={classnames({
"maputnik-layer-list-item": true,
"maputnik-layer-list-item-selected": this.props.isSelected,
[this.props.className]: true,
[this.props.className!]: true,
})}>
<DraggableLabel {...this.props} />
<span style={{flexGrow: 1}} />
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
action={'delete'}
classBlockName="delete"
onClick={e => this.props.onLayerDestroy(this.props.layerIndex)}
/>
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
action={'duplicate'}
classBlockName="duplicate"
onClick={e => this.props.onLayerCopy(this.props.layerIndex)}
/>
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
action={visibilityAction}
classBlockName="visibility"
classBlockModifier={visibilityAction}
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerIndex)}
/>
<DraggableLabel {...this.props} />
<span style={{flexGrow: 1}} />
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
action={'delete'}
classBlockName="delete"
onClick={_e => this.props.onLayerDestroy!(this.props.layerIndex)}
/>
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
action={'duplicate'}
classBlockName="duplicate"
onClick={_e => this.props.onLayerCopy!(this.props.layerIndex)}
/>
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
action={visibilityAction}
classBlockName="visibility"
classBlockModifier={visibilityAction}
onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
/>
</li>
}
}
const LayerListItemSortable = SortableElement((props) => <LayerListItem {...props} />);
const LayerListItemSortable = SortableElement<LayerListItemProps>((props: LayerListItemProps) => <LayerListItem {...props} />);
export default LayerListItemSortable;
-247
View File
@@ -1,247 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import MapLibreGl from 'maplibre-gl'
import MapboxInspect from 'mapbox-gl-inspect'
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'
import MapMaplibreGlFeaturePropertyPopup from './MapMaplibreGlFeaturePropertyPopup'
import tokens from '../config/tokens.json'
import colors from 'mapbox-gl-inspect/lib/colors'
import Color from 'color'
import ZoomControl from '../libs/zoomcontrol'
import { colorHighlightedLayer } from '../libs/highlight'
import 'maplibre-gl/dist/maplibre-gl.css'
import '../maplibregl.css'
import '../libs/maplibre-rtl'
const IS_SUPPORTED = MapLibreGl.supported();
function renderPopup(popup, mountNode) {
ReactDOM.render(popup, mountNode);
return mountNode;
}
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
const backgroundLayer = {
"id": "background",
"type": "background",
"paint": {
"background-color": '#1c1f24',
}
}
const layer = colorHighlightedLayer(highlightedLayer)
if(layer) {
coloredLayers.push(layer)
}
const sources = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster' && source.type !== 'raster-dem') {
sources[sourceId] = source
}
})
const inspectStyle = {
...originalMapStyle,
sources: sources,
layers: [backgroundLayer].concat(coloredLayers)
}
return inspectStyle
}
export default class MapMaplibreGl extends React.Component {
static propTypes = {
onDataChange: PropTypes.func,
onLayerSelect: PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
inspectModeEnabled: PropTypes.bool.isRequired,
highlightedLayer: PropTypes.object,
options: PropTypes.object,
replaceAccessTokens: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
}
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
onLayerSelect: () => {},
onChange: () => {},
options: {},
}
constructor(props) {
super(props)
this.state = {
map: null,
inspect: null,
}
}
updateMapFromProps(props) {
if(!IS_SUPPORTED) return;
if(!this.state.map) return
//Maplibre GL now does diffing natively so we don't need to calculate
//the necessary operations ourselves!
this.state.map.setStyle(
this.props.replaceAccessTokens(props.mapStyle),
{diff: true}
)
}
shouldComponentUpdate(nextProps, nextState) {
let should = false;
try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
} catch(e) {
// no biggie, carry on
}
return should;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if(!IS_SUPPORTED) return;
const map = this.state.map;
this.updateMapFromProps(this.props);
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
// eslint-disable-next-line
this.state.inspect._popupBlocked = false;
this.state.inspect.toggleInspector()
}
if (map) {
if (this.props.inspectModeEnabled) {
// HACK: We need to work out why we need to do this and what's causing
// this error. I'm assuming an issue with maplibre-gl update and
// mapbox-gl-inspect.
try {
this.state.inspect.render();
} catch(err) {
console.error("FIXME: Caught error", err);
}
}
map.showTileBoundaries = this.props.options.showTileBoundaries;
map.showCollisionBoxes = this.props.options.showCollisionBoxes;
map.showOverdrawInspector = this.props.options.showOverdrawInspector;
}
}
componentDidMount() {
if(!IS_SUPPORTED) return;
const mapOpts = {
...this.props.options,
container: this.container,
style: this.props.mapStyle,
hash: true,
maxZoom: 24
}
const map = new MapLibreGl.Map(mapOpts);
const mapViewChange = () => {
const center = map.getCenter();
const zoom = map.getZoom();
this.props.onChange({center, zoom});
}
mapViewChange();
map.showTileBoundaries = mapOpts.showTileBoundaries;
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
map.showOverdrawInspector = mapOpts.showOverdrawInspector;
const zoomControl = new ZoomControl;
map.addControl(zoomControl, 'top-right');
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
map.addControl(nav, 'top-right');
const tmpNode = document.createElement('div');
const inspect = new MapboxInspect({
popup: new MapLibreGl.Popup({
closeOnClick: false
}),
showMapPopup: true,
showMapPopupOnHover: false,
showInspectMapPopupOnHover: true,
showInspectButton: false,
blockHoverPopupOnClick: true,
assignLayerColor: (layerId, alpha) => {
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
},
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
renderPopup: features => {
if(this.props.inspectModeEnabled) {
return renderPopup(<MapMaplibreGlFeaturePropertyPopup features={features} />, tmpNode);
} else {
return renderPopup(<MapMaplibreGlLayerPopup features={features} onLayerSelect={this.onLayerSelectById} zoom={this.state.zoom} />, tmpNode);
}
}
})
map.addControl(inspect)
map.on("style.load", () => {
this.setState({
map,
inspect,
zoom: map.getZoom()
});
})
map.on("data", e => {
if(e.dataType !== 'tile') return
this.props.onDataChange({
map: this.state.map
})
})
map.on("error", e => {
console.log("ERROR", e);
})
map.on("zoom", e => {
this.setState({
zoom: map.getZoom()
});
});
map.on("dragend", mapViewChange);
map.on("zoomend", mapViewChange);
}
onLayerSelectById = (id) => {
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id);
this.props.onLayerSelect(index);
}
render() {
if(IS_SUPPORTED) {
return <div
className="maputnik-map__map"
role="region"
aria-label="Map view"
ref={x => this.container = x}
data-wd-key="maplibre:map"
></div>
}
else {
return <div
className="maputnik-map maputnik-map--error"
>
<div className="maputnik-map__error-message">
Error: Cannot load MaplibreGL, WebGL is either unsupported or disabled
</div>
</div>
}
}
}
+279
View File
@@ -0,0 +1,279 @@
import React, {type JSX} from 'react'
import ReactDOM from 'react-dom'
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
import MaplibreInspect from '@maplibre/maplibre-gl-inspect'
import colors from '@maplibre/maplibre-gl-inspect/lib/colors'
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'
import MapMaplibreGlFeaturePropertyPopup, { InspectFeature } from './MapMaplibreGlFeaturePropertyPopup'
import Color from 'color'
import ZoomControl from '../libs/zoomcontrol'
import { HighlightedLayer, colorHighlightedLayer } from '../libs/highlight'
import 'maplibre-gl/dist/maplibre-gl.css'
import '../maplibregl.css'
import '../libs/maplibre-rtl'
import MaplibreGeocoder, { MaplibreGeocoderApi, MaplibreGeocoderApiConfig } from '@maplibre/maplibre-gl-geocoder';
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
import { withTranslation, WithTranslation } from 'react-i18next'
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
ReactDOM.render(popup, mountNode);
return mountNode as HTMLElement;
}
function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) {
const backgroundLayer = {
"id": "background",
"type": "background",
"paint": {
"background-color": '#1c1f24',
}
} as LayerSpecification
const layer = colorHighlightedLayer(highlightedLayer)
if(layer) {
coloredLayers.push(layer)
}
const sources: {[key:string]: SourceSpecification} = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster' && source.type !== 'raster-dem') {
sources[sourceId] = source
}
})
const inspectStyle = {
...originalMapStyle,
sources: sources,
layers: [backgroundLayer].concat(coloredLayers as LayerSpecification[])
}
return inspectStyle
}
type MapMaplibreGlInternalProps = {
onDataChange?(event: {map: Map | null}): unknown
onLayerSelect(...args: unknown[]): unknown
mapStyle: StyleSpecification
inspectModeEnabled: boolean
highlightedLayer?: HighlightedLayer
options?: Partial<MapOptions> & {
showTileBoundaries?: boolean
showCollisionBoxes?: boolean
showOverdrawInspector?: boolean
}
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
onChange(value: {center: LngLat, zoom: number}): unknown
} & WithTranslation;
type MapMaplibreGlState = {
map: Map | null
inspect: MaplibreInspect | null
zoom?: number
};
class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> {
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
onLayerSelect: () => {},
onChange: () => {},
options: {} as MapOptions,
}
container: HTMLDivElement | null = null
constructor(props: MapMaplibreGlInternalProps) {
super(props)
this.state = {
map: null,
inspect: null,
}
}
shouldComponentUpdate(nextProps: MapMaplibreGlInternalProps, nextState: MapMaplibreGlState) {
let should = false;
try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
} catch(e) {
// no biggie, carry on
}
return should;
}
componentDidUpdate() {
const map = this.state.map;
const styleWithTokens = this.props.replaceAccessTokens(this.props.mapStyle);
if (map) {
// Maplibre GL now does diffing natively so we don't need to calculate
// the necessary operations ourselves!
// We also need to update the style for inspect to work properly
map.setStyle(styleWithTokens, {diff: true});
map.showTileBoundaries = this.props.options?.showTileBoundaries!;
map.showCollisionBoxes = this.props.options?.showCollisionBoxes!;
map.showOverdrawInspector = this.props.options?.showOverdrawInspector!;
}
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
this.state.inspect.toggleInspector()
}
if (this.state.inspect && this.props.inspectModeEnabled) {
this.state.inspect.setOriginalStyle(styleWithTokens);
// In case the sources are the same, there's a need to refresh the style
setTimeout(() => {
this.state.inspect!.render();
}, 500);
}
}
componentDidMount() {
const mapOpts = {
...this.props.options,
container: this.container!,
style: this.props.mapStyle,
hash: true,
maxZoom: 24,
// setting to always load glyphs of CJK fonts from server
// https://maplibre.org/maplibre-gl-js/docs/examples/local-ideographs/
localIdeographFontFamily: false
} satisfies MapOptions;
const map = new MapLibreGl.Map(mapOpts);
const mapViewChange = () => {
const center = map.getCenter();
const zoom = map.getZoom();
this.props.onChange({center, zoom});
}
mapViewChange();
map.showTileBoundaries = mapOpts.showTileBoundaries!;
map.showCollisionBoxes = mapOpts.showCollisionBoxes!;
map.showOverdrawInspector = mapOpts.showOverdrawInspector!;
this.initGeocoder(map);
const zoomControl = new ZoomControl(this.props.t("Zoom:"));
map.addControl(zoomControl, 'top-right');
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
map.addControl(nav, 'top-right');
const tmpNode = document.createElement('div');
const inspect = new MaplibreInspect({
popup: new MapLibreGl.Popup({
closeOnClick: false
}),
showMapPopup: true,
showMapPopupOnHover: false,
showInspectMapPopupOnHover: true,
showInspectButton: false,
blockHoverPopupOnClick: true,
assignLayerColor: (layerId: string, alpha: number) => {
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
},
buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
renderPopup: (features: InspectFeature[]) => {
if(this.props.inspectModeEnabled) {
return renderPopup(<MapMaplibreGlFeaturePropertyPopup features={features} />, tmpNode);
} else {
return renderPopup(<MapMaplibreGlLayerPopup features={features} onLayerSelect={this.onLayerSelectById} zoom={this.state.zoom} />, tmpNode);
}
}
})
map.addControl(inspect)
map.on("style.load", () => {
this.setState({
map,
inspect,
zoom: map.getZoom()
});
})
map.on("data", e => {
if(e.dataType !== 'tile') return
this.props.onDataChange!({
map: this.state.map
})
})
map.on("error", e => {
console.log("ERROR", e);
})
map.on("zoom", _e => {
this.setState({
zoom: map.getZoom()
});
});
map.on("dragend", mapViewChange);
map.on("zoomend", mapViewChange);
}
onLayerSelectById = (id: string) => {
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id);
this.props.onLayerSelect(index);
}
initGeocoder(map: Map) {
const geocoderConfig = {
forwardGeocode: async (config: MaplibreGeocoderApiConfig) => {
const features = [];
try {
const request = `https://nominatim.openstreetmap.org/search?q=${config.query}&format=geojson&polygon_geojson=1&addressdetails=1`;
const response = await fetch(request);
const geojson = await response.json();
for (const feature of geojson.features) {
const center = [
feature.bbox[0] +
(feature.bbox[2] - feature.bbox[0]) / 2,
feature.bbox[1] +
(feature.bbox[3] - feature.bbox[1]) / 2
];
const point = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: center
},
place_name: feature.properties.display_name,
properties: feature.properties,
text: feature.properties.display_name,
place_type: ['place'],
center
};
features.push(point);
}
} catch (e) {
console.error(`Failed to forwardGeocode with error: ${e}`);
}
return {
features
};
},
} as unknown as MaplibreGeocoderApi;
const geocoder = new MaplibreGeocoder(geocoderConfig, {
placeholder: this.props.t("Search"),
maplibregl: MapLibreGl,
});
map.addControl(geocoder, 'top-left');
}
render() {
const t = this.props.t;
return <div
className="maputnik-map__map"
role="region"
aria-label={t("Map view")}
ref={x => this.container = x}
data-wd-key="maplibre:map"
></div>
}
}
const MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
export default MapMaplibreGl;
@@ -1,78 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import FieldString from './FieldString'
function displayValue(value) {
if (typeof value === 'undefined' || value === null) return value;
if (value instanceof Date) return value.toLocaleString();
if (typeof value === 'object' ||
typeof value === 'number' ||
typeof value === 'string') return value.toString();
return value;
}
function renderProperties(feature) {
return Object.keys(feature.properties).map(propertyName => {
const property = feature.properties[propertyName]
return <Block key={propertyName} label={propertyName}>
<FieldString value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
</Block>
})
}
function renderFeatureId(feature) {
return <Block key={"feature-id"} label={"feature_id"}>
<FieldString value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} />
</Block>
}
function renderFeature(feature, idx) {
return <div key={`${feature.sourceLayer}-${idx}`}>
<div className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
<Block key={"property-type"} label={"$type"}>
<FieldString value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
</Block>
{renderFeatureId(feature)}
{renderProperties(feature)}
</div>
}
function removeDuplicatedFeatures(features) {
let uniqueFeatures = [];
features.forEach(feature => {
const featureIndex = uniqueFeatures.findIndex(feature2 => {
return feature.layer['source-layer'] === feature2.layer['source-layer']
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
})
if(featureIndex === -1) {
uniqueFeatures.push(feature)
} else {
if(uniqueFeatures[featureIndex].hasOwnProperty('inspectModeCounter')) {
uniqueFeatures[featureIndex].inspectModeCounter++
} else {
uniqueFeatures[featureIndex].inspectModeCounter = 2
}
}
})
return uniqueFeatures
}
class FeaturePropertyPopup extends React.Component {
static propTypes = {
features: PropTypes.array
}
render() {
const features = removeDuplicatedFeatures(this.props.features)
return <div className="maputnik-feature-property-popup">
{features.map(renderFeature)}
</div>
}
}
export default FeaturePropertyPopup
@@ -0,0 +1,80 @@
import React from 'react'
import type { GeoJSONFeatureWithSourceLayer } from '@maplibre/maplibre-gl-inspect'
export type InspectFeature = GeoJSONFeatureWithSourceLayer & {
inspectModeCounter?: number
counter?: number
}
function displayValue(value: string | number | Date | object | undefined) {
if (typeof value === 'undefined' || value === null) return value;
if (value instanceof Date) return value.toLocaleString();
if (typeof value === 'object' ||
typeof value === 'number' ||
typeof value === 'string') return value.toString();
return value;
}
function renderKeyValueTableRow(key: string, value: string | undefined) {
return <tr key={key}>
<td className="maputnik-popup-table-cell">{key}</td>
<td className="maputnik-popup-table-cell">{value}</td>
</tr>
}
function renderFeature(feature: InspectFeature, idx: number) {
return <React.Fragment key={idx}>
<tr>
<td colSpan={2} className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</td>
</tr>
{renderKeyValueTableRow("$type", feature.geometry.type)}
{renderKeyValueTableRow("$id", displayValue(feature.id))}
{Object.keys(feature.properties).map(propertyName => {
const property = feature.properties[propertyName];
return renderKeyValueTableRow(propertyName, displayValue(property))
})}
</React.Fragment>
}
function removeDuplicatedFeatures(features: InspectFeature[]) {
const uniqueFeatures: InspectFeature[] = [];
features.forEach(feature => {
const featureIndex = uniqueFeatures.findIndex(feature2 => {
return feature.layer['source-layer'] === feature2.layer['source-layer']
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
})
if(featureIndex === -1) {
uniqueFeatures.push(feature)
} else {
if('inspectModeCounter' in uniqueFeatures[featureIndex]) {
uniqueFeatures[featureIndex].inspectModeCounter!++
} else {
uniqueFeatures[featureIndex].inspectModeCounter = 2
}
}
})
return uniqueFeatures
}
type FeaturePropertyPopupProps = {
features: InspectFeature[]
};
class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
render() {
const features = removeDuplicatedFeatures(this.props.features)
return <div className="maputnik-feature-property-popup">
<table className="maputnik-popup-table">
<tbody>
{features.map(renderFeature)}
</tbody>
</table>
</div>
}
}
export default FeaturePropertyPopup
+26 -32
View File
@@ -1,21 +1,23 @@
import React from 'react'
import IconLayer from './IconLayer'
import type {InspectFeature} from './MapMaplibreGlFeaturePropertyPopup';
function groupFeaturesBySourceLayer(features: any[]) {
const sources = {} as any
function groupFeaturesBySourceLayer(features: InspectFeature[]) {
const sources: {[key: string]: InspectFeature[]} = {}
let returnedFeatures = {} as any;
const returnedFeatures: {[key: string]: number} = {}
features.forEach(feature => {
if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
const sourceKey = feature.layer['source-layer'] as string;
if(Object.prototype.hasOwnProperty.call(returnedFeatures, feature.layer.id)) {
returnedFeatures[feature.layer.id]++
const featureObject = sources[feature.layer['source-layer']].find((f: any) => f.layer.id === feature.layer.id)
const featureObject = sources[sourceKey].find((f: InspectFeature) => f.layer.id === feature.layer.id)
featureObject.counter = returnedFeatures[feature.layer.id]
featureObject!.counter = returnedFeatures[feature.layer.id]
} else {
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
sources[feature.layer['source-layer']].push(feature)
sources[sourceKey] = sources[sourceKey] || []
sources[sourceKey].push(feature)
returnedFeatures[feature.layer.id] = 1
}
@@ -25,13 +27,13 @@ function groupFeaturesBySourceLayer(features: any[]) {
}
type FeatureLayerPopupProps = {
onLayerSelect(...args: unknown[]): unknown
features: any[]
onLayerSelect(layerId: string): unknown
features: InspectFeature[]
zoom?: number
};
class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
_getFeatureColor(feature: any, _zoom?: number) {
_getFeatureColor(feature: InspectFeature, _zoom?: number) {
// Guard because openlayers won't have this
if (!feature.layer.paint) {
return;
@@ -39,29 +41,21 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
try {
const paintProps = feature.layer.paint;
let propName;
if(paintProps.hasOwnProperty("text-color") && paintProps["text-color"]) {
propName = "text-color";
if("text-color" in paintProps && paintProps["text-color"]) {
return String(paintProps["text-color"]);
}
else if (paintProps.hasOwnProperty("fill-color") && paintProps["fill-color"]) {
propName = "fill-color";
if ("fill-color" in paintProps && paintProps["fill-color"]) {
return String(paintProps["fill-color"]);
}
else if (paintProps.hasOwnProperty("line-color") && paintProps["line-color"]) {
propName = "line-color";
if ("line-color" in paintProps && paintProps["line-color"]) {
return String(paintProps["line-color"]);
}
else if (paintProps.hasOwnProperty("fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
propName = "fill-extrusion-color";
}
if(propName) {
let color = feature.layer.paint[propName];
return String(color);
}
else {
// Default color
return "black";
if ("fill-extrusion-color" in paintProps && paintProps["fill-extrusion-color"]) {
return String(paintProps["fill-extrusion-color"]);
}
// Default color
return "black";
}
// This is quite complex, just incase there's an edgecase we're missing
// always return black if we get an unexpected error.
@@ -75,12 +69,12 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
const sources = groupFeaturesBySourceLayer(this.props.features)
const items = Object.keys(sources).map(vectorLayerId => {
const layers = sources[vectorLayerId].map((feature: any, idx: number) => {
const layers = sources[vectorLayerId].map((feature: InspectFeature, idx: number) => {
const featureColor = this._getFeatureColor(feature, this.props.zoom);
return <div
key={idx}
className="maputnik-popup-layer"
key={idx}
className="maputnik-popup-layer"
>
<div
className="maputnik-popup-layer__swatch"
+18 -13
View File
@@ -1,14 +1,16 @@
import React from 'react'
import {throttle} from 'lodash';
import { WithTranslation, withTranslation } from 'react-i18next';
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup';
import 'ol/ol.css'
//@ts-ignore
import {apply} from 'ol-mapbox-style';
import {Map, View, Overlay} from 'ol';
import {toLonLat} from 'ol/proj';
import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec';
import type {StyleSpecification} from 'maplibre-gl';
function renderCoords (coords: string[]) {
@@ -22,7 +24,7 @@ function renderCoords (coords: string[]) {
}
}
type MapOpenLayersProps = {
type MapOpenLayersInternalProps = {
onDataChange?(...args: unknown[]): unknown
mapStyle: object
accessToken?: string
@@ -31,7 +33,7 @@ type MapOpenLayersProps = {
debugToolbox: boolean
replaceAccessTokens(...args: unknown[]): unknown
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
type MapOpenLayersState = {
zoom: string
@@ -41,7 +43,7 @@ type MapOpenLayersState = {
selectedFeatures?: any[]
};
export default class MapOpenLayers extends React.Component<MapOpenLayersProps, MapOpenLayersState> {
class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps, MapOpenLayersState> {
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
@@ -53,7 +55,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
overlay: Overlay | undefined;
popupContainer: HTMLElement | null = null;
constructor(props: MapOpenLayersProps) {
constructor(props: MapOpenLayersInternalProps) {
super(props);
this.state = {
zoom: "0",
@@ -72,7 +74,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
apply(this.map, newMapStyle);
}
componentDidUpdate(prevProps: MapOpenLayersProps) {
componentDidUpdate(prevProps: MapOpenLayersInternalProps) {
if (this.props.mapStyle !== prevProps.mapStyle) {
this.updateStyle(
this.props.replaceAccessTokens(this.props.mapStyle)
@@ -99,7 +101,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
});
map.on('pointermove', (evt) => {
var coords = toLonLat(evt.coordinate);
const coords = toLonLat(evt.coordinate);
this.setState({
cursor: [
coords[0].toFixed(2),
@@ -150,6 +152,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
}
render() {
const t = this.props.t;
return <div className="maputnik-ol-container">
<div
ref={x => this.popupContainer = x}
@@ -159,7 +162,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
<button
className="maplibregl-popup-close-button"
onClick={this.closeOverlay}
aria-label="Close popup"
aria-label={t("Close popup")}
>
×
</button>
@@ -169,20 +172,20 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
/>
</div>
<div className="maputnik-ol-zoom">
Zoom: {this.state.zoom}
{t("Zoom:")} {this.state.zoom}
</div>
{this.props.debugToolbox &&
<div className="maputnik-ol-debug">
<div>
<label>cursor: </label>
<label>{t("cursor:")} </label>
<span>{renderCoords(this.state.cursor)}</span>
</div>
<div>
<label>center: </label>
<label>{t("center:")} </label>
<span>{renderCoords(this.state.center)}</span>
</div>
<div>
<label>rotation: </label>
<label>{t("rotation:")} </label>
<span>{this.state.rotation}</span>
</div>
</div>
@@ -191,7 +194,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
className="maputnik-ol"
ref={x => this.container = x}
role="region"
aria-label="Map view"
aria-label={t("Map view")}
style={{
...this.props.style,
}}>
@@ -200,3 +203,5 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
}
}
const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
export default MapOpenLayers;
+11 -8
View File
@@ -1,26 +1,26 @@
import React from 'react'
import React, { PropsWithChildren } from 'react'
import {MdClose} from 'react-icons/md'
import AriaModal from 'react-aria-modal'
import classnames from 'classnames';
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalProps = {
type ModalInternalProps = PropsWithChildren & {
"data-wd-key"?: string
isOpen: boolean
title: string
onOpenToggle(...args: unknown[]): unknown
onOpenToggle(value: boolean): unknown
underlayClickExits?: boolean
underlayProps?: any
className?: string
};
} & WithTranslation;
export default class Modal extends React.Component<ModalProps> {
class ModalInternal extends React.Component<ModalInternalProps> {
static defaultProps = {
underlayClickExits: true
}
// See <https://github.com/maputnik/editor/issues/416>
// See <https://github.com/maplibre/maputnik/issues/416>
onClose = () => {
if (document.activeElement) {
(document.activeElement as HTMLElement).blur();
@@ -32,6 +32,7 @@ export default class Modal extends React.Component<ModalProps> {
}
render() {
const t = this.props.t;
if(this.props.isOpen) {
return <AriaModal
titleText={this.props.title}
@@ -49,7 +50,7 @@ export default class Modal extends React.Component<ModalProps> {
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
<span className="maputnik-modal-header-space"></span>
<button className="maputnik-modal-header-toggle"
title="Close modal"
title={t("Close modal")}
onClick={this.onClose}
data-wd-key={this.props["data-wd-key"]+".close-modal"}
>
@@ -68,3 +69,5 @@ export default class Modal extends React.Component<ModalProps> {
}
}
const Modal = withTranslation()(ModalInternal);
export default Modal;
+42 -37
View File
@@ -6,24 +6,26 @@ import FieldType from './FieldType'
import FieldId from './FieldId'
import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer'
import type {LayerSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalAddProps = {
layers: unknown[]
onLayersChange(...args: unknown[]): unknown
type ModalAddInternalProps = {
layers: LayerSpecification[]
onLayersChange(layers: LayerSpecification[]): unknown
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
onOpenToggle(open: boolean): unknown
// A dict of source id's and the available source layers
sources: any
};
} & WithTranslation;
type ModalAddState = {
type: string
type: LayerSpecification["type"]
id: string
source?: string
'source-layer'?: string
};
export default class ModalAdd extends React.Component<ModalAddProps, ModalAddState> {
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
addLayer = () => {
const changedLayers = this.props.layers.slice(0)
const layer: ModalAddState = {
@@ -38,13 +40,13 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
}
}
changedLayers.push(layer)
changedLayers.push(layer as LayerSpecification)
this.props.onLayersChange(changedLayers)
this.props.onOpenToggle(false)
}
constructor(props: ModalAddProps) {
constructor(props: ModalAddInternalProps) {
super(props)
const state: ModalAddState = {
type: 'fill',
@@ -53,12 +55,12 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
if(props.sources.length > 0) {
state.source = Object.keys(this.props.sources)[0];
state['source-layer'] = this.props.sources[state.source as keyof ModalAddProps["sources"]][0]
state['source-layer'] = this.props.sources[state.source as keyof ModalAddInternalProps["sources"]][0]
}
this.state = state;
}
componentDidUpdate(_prevProps: ModalAddProps, prevState: ModalAddState) {
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
// Check if source is valid for new type
const oldType = prevState.type;
const newType = this.state.type;
@@ -67,7 +69,7 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
const availableSourcesNew = this.getSources(newType);
if(
// Type has changed
// Type has changed
oldType !== newType
&& prevState.source !== ""
// Was a valid source previously
@@ -112,7 +114,7 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
]
}
for(let [key, val] of Object.entries(this.props.sources) as any) {
for(const [key, val] of Object.entries(this.props.sources) as any) {
const valType = val.type as keyof typeof types;
if(types[valType] && types[valType].indexOf(type) > -1) {
sources.push(key);
@@ -124,54 +126,57 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
render() {
const t = this.props.t;
const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source!);
return <Modal
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Add Layer'}
title={t('Add Layer')}
data-wd-key="modal:add-layer"
className="maputnik-add-modal"
>
<div className="maputnik-add-layer">
<FieldId
value={this.state.id}
wdKey="add-layer.layer-id"
onChange={(v: string) => {
this.setState({ id: v })
}}
/>
<FieldType
value={this.state.type}
wdKey="add-layer.layer-type"
onChange={(v: string) => this.setState({ type: v })}
/>
{this.state.type !== 'background' &&
<FieldId
value={this.state.id}
wdKey="add-layer.layer-id"
onChange={(v: string) => {
this.setState({ id: v })
}}
/>
<FieldType
value={this.state.type}
wdKey="add-layer.layer-type"
onChange={(v: LayerSpecification["type"]) => this.setState({ type: v })}
/>
{this.state.type !== 'background' &&
<FieldSource
sourceIds={sources}
wdKey="add-layer.layer-source-block"
value={this.state.source}
onChange={(v: string) => this.setState({ source: v })}
/>
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
<FieldSourceLayer
isFixed={true}
sourceLayerIds={layers}
value={this.state['source-layer']}
onChange={(v: string) => this.setState({ 'source-layer': v })}
/>
}
<InputButton
className="maputnik-add-layer-button"
onClick={this.addLayer}
data-wd-key="add-layer"
>
Add Layer
</InputButton>
}
<InputButton
className="maputnik-add-layer-button"
onClick={this.addLayer}
data-wd-key="add-layer"
>
{t("Add Layer")}
</InputButton>
</div>
</Modal>
}
}
const ModalAdd = withTranslation()(ModalAddInternal);
export default ModalAdd;
+26 -21
View File
@@ -1,48 +1,49 @@
import React from 'react'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal'
type ModalDebugProps = {
type ModalDebugInternalProps = {
isOpen: boolean
renderer: string
onChangeMaboxGlDebug(...args: unknown[]): unknown
onChangeOpenlayersDebug(...args: unknown[]): unknown
onOpenToggle(...args: unknown[]): unknown
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
onChangeOpenlayersDebug(key: string, checked: boolean): unknown
onOpenToggle(value: boolean): unknown
maplibreGlDebugOptions?: object
openlayersDebugOptions?: object
mapView: {
zoom: number
center: {
lng: string
lat: string
lng: number
lat: number
}
}
};
} & WithTranslation;
export default class ModalDebug extends React.Component<ModalDebugProps> {
class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
render() {
const {mapView} = this.props;
const {t, mapView} = this.props;
const osmZoom = Math.round(mapView.zoom)+1;
const osmLon = Number.parseFloat(mapView.center.lng).toFixed(5);
const osmLat = Number.parseFloat(mapView.center.lat).toFixed(5);
const osmLon = +(mapView.center.lng).toFixed(5);
const osmLat = +(mapView.center.lat).toFixed(5);
return <Modal
data-wd-key="modal:debug"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Debug'}
title={t('Debug')}
>
<section className="maputnik-modal-section maputnik-modal-shortcuts">
<h1>Options</h1>
<h1>{t("Options")}</h1>
{this.props.renderer === 'mlgljs' &&
<ul>
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
return <li key={key}>
<label>
<input type="checkbox" checked={val} onChange={(e) => this.props.onChangeMaboxGlDebug(key, e.target.checked)} /> {key}
<input type="checkbox" checked={val} onChange={(e) => this.props.onChangeMaplibreGlDebug(key, e.target.checked)} /> {key}
</label>
</li>
})}
@@ -63,16 +64,20 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
<section className="maputnik-modal-section">
<h1>Links</h1>
<p>
<a
target="_blank"
rel="noopener noreferrer"
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
>
Open in OSM
</a> &mdash; Opens the current view on openstreetmap.org
<Trans t={t}>
<a
target="_blank"
rel="noopener noreferrer"
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
>
Open in OSM
</a> &mdash; Opens the current view on openstreetmap.org
</Trans>
</p>
</section>
</Modal>
}
}
const ModalDebug = withTranslation()(ModalDebugInternal);
export default ModalDebug;

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