Compare commits

...

130 Commits

Author SHA1 Message Date
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="8bd704ec0d"><code>8bd704e</code></a>
4.0.8</li>
<li><a
href="a0e68416a4"><code>a0e6841</code></a>
run verb to generate README documentation</li>
<li><a
href="4ec288484f"><code>4ec2884</code></a>
Merge branch 'v4' into hauserkristof-feature/v4.0.8</li>
<li><a
href="03aa805217"><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="814f5f70ef"><code>814f5f7</code></a>
lint</li>
<li><a
href="67fcce6a10"><code>67fcce6</code></a>
fix: CHANGELOG about braces &amp; CVE-2024-4068, v4.0.5</li>
<li><a
href="113f2e3fa7"><code>113f2e3</code></a>
fix: CVE numbers in CHANGELOG</li>
<li><a
href="d9dbd9a266"><code>d9dbd9a</code></a>
feat: updated CHANGELOG</li>
<li><a
href="2ab13157f4"><code>2ab1315</code></a>
fix: use actions/setup-node@v4</li>
<li><a
href="1406ea38f3"><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](6e536ff007)
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="74b2db2938"><code>74b2db2</code></a>
3.0.3</li>
<li><a
href="88f1429a0f"><code>88f1429</code></a>
update eslint. lint, fix unit tests.</li>
<li><a
href="415d660c30"><code>415d660</code></a>
Snyk js braces 6838727 (<a
href="https://redirect.github.com/micromatch/braces/issues/40">#40</a>)</li>
<li><a
href="190510f79d"><code>190510f</code></a>
fix tests, skip 1 test in test/braces.expand</li>
<li><a
href="716eb9f12d"><code>716eb9f</code></a>
readme bump</li>
<li><a
href="a5851e57f4"><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="2092bd1fb1"><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="9f5b4cf473"><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="98414f9f1f"><code>98414f9</code></a>
remove funding file</li>
<li><a
href="665ab5d561"><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="7369016d8a"><code>7369016</code></a>
release: v5.2.6</li>
<li><a
href="ba5269cca8"><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="7a2791ce17"><code>7a2791c</code></a>
release: v5.2.5</li>
<li><a
href="7093f779b7"><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="3a73e485cd"><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="6a07243a0e"><code>6a07243</code></a>
release: v5.2.4</li>
<li><a
href="fbf69d5f6c"><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="a67f9f6945"><code>a67f9f6</code></a>
release: v5.2.3</li>
<li><a
href="8617e7638e"><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="f184c8032b"><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](bc5ecfade6/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="ee81e19676"><code>ee81e19</code></a>
release: v5.0.12</li>
<li><a
href="91641c4da0"><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="ef89f8092f"><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="b44c49302f"><code>b44c493</code></a>
release: v5.0.11</li>
<li><a
href="d2aa0969ee"><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="2a540eee82"><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="5ea9edbc9c"><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="49d21fe1fe"><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="8de7bd2b68"><code>8de7bd2</code></a>
fix(assets): avoid splitting <code>,</code> inside base64 value of
<code>srcset</code> attribute (#...</li>
<li><a
href="19e3c9a8a1"><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
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
153 changed files with 5876 additions and 13448 deletions

View File

@@ -1,4 +0,0 @@
{
"packages": [],
"sandboxes": ["/"]
}

11
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
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.

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
- run: docker build -t test-docker-image-build .
# build the editor
build-node:
@@ -53,52 +53,40 @@ jobs:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- run: npm run build-storybook
- name: artifacts/editor
uses: actions/upload-artifact@v1
- name: artifacts/maputnik
uses: actions/upload-artifact@v4
with:
name: editor
name: maputnik
path: dist
- name: artifacts/storybook
uses: actions/upload-artifact@v1
with:
name: storybook
path: build/storybook
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: ^1.19.x
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v4
with:
repository: maputnik/desktop
ref: master
path: ./src/github.com/maputnik/desktop/
- name: Make
run: cd src/github.com/maputnik/desktop/ && make
- name: Build desktop artifacts
run: npm run build-desktop
- name: Artifacts/linux
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: maputnik-linux
path: ./src/github.com/maputnik/desktop/bin/linux/
path: ./desktop/bin/linux/
- name: Artifacts/darwin
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: maputnik-darwin
path: ./src/github.com/maputnik/desktop/bin/darwin/
path: ./desktop/bin/darwin/
- name: Artifacts/windows
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: maputnik-windows
path: ./src/github.com/maputnik/desktop/bin/windows/
path: ./desktop/bin/windows/
e2e-tests:
name: "E2E tests using ${{ matrix.browser }}"

View File

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

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
.github/workflows/release.yml vendored Normal file
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
.gitignore vendored
View File

@@ -33,6 +33,6 @@ node_modules
public
/errorShots
/old
/build
/cypress/screenshots
/dist/
/desktop/version.go

1
.npmrc Normal file
View File

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

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;

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,
});

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
CHANGELOG.md Normal file
View File

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

View File

@@ -2,12 +2,12 @@ FROM node:18 as builder
WORKDIR /maputnik
# Only copy package.json to prevent npm install from running on every build
COPY package.json package-lock.json ./
RUN npm install
COPY package.json package-lock.json .npmrc ./
RUN npm ci
# Build maputnik
COPY . .
RUN npm run build
RUN npx vite build
#---------------------------------------------------------------------------
# Create a clean nginx-alpine slim image with just the build results

View File

@@ -1,5 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015 Lukas Martinelli
Copyright (c) 2024 MapLibre contributors
Permission is hereby granted, free of charge, to any person obtaining a copy

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/)
@@ -14,18 +14,18 @@ targeted at developers and map designers.
## Usage
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
- :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
```
## 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)
@@ -66,7 +66,8 @@ Lint the JavaScript code.
```
# run linter
npm run lint
npm run lint-styles
npm run lint-css
npm run sort-styles
```
@@ -93,10 +94,15 @@ 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

11
build/README.md Normal file
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
build/bump-version-changelog.js Executable file
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
build/release-notes.js Executable file
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());

View File

@@ -87,4 +87,39 @@ describe("history", () => {
],
});
});
it("should not redo after undo and value change", () => {
when.setStyle("geojson");
when.modal.open();
when.modal.fillLayers({
id: "step 1",
type: "background",
});
when.modal.open();
when.modal.fillLayers({
id: "step 2",
type: "background",
});
when.typeKeys(undoKeyCombo);
when.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.modal.open();
when.modal.fillLayers({
id: "step 3",
type: "background",
});
when.typeKeys(redoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 3",
type: "background",
},
],
});
});
});

35
cypress/e2e/i18n.cy.ts Normal file
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("スタイル設定");
});
});
});

View File

@@ -280,6 +280,25 @@ describe("layers", () => {
});
});
});
describe("opacity", () => {
let bgId: string;
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.type("spec-field-input:background-opacity", "0.");
});
it("should keep '.' in the input field", () => {
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
});
it("should revert to a valid value when focus out", () => {
when.click("layer-list-item:background:" + bgId);
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue('0');
});
});
});
describe("filter", () => {

View File

@@ -23,4 +23,10 @@ describe("map", () => {
);
});
});
describe("search", () => {
it('should exist', () => {
then(get.searchControl()).shouldBeVisible();
});
});
});

View File

@@ -1,3 +1,5 @@
/// <reference types="cypress-plugin-tab" />
import { CypressHelper } from "@shellygo/cypress-test-utils";
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
import MaputnikCypressHelper from "./maputnik-cypress-helper";
@@ -14,7 +16,7 @@ const styleFromWindow = (win: Window) => {
export class MaputnikAssertable<T> extends Assertable<T> {
shouldEqualToStoredStyle = () =>
then(
new CypressHelper().get.window().then((win) => {
new CypressHelper().get.window().then((win: Window) => {
const style = styleFromWindow(win);
then(this.chainable).shouldDeepNestedInclude(style);
})
@@ -129,7 +131,9 @@ export class MaputnikDriver {
this.helper.when.acceptConfirm();
}
// when methods should not include assertions
this.helper.get.elementByTestId("toolbar:link").should("be.visible");
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
toolbarLink.scrollIntoView();
toolbarLink.should("be.visible");
},
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
@@ -177,5 +181,6 @@ export class MaputnikDriver {
skipTargetLayerEditor: () =>
this.helper.get.elementByTestId("skip-target-layer-editor"),
canvas: () => this.helper.get.element("canvas"),
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
};
}

View File

@@ -79,12 +79,12 @@ describe("modals", () => {
when.click("nav:settings");
});
describe("when click name", () => {
describe("when click name filed spec information", () => {
beforeEach(() => {
when.click("field-doc-button-Name");
});
it("name", () => {
it("should show the spec information", () => {
then(get.elementsText("spec-field-doc")).shouldInclude(
"name for the style"
);
@@ -142,7 +142,7 @@ describe("modals", () => {
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().pipe((style) => style.metadata)
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({
"maputnik:openmaptiles_access_token": apiKey,
});
@@ -156,7 +156,7 @@ describe("modals", () => {
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().pipe((style) => style.metadata)
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
});

31
desktop/.gitignore vendored Normal file
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
desktop/LICENSE Normal file
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
desktop/Makefile Normal file
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
desktop/README.md Normal file
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
desktop/api.go Normal file
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())
}
}

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
desktop/go.mod Normal file
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
desktop/go.sum Normal file
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
desktop/maputnik.go Normal file
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
i18next-parser.config.ts Normal file
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

14081
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,49 @@
{
"name": "maputnik",
"version": "2.0.0-pre.2",
"version": "2.1.0",
"description": "A MapLibre GL visual style editor",
"type": "module",
"main": "''",
"scripts": {
"start": "vite",
"build": "tsc && vite build",
"build": "tsc && vite build --base=/maputnik/",
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "cypress run",
"cy:open": "cypress open",
"lint-css": "stylelint \"src/styles/*.scss\"",
"storybook": "storybook dev -h 0.0.0.0 -p 6006",
"build-storybook": "storybook build -o build/storybook"
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
},
"repository": {
"type": "git",
"url": "https://github.com/maputnik/editor"
"url": "https://github.com/maplibre/maputnik"
},
"author": "Lukas Martinelli",
"license": "MIT",
"homepage": "https://github.com/maputnik/editor#readme",
"homepage": "https://github.com/maplibre/maputnik#readme",
"dependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
"@mdi/js": "^6.6.96",
"@mdi/react": "^1.5.0",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^6.16.0",
"@maplibre/maplibre-gl-geocoder": "^1.6.0",
"@maplibre/maplibre-gl-inspect": "^1.6.3",
"@maplibre/maplibre-gl-style-spec": "^20.1.1",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"array-move": "^4.0.0",
"buffer": "^6.0.3",
"classnames": "^2.3.1",
"classnames": "^2.5.1",
"codemirror": "^5.65.2",
"color": "^4.2.3",
"cypress-plugin-tab": "^1.0.5",
"detect-browser": "^5.3.0",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"json-stringify-pretty-compact": "^3.0.0",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
"json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.21",
@@ -45,30 +53,30 @@
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"mapbox-gl-inspect": "^1.3.1",
"maplibre-gl": "^2.4.0",
"maplibre-gl": "^4.1.2",
"maputnik-design": "github:maputnik/design#172b06c",
"ol": "^6.14.1",
"ol-mapbox-style": "^7.1.1",
"prop-types": "^15.8.1",
"react": "^16.0.0",
"react-accessible-accordion": "^4.0.0",
"react": "^18.2.0",
"react-accessible-accordion": "^5.0.0",
"react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^4.0.1",
"react-aria-modal": "^5.0.2",
"react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^16.0.0",
"react-dom": "^18.2.0",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.0.1",
"react-icon-base": "^2.1.2",
"react-icons": "^4.3.1",
"react-icons": "^5.0.1",
"react-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0",
"sass": "^1.50.0",
"slugify": "^1.6.5",
"sass": "^1.72.0",
"slugify": "^1.6.6",
"string-hash": "^1.1.3",
"url": "^0.11.0"
"url": "^0.11.3"
},
"jshintConfig": {
"esversion": 6
@@ -88,24 +96,15 @@
}
},
"devDependencies": {
"@cypress/code-coverage": "^3.12.15",
"@cypress/code-coverage": "^3.12.30",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^5.0.5",
"@shellygo/cypress-test-utils": "^2.0.17",
"@storybook/addon-a11y": "^7.6.5",
"@storybook/addon-actions": "^7.6.5",
"@storybook/addon-links": "^7.6.5",
"@storybook/addon-storysource": "^7.6.5",
"@storybook/addons": "^7.6.5",
"@storybook/builder-vite": "^7.6.5",
"@storybook/react": "^7.6.5",
"@storybook/react-vite": "^7.6.5",
"@storybook/theming": "^7.6.5",
"@shellygo/cypress-test-utils": "^2.1.9",
"@types/codemirror": "^5.60.15",
"@types/color": "^3.0.6",
"@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7",
"@types/geojson": "^7946.0.13",
"@types/geojson": "^7946.0.14",
"@types/json-to-ast": "^2.1.4",
"@types/lodash.capitalize": "^4.2.9",
"@types/lodash.clamp": "^4.0.9",
@@ -113,37 +112,38 @@
"@types/lodash.get": "^4.4.9",
"@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9",
"@types/react": "^16.14.52",
"@types/react-aria-menubutton": "^6.2.13",
"@types/react-aria-modal": "^4.0.9",
"@types/react-autocomplete": "^1.8.9",
"@types/mocha": "^10.0.6",
"@types/randomcolor": "^0.5.9",
"@types/react": "^18.2.67",
"@types/react-aria-menubutton": "^6.2.14",
"@types/react-aria-modal": "^4.0.10",
"@types/react-autocomplete": "^1.8.10",
"@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.10",
"@types/react-dom": "^16.9.24",
"@types/react-color": "^3.0.12",
"@types/react-dom": "^18.2.22",
"@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6",
"@types/string-hash": "^1.1.3",
"@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.0",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react": "^4.2.1",
"cors": "^2.8.5",
"cypress": "^13.6.1",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
"cypress": "^13.13.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"express": "^4.17.3",
"eslint-plugin-react-refresh": "^0.4.6",
"i18next-parser": "^9.0.1",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.0",
"mocha": "^9.2.2",
"postcss": "^8.4.12",
"react-hot-loader": "^4.13.0",
"storybook": "^7.6.5",
"stylelint": "^14.6.1",
"stylelint-config-recommended-scss": "^6.0.0",
"stylelint-scss": "^4.2.0",
"typescript": "^5.3.3",
"uuid": "^8.3.2",
"vite": "^5.0.0",
"vite-plugin-istanbul": "^5.0.0"
"istanbul-lib-coverage": "^3.2.2",
"mocha": "^10.3.0",
"postcss": "^8.4.38",
"react-hot-loader": "^4.13.1",
"stylelint": "^16.2.1",
"stylelint-config-recommended-scss": "^14.0.0",
"stylelint-scss": "^6.2.1",
"typescript": "^5.4.3",
"uuid": "^9.0.1",
"vite": "^5.2.6",
"vite-plugin-istanbul": "^6.0.0"
}
}

View File

@@ -9,7 +9,7 @@ import {unset} from 'lodash'
import {arrayMoveMutable} from 'array-move'
import hash from "string-hash";
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
import MapMaplibreGl from './MapMaplibreGl'
import MapOpenLayers from './MapOpenLayers'
@@ -24,7 +24,6 @@ import ModalExport from './ModalExport'
import ModalSources from './ModalSources'
import ModalOpen from './ModalOpen'
import ModalShortcuts from './ModalShortcuts'
import ModalSurvey from './ModalSurvey'
import ModalDebug from './ModalDebug'
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
@@ -128,7 +127,6 @@ type AppState = {
open: boolean
shortcuts: boolean
export: boolean
survey: boolean
debug: boolean
}
}
@@ -137,7 +135,6 @@ export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore;
styleStore: StyleStore | ApiStyleStore;
layerWatcher: LayerWatcher;
shortcutEl: ModalShortcuts | null = null;
constructor(props: any) {
super(props)
@@ -277,7 +274,6 @@ export default class App extends React.Component<any, AppState> {
shortcuts: false,
export: false,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
survey: false,
debug: false,
},
maplibreGlDebugOptions: {
@@ -379,8 +375,7 @@ export default class App extends React.Component<any, AppState> {
this.getInitialStateFromUrl(newStyle);
}
// This "any" can be removed in latest version of maplibre where maplibre re-exported types from style-spec
const errors = validate(newStyle as any, latest) || [];
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
// The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here.
@@ -840,10 +835,6 @@ export default class App extends React.Component<any, AppState> {
}
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
this.setState({
isOpen: {
...this.state.isOpen,
@@ -943,7 +934,6 @@ export default class App extends React.Component<any, AppState> {
mapView={this.state.mapView}
/>
<ModalShortcuts
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>
@@ -971,10 +961,6 @@ export default class App extends React.Component<any, AppState> {
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<ModalSurvey
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div>
return <AppLayout

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,17 +24,21 @@ class AppLayout extends React.Component<AppLayoutProps> {
}
render() {
document.body.dir = this.props.i18n.dir();
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-list">
{this.props.layerList}
<div className="maputnik-layout-main">
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
@@ -43,4 +48,5 @@ class AppLayout extends React.Component<AppLayoutProps> {
}
}
export default AppLayout
const AppLayout = withTranslation()(AppLayoutInternal);
export default AppLayout;

View File

@@ -1,23 +1,24 @@
import React from 'react'
import {formatLayerId} from '../libs/format';
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
type AppMessagePanelProps = {
type AppMessagePanelInternalProps = {
errors?: unknown[]
infos?: unknown[]
infos?: string[]
mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown
currentLayer?: LayerSpecification
selectedLayerIndex?: number
};
} & WithTranslation;
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const {selectedLayerIndex} = this.props;
const {t, selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
@@ -25,7 +26,9 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
content = (
<>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
<Trans t={t}>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
</Trans>
{selectedLayerIndex !== parsed.data.index &&
<>
&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;

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();
@@ -43,29 +46,6 @@ class ToolbarLink extends React.Component<ToolbarLinkProps> {
}
}
type ToolbarLinkHighlightedProps = {
className?: string
children?: React.ReactNode
href?: string
onToggleModal?(...args: unknown[]): unknown
};
class ToolbarLinkHighlighted extends React.Component<ToolbarLinkHighlightedProps> {
render() {
return <a
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
data-wd-key="toolbar:link-highlighted"
>
<span className="maputnik-toolbar-link-wrapper">
{this.props.children}
</span>
</a>
}
}
type ToolbarSelectProps = {
children?: React.ReactNode
wdKey?: string
@@ -102,7 +82,7 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
type AppToolbarProps = {
type AppToolbarInternalProps = {
mapStyle: object
inspectModeEnabled: boolean
onStyleChanged(...args: unknown[]): unknown
@@ -115,9 +95,9 @@ type AppToolbarProps = {
onSetMapState(mapState: MapState): unknown
mapState?: MapState
renderer?: string
};
} & WithTranslation;
export default class AppToolbar extends React.Component<AppToolbarProps> {
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
state = {
isOpen: {
settings: false,
@@ -132,6 +112,10 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
this.props.onSetMapState(val);
}
handleLanguageChange(val: string) {
this.props.i18n.changeLanguage(val);
}
onSkip = (target: string) => {
if (target === "map") {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
@@ -143,40 +127,41 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
}
render() {
const t = this.props.t;
const views = [
{
id: "map",
group: "general",
title: "Map",
title: t("Map"),
},
{
id: "inspect",
group: "general",
title: "Inspect",
title: t("Inspect"),
disabled: this.props.renderer === 'ol',
},
{
id: "filter-deuteranopia",
group: "color-accessibility",
title: "Deuteranopia filter",
title: t("Deuteranopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-protanopia",
group: "color-accessibility",
title: "Protanopia filter",
title: t("Protanopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-tritanopia",
group: "color-accessibility",
title: "Tritanopia filter",
title: t("Tritanopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-achromatopsia",
group: "color-accessibility",
title: "Achromatopsia filter",
title: t("Achromatopsia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
];
@@ -196,29 +181,29 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-list")}
>
Layers list
{t("Layers list")}
</button>
<button
data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-editor")}
>
Layer editor
{t("Layer editor")}
</button>
<button
data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("map")}
>
Map view
{t("Map view")}
</button>
<a
className="maputnik-toolbar-logo"
target="blank"
rel="noreferrer noopener"
href="https://github.com/maputnik/editor"
href="https://github.com/maplibre/maputnik"
>
<img src="node_modules/maputnik-design/logos/logo-color.svg" />
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
<h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
@@ -228,24 +213,24 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser />
<IconText>Open</IconText>
<IconText>{t("Open")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>Export</IconText>
<IconText>{t("Export")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers />
<IconText>Data Sources</IconText>
<IconText>{t("Data Sources")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings />
<IconText>Style Settings</IconText>
<IconText>{t("Style Settings")}</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect">
<MdFindInPage />
<label>View
<label>{t("View")}
<select
className="maputnik-select"
data-wd-key="maputnik-select"
@@ -259,7 +244,7 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
</option>
);
})}
<optgroup label="Color accessibility">
<optgroup label={t("Color accessibility")}>
{views.filter(v => v.group === "color-accessibility").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled}>
@@ -272,16 +257,35 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<ToolbarSelect wdKey="nav:language">
<MdLanguage />
<label>{t("Language")}
<select
className="maputnik-select"
data-wd-key="maputnik-lang-select"
onChange={(e) => this.handleLanguageChange(e.target.value)}
value={this.props.i18n.language}
>
{Object.entries(supportedLanguages).map(([code, name]) => {
return (
<option key={code} value={code}>
{name}
</option>
);
})}
</select>
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
<MdHelpOutline />
<IconText>Help</IconText>
<IconText>{t("Help")}</IconText>
</ToolbarLink>
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
<MdAssignmentTurnedIn />
<IconText>Take the Maputnik Survey</IconText>
</ToolbarLinkHighlighted>
</div>
</div>
</nav>
}
}
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;

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

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)
);

View File

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

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
}

View File

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

View File

@@ -1,6 +1,6 @@
import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputString from './InputString'
@@ -13,7 +13,8 @@ type FieldIdProps = {
export default class FieldId extends React.Component<FieldIdProps> {
render() {
return <Block label={"ID"} fieldSpec={latest.layer.id}
return <Block label="ID" fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>

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(value: number | undefined): unknown
error?: {message: string}
};
} & WithTranslation;
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
render() {
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
const t = this.props.t;
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
@@ -28,3 +30,6 @@ export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
</Block>
}
}
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
export default FieldMaxZoom;

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?: {message: string}
};
} & WithTranslation;
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
render() {
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
const t = this.props.t;
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
@@ -28,3 +30,6 @@ export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
</Block>
}
}
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
export default FieldMinZoom;

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?(value: string| undefined): unknown
sourceIds?: unknown[]
error?: {message: string}
};
} & WithTranslation;
export default class FieldSource extends React.Component<FieldSourceProps> {
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
static defaultProps = {
onChange: () => {},
sourceIds: [],
}
render() {
const t = this.props.t;
return <Block
label={"Source"}
label={t("Source")}
fieldSpec={latest.layer.source}
error={this.props.error}
data-wd-key={this.props.wdKey}
@@ -33,3 +35,6 @@ export default class FieldSource extends React.Component<FieldSourceProps> {
</Block>
}
}
const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource;

View File

@@ -3,16 +3,17 @@ import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceLayerProps = {
type FieldSourceLayerInternalProps = {
value?: string
onChange?(...args: unknown[]): unknown
sourceLayerIds?: unknown[]
isFixed?: boolean
error?: {message: string}
};
} & WithTranslation;
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
@@ -20,8 +21,9 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
}
render() {
const t = this.props.t;
return <Block
label={"Source Layer"}
label={t("Source Layer")}
fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
error={this.props.error}
@@ -35,3 +37,6 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
</Block>
}
}
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
export default FieldSourceLayer;

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(value: string): unknown
error?: {message: string}
disabled?: boolean
};
} & WithTranslation;
export default class FieldType extends React.Component<FieldTypeProps> {
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
static defaultProps = {
disabled: false,
}
render() {
return <Block label={"Type"} fieldSpec={latest.layer.type}
const t = this.props.t;
return <Block label={t("Type")} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
@@ -50,3 +52,6 @@ export default class FieldType extends React.Component<FieldTypeProps> {
</Block>
}
}
const FieldType = withTranslation()(FieldTypeInternal);
export default FieldType;

View File

@@ -1,9 +1,9 @@
import React, { ReactElement } from 'react'
import React, { PropsWithChildren, ReactElement } from 'react'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
import generateUniqueId from '../libs/document-uid';
type FieldsetProps = {
type FieldsetProps = PropsWithChildren & {
label?: string,
fieldSpec?: { doc?: string },
action?: ReactElement,

View File

@@ -13,9 +13,10 @@ import FilterEditorBlock from './FilterEditorBlock'
import InputButton from './InputButton'
import Doc from './Doc'
import ExpressionProperty from './_ExpressionProperty';
import { WithTranslation, withTranslation } from 'react-i18next';
function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
const filter = props.filter || ['all'];
if (!Array.isArray(filter)) {
@@ -47,7 +48,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
"sources": {
"tmp": {
"type": "geojson",
"data": {}
"data": ''
}
},
"sprite": "",
@@ -89,13 +90,13 @@ function hasNestedCombiningFilter(filter: LegacyFilterSpecification | Expression
return false
}
type FilterEditorProps = {
type FilterEditorInternalProps = {
/** Properties of the vector layer and the available fields */
properties?: {[key:string]: any}
filter?: any[]
errors?: {[key:string]: any}
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
};
} & WithTranslation;
type FilterEditorState = {
showDoc: boolean
@@ -103,12 +104,12 @@ type FilterEditorState = {
valueIsSimpleFilter?: boolean
};
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
static defaultProps = {
filter: ["all"],
}
constructor (props: FilterEditorProps) {
constructor (props: FilterEditorInternalProps) {
super(props);
this.state = {
showDoc: false,
@@ -155,17 +156,17 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
})
}
static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
// Upgrade but never downgrade
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
if (!displaySimpleFilter && state.displaySimpleFilter === true) {
return {
displaySimpleFilter: false,
valueIsSimpleFilter: false,
};
}
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
return {
valueIsSimpleFilter: true,
}
@@ -178,7 +179,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
}
render() {
const {errors} = this.props;
const {errors, t} = this.props;
const {displaySimpleFilter} = this.state;
const fieldSpec={
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
@@ -190,16 +191,16 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
if (isNestedCombiningFilter) {
return <div className="maputnik-filter-editor-unsupported">
<p>
Nested filters are not supported.
{t("Nested filters are not supported.")}
</p>
<InputButton
onClick={this.makeExpression}
title="Convert to expression"
title={t("Convert to expression")}
>
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
Upgrade to expression
{t("Upgrade to expression")}
</InputButton>
</div>
}
@@ -212,7 +213,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
<div>
<InputButton
onClick={this.makeExpression}
title="Convert to expression"
title={t("Convert to expression")}
className="maputnik-make-zoom-function"
>
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
@@ -247,13 +248,17 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
<Block
key="top"
fieldSpec={fieldSpec}
label={"Filter"}
label={t("Filter")}
action={actions}
>
<InputSelect
value={combiningOp}
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
options={[
["all", t("every filter matches")],
["none", t("no filter matches")],
["any", t("any filter matches")]
]}
/>
</Block>
{editorBlocks}
@@ -268,7 +273,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add filter
</svg> {t("Add filter")}
</InputButton>
</div>
<div
@@ -299,12 +304,13 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
/>
{this.state.valueIsSimpleFilter &&
<div className="maputnik-expr-infobox">
You&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>
}
@@ -313,3 +319,6 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
}
}
}
const FilterEditor = withTranslation()(FilterEditorInternal);
export default FilterEditor;

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;

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);

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
@@ -117,23 +124,27 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
className="maputnik-array-add-value"
onClick={this.addValue}
>
Add value
{t("Add value")}
</InputButton>
</div>
);
}
}
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
export default FieldDynamicArray;
type DeleteValueInputButtonProps = {
onClick?(...args: unknown[]): unknown
};
} & WithTranslation;
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
render() {
const t = this.props.t;
return <InputButton
className="maputnik-delete-stop"
onClick={this.props.onClick}
title="Remove array item"
title={t("Remove array item")}
>
<FieldDocLabel
label={<MdDelete />}

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'
@@ -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;

View File

@@ -19,7 +19,10 @@ type InputNumberState = {
editing: boolean
editingRange?: boolean
value?: number
dirtyValue?: number
/**
* This is the value that is currently being edited. It can be an invalid value.
*/
dirtyValue?: number | string | undefined
}
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
@@ -38,7 +41,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}
}
static getDerivedStateFromProps(props: InputNumberProps, state: InputNumberState) {
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
if (!state.editing && props.value !== state.value) {
return {
value: props.value,
@@ -66,7 +69,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}
this.setState({
dirtyValue: newValue === "" ? undefined : value,
dirtyValue: newValue === "" ? undefined : newValue,
})
}
@@ -125,7 +128,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
// for example we might go from 13 to 13.23, however because we know
// that came from a keyboard event we always want to increase by a
// single step value.
if (value < this.state.dirtyValue!) {
if (value < +this.state.dirtyValue!) {
value = this.state.value! - step;
}
else {

View File

@@ -49,6 +49,7 @@ export default class SpecField extends React.Component<SpecFieldProps> {
value: this.props.value,
default: this.props.fieldSpec?.default,
name: this.props.fieldName,
"data-wd-key": "spec-field-input:" + this.props.fieldName,
onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
'aria-label': this.props['aria-label'],
}

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

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;

View File

@@ -19,31 +19,45 @@ import FieldSourceLayer from './FieldSourceLayer'
import { changeType, changeProperty } from '../libs/layer'
import layout from '../config/layout.json'
import {formatLayerId} from '../libs/format';
import { WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
function getLayoutForType(type: LayerSpecification["type"]) {
return layout[type] ? layout[type] : layout.invalid;
function getLayoutForType(type: LayerSpecification["type"], t: TFunction) {
return layout[type] ? {
...layout[type],
groups: layout[type].groups.map(group => {
return {
...group,
id: group.title.replace(/ /g, "_"),
title: t(group.title)
};
}),
} : layout.invalid;
}
function layoutGroups(layerType: LayerSpecification["type"]): {title: string, type: string, fields?: string[]}[] {
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
const layerGroup = {
title: 'Layer',
id: 'layer',
title: t('Layer'),
type: 'layer'
}
const filterGroup = {
title: 'Filter',
id: 'filter',
title: t('Filter'),
type: 'filter'
}
const editorGroup = {
title: 'JSON Editor',
id: 'jsoneditor',
title: t('JSON Editor'),
type: 'jsoneditor'
}
return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType).groups)
.concat(getLayoutForType(layerType, t).groups)
.concat([editorGroup])
}
type LayerEditorProps = {
type LayerEditorInternalProps = {
layer: LayerSpecification
sources: {[key: string]: SourceSpecification}
vectorLayers: {[key: string]: any}
@@ -58,14 +72,14 @@ type LayerEditorProps = {
isLastLayer?: boolean
layerIndex: number
errors?: any[]
};
} & WithTranslation;
type LayerEditorState = {
editorGroups: {[keys:string]: boolean}
};
/** Layer editor supporting multiple types of layers. */
export default class LayerEditor extends React.Component<LayerEditorProps, LayerEditorState> {
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
static defaultProps = {
onLayerChanged: () => {},
onLayerIdChange: () => {},
@@ -76,22 +90,22 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
reactIconBase: PropTypes.object
}
constructor(props: LayerEditorProps) {
constructor(props: LayerEditorInternalProps) {
super(props)
//TODO: Clean this up and refactor into function
const editorGroups: {[keys:string]: boolean} = {}
layoutGroups(this.props.layer.type).forEach(group => {
layoutGroups(this.props.layer.type, props.t).forEach(group => {
editorGroups[group.title] = true
})
this.state = { editorGroups }
}
static getDerivedStateFromProps(props: LayerEditorProps, state: LayerEditorState) {
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
const additionalGroups = { ...state.editorGroups }
getLayoutForType(props.layer.type).groups.forEach(group => {
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
@@ -242,17 +256,19 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
}
render() {
const t = this.props.t;
const groupIds: string[] = [];
const layerType = this.props.layer.type
const groups = layoutGroups(layerType).filter(group => {
const groups = layoutGroups(layerType, t).filter(group => {
return !(layerType === 'background' && group.type === 'source')
}).map(group => {
const groupId = group.title.replace(/ /g, "_");
const groupId = group.id;
groupIds.push(groupId);
return <LayerEditorGroup
data-wd-key={group.title}
id={groupId}
key={group.title}
key={groupId}
title={group.title}
isActive={this.state.editorGroups[group.title]}
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
@@ -265,25 +281,25 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
delete: {
text: "Delete",
text: t("Delete"),
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
},
duplicate: {
text: "Duplicate",
text: t("Duplicate"),
handler: () => this.props.onLayerCopy(this.props.layerIndex)
},
hide: {
text: (layout.visibility === "none") ? "Show" : "Hide",
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
},
moveLayerUp: {
text: "Move layer up",
text: t("Move layer up"),
// Not actually used...
disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1)
},
moveLayerDown: {
text: "Move layer down",
text: t("Move layer down"),
// Not actually used...
disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1)
@@ -297,12 +313,12 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
return <section className="maputnik-layer-editor"
role="main"
aria-label="Layer editor"
aria-label={t("Layer editor")}
>
<header>
<div className="layer-header">
<h2 className="layer-header__title">
Layer: {formatLayerId(this.props.layer.id)}
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
</h2>
<div className="layer-header__info">
<Wrapper
@@ -310,7 +326,11 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
onSelection={handleSelection}
closeOnSelection={false}
>
<Button id="skip-target-layer-editor" data-wd-key="skip-target-layer-editor" className='more-menu__button' title="Layer options">
<Button
id="skip-target-layer-editor"
data-wd-key="skip-target-layer-editor"
className='more-menu__button'
title={"Layer options"}>
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
@@ -340,3 +360,6 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
</section>
}
}
const LayerEditor = withTranslation()(LayerEditorInternal);
export default LayerEditor;

View File

@@ -10,6 +10,7 @@ import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
import type {LayerSpecification} from 'maplibre-gl';
import generateUniqueId from '../libs/document-uid';
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
import { WithTranslation, withTranslation } from 'react-i18next';
type LayerListContainerProps = {
layers: LayerSpecification[]
@@ -22,6 +23,7 @@ type LayerListContainerProps = {
sources: object
errors: any[]
};
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
type LayerListContainerState = {
collapsedGroups: {[ket: string]: boolean}
@@ -31,14 +33,14 @@ type LayerListContainerState = {
};
// List of collapsible layer editors
class LayerListContainer extends React.Component<LayerListContainerProps, LayerListContainerState> {
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
static defaultProps = {
onLayerSelect: () => {},
}
selectedItemRef: React.RefObject<any>;
scrollContainerRef: React.RefObject<HTMLElement>;
constructor(props: LayerListContainerProps) {
constructor(props: LayerListContainerInternalProps) {
super(props);
this.selectedItemRef = React.createRef();
this.scrollContainerRef = React.createRef();
@@ -259,10 +261,12 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
})
})
const t = this.props.t;
return <section
className="maputnik-layer-list"
role="complementary"
aria-label="Layers list"
aria-label={t("Layers list")}
ref={this.scrollContainerRef}
>
<ModalAdd
@@ -274,7 +278,7 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
onLayersChange={this.props.onLayersChange}
/>
<header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">Layers</span>
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
<span className="maputnik-space" />
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
@@ -283,7 +287,11 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
data-wd-key="skip-target-layer-list"
onClick={this.toggleLayers}
className="maputnik-button">
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
{this.state.areAllGroupsExpanded === true ?
t("Collapse")
:
t("Expand")
}
</button>
</div>
</div>
@@ -293,14 +301,14 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
onClick={this.toggleModal.bind(this, 'add')}
data-wd-key="layer-list:add-layer"
className="maputnik-button maputnik-button-selected">
Add Layer
{t("Add Layer")}
</button>
</div>
</div>
</header>
<div
role="navigation"
aria-label="Layers list"
aria-label={t("Layers list")}
>
<ul className="maputnik-layer-list-container">
{listItems}
@@ -310,6 +318,13 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
}
}
// The next two lines have react-refresh/only-export-components disabled because they are
// internal components that are not intended to be used outside of this file.
// For some reason, the linter is not recognizing these components correctly.
// When these components are migrated to functional components, the HOCs will no longer be needed
// and the comments can be removed.
// eslint-disable-next-line react-refresh/only-export-components
const LayerListContainer = withTranslation()(LayerListContainerInternal);
// eslint-disable-next-line react-refresh/only-export-components
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)

View File

@@ -139,6 +139,6 @@ class LayerListItem extends React.Component<LayerListItemProps> {
}
}
const LayerListItemSortable = SortableElement((props: LayerListItemProps) => <LayerListItem {...props} />);
const LayerListItemSortable = SortableElement<LayerListItemProps>((props: LayerListItemProps) => <LayerListItem {...props} />);
export default LayerListItemSortable;

View File

@@ -1,10 +1,8 @@
import React, {type JSX} from 'react'
import ReactDOM from 'react-dom'
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
// @ts-ignore
import MapboxInspect from 'mapbox-gl-inspect'
// @ts-ignore
import colors from 'mapbox-gl-inspect/lib/colors'
import MaplibreInspect from '@maplibre/maplibre-gl-inspect'
import colors from '@maplibre/maplibre-gl-inspect/lib/colors'
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'
import MapMaplibreGlFeaturePropertyPopup, { InspectFeature } from './MapMaplibreGlFeaturePropertyPopup'
import Color from 'color'
@@ -13,13 +11,13 @@ import { HighlightedLayer, colorHighlightedLayer } from '../libs/highlight'
import 'maplibre-gl/dist/maplibre-gl.css'
import '../maplibregl.css'
import '../libs/maplibre-rtl'
import MaplibreGeocoder, { MaplibreGeocoderApi, MaplibreGeocoderApiConfig } from '@maplibre/maplibre-gl-geocoder';
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
import { withTranslation, WithTranslation } from 'react-i18next'
const IS_SUPPORTED = MapLibreGl.supported();
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container) {
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
ReactDOM.render(popup, mountNode);
return mountNode;
return mountNode as HTMLElement;
}
function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) {
@@ -37,6 +35,7 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
}
const sources: {[key:string]: SourceSpecification} = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster' && source.type !== 'raster-dem') {
@@ -52,7 +51,7 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
return inspectStyle
}
type MapMaplibreGlProps = {
type MapMaplibreGlInternalProps = {
onDataChange?(event: {map: Map | null}): unknown
onLayerSelect(...args: unknown[]): unknown
mapStyle: StyleSpecification
@@ -65,15 +64,15 @@ type MapMaplibreGlProps = {
}
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
onChange(value: {center: LngLat, zoom: number}): unknown
};
} & WithTranslation;
type MapMaplibreGlState = {
map: Map | null
inspect: MapboxInspect | null
inspect: MaplibreInspect | null
zoom?: number
};
export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, MapMaplibreGlState> {
class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> {
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
@@ -83,7 +82,7 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
}
container: HTMLDivElement | null = null
constructor(props: MapMaplibreGlProps) {
constructor(props: MapMaplibreGlInternalProps) {
super(props)
this.state = {
map: null,
@@ -91,20 +90,8 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
}
}
updateMapFromProps(props: MapMaplibreGlProps) {
if(!IS_SUPPORTED) return;
if(!this.state.map) return
//Maplibre GL now does diffing natively so we don't need to calculate
//the necessary operations ourselves!
this.state.map.setStyle(
this.props.replaceAccessTokens(props.mapStyle),
{diff: true}
)
}
shouldComponentUpdate(nextProps: MapMaplibreGlProps, nextState: MapMaplibreGlState) {
shouldComponentUpdate(nextProps: MapMaplibreGlInternalProps, nextState: MapMaplibreGlState) {
let should = false;
try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
@@ -115,46 +102,42 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
}
componentDidUpdate() {
if(!IS_SUPPORTED) return;
const map = this.state.map;
this.updateMapFromProps(this.props);
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
// eslint-disable-next-line
this.state.inspect._popupBlocked = false;
this.state.inspect.toggleInspector()
}
const styleWithTokens = this.props.replaceAccessTokens(this.props.mapStyle);
if (map) {
if (this.props.inspectModeEnabled) {
// HACK: We need to work out why we need to do this and what's causing
// this error. I'm assuming an issue with maplibre-gl update and
// mapbox-gl-inspect.
try {
this.state.inspect.render();
} catch(err) {
console.error("FIXME: Caught error", err);
}
}
// Maplibre GL now does diffing natively so we don't need to calculate
// the necessary operations ourselves!
// We also need to update the style for inspect to work properly
map.setStyle(styleWithTokens, {diff: true});
map.showTileBoundaries = this.props.options?.showTileBoundaries!;
map.showCollisionBoxes = this.props.options?.showCollisionBoxes!;
map.showOverdrawInspector = this.props.options?.showOverdrawInspector!;
}
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
this.state.inspect.toggleInspector()
}
if (this.state.inspect && this.props.inspectModeEnabled) {
this.state.inspect.setOriginalStyle(styleWithTokens);
// In case the sources are the same, there's a need to refresh the style
setTimeout(() => {
this.state.inspect!.render();
}, 500);
}
}
componentDidMount() {
if(!IS_SUPPORTED) return;
const mapOpts = {
...this.props.options,
container: this.container!,
style: this.props.mapStyle,
hash: true,
maxZoom: 24
}
maxZoom: 24,
// setting to always load glyphs of CJK fonts from server
// https://maplibre.org/maplibre-gl-js/docs/examples/local-ideographs/
localIdeographFontFamily: false
} satisfies MapOptions;
const map = new MapLibreGl.Map(mapOpts);
@@ -169,7 +152,9 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
map.showCollisionBoxes = mapOpts.showCollisionBoxes!;
map.showOverdrawInspector = mapOpts.showOverdrawInspector!;
const zoomControl = new ZoomControl;
this.initGeocoder(map);
const zoomControl = new ZoomControl(this.props.t("Zoom:"));
map.addControl(zoomControl, 'top-right');
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
@@ -177,7 +162,7 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
const tmpNode = document.createElement('div');
const inspect = new MapboxInspect({
const inspect = new MaplibreInspect({
popup: new MapLibreGl.Popup({
closeOnClick: false
}),
@@ -234,25 +219,61 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
this.props.onLayerSelect(index);
}
initGeocoder(map: Map) {
const geocoderConfig = {
forwardGeocode: async (config: MaplibreGeocoderApiConfig) => {
const features = [];
try {
const request = `https://nominatim.openstreetmap.org/search?q=${config.query}&format=geojson&polygon_geojson=1&addressdetails=1`;
const response = await fetch(request);
const geojson = await response.json();
for (const feature of geojson.features) {
const center = [
feature.bbox[0] +
(feature.bbox[2] - feature.bbox[0]) / 2,
feature.bbox[1] +
(feature.bbox[3] - feature.bbox[1]) / 2
];
const point = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: center
},
place_name: feature.properties.display_name,
properties: feature.properties,
text: feature.properties.display_name,
place_type: ['place'],
center
};
features.push(point);
}
} catch (e) {
console.error(`Failed to forwardGeocode with error: ${e}`);
}
return {
features
};
},
} as unknown as MaplibreGeocoderApi;
const geocoder = new MaplibreGeocoder(geocoderConfig, {
placeholder: this.props.t("Search"),
maplibregl: MapLibreGl,
});
map.addControl(geocoder, 'top-left');
}
render() {
if(IS_SUPPORTED) {
return <div
className="maputnik-map__map"
role="region"
aria-label="Map view"
ref={x => this.container = x}
data-wd-key="maplibre:map"
></div>
}
else {
return <div
className="maputnik-map maputnik-map--error"
>
<div className="maputnik-map__error-message">
Error: Cannot load MaplibreGL, WebGL is either unsupported or disabled
</div>
</div>
}
const t = this.props.t;
return <div
className="maputnik-map__map"
role="region"
aria-label={t("Map view")}
ref={x => this.container = x}
data-wd-key="maplibre:map"
></div>
}
}
const MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
export default MapMaplibreGl;

View File

@@ -1,18 +1,12 @@
import React from 'react'
import Block from './Block'
import FieldString from './FieldString'
import type { GeoJSONFeatureWithSourceLayer } from '@maplibre/maplibre-gl-inspect'
export type InspectFeature = {
id: string
properties: {[key:string]: any}
layer: {[key:string]: any}
geometry: GeoJSON.Geometry
sourceLayer: string
export type InspectFeature = GeoJSONFeatureWithSourceLayer & {
inspectModeCounter?: number
counter?: number
}
function displayValue(value: string | number | Date | object) {
function displayValue(value: string | number | Date | object | undefined) {
if (typeof value === 'undefined' || value === null) return value;
if (value instanceof Date) return value.toLocaleString();
if (typeof value === 'object' ||
@@ -21,30 +15,25 @@ function displayValue(value: string | number | Date | object) {
return value;
}
function renderProperties(feature: InspectFeature) {
return Object.keys(feature.properties).map(propertyName => {
const property = feature.properties[propertyName]
return <Block key={propertyName} label={propertyName}>
<FieldString value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
</Block>
})
}
function renderFeatureId(feature: InspectFeature) {
return <Block key={"feature-id"} label={"feature_id"}>
<FieldString value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} />
</Block>
function renderKeyValueTableRow(key: string, value: string | undefined) {
return <tr key={key}>
<td className="maputnik-popup-table-cell">{key}</td>
<td className="maputnik-popup-table-cell">{value}</td>
</tr>
}
function renderFeature(feature: InspectFeature, idx: number) {
return <div key={`${feature.sourceLayer}-${idx}`}>
<div className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
<Block key={"property-type"} label={"$type"}>
<FieldString value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
</Block>
{renderFeatureId(feature)}
{renderProperties(feature)}
</div>
return <React.Fragment key={idx}>
<tr>
<td colSpan={2} className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</td>
</tr>
{renderKeyValueTableRow("$type", feature.geometry.type)}
{renderKeyValueTableRow("$id", displayValue(feature.id))}
{Object.keys(feature.properties).map(propertyName => {
const property = feature.properties[propertyName];
return renderKeyValueTableRow(propertyName, displayValue(property))
})}
</React.Fragment>
}
function removeDuplicatedFeatures(features: InspectFeature[]) {
@@ -78,7 +67,11 @@ class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
render() {
const features = removeDuplicatedFeatures(this.props.features)
return <div className="maputnik-feature-property-popup">
{features.map(renderFeature)}
<table className="maputnik-popup-table">
<tbody>
{features.map(renderFeature)}
</tbody>
</table>
</div>
}
}

View File

@@ -8,15 +8,16 @@ function groupFeaturesBySourceLayer(features: InspectFeature[]) {
const returnedFeatures: {[key: string]: number} = {}
features.forEach(feature => {
const sourceKey = feature.layer['source-layer'] as string;
if(Object.prototype.hasOwnProperty.call(returnedFeatures, feature.layer.id)) {
returnedFeatures[feature.layer.id]++
const featureObject = sources[feature.layer['source-layer']].find((f: InspectFeature) => f.layer.id === feature.layer.id)
const featureObject = sources[sourceKey].find((f: InspectFeature) => f.layer.id === feature.layer.id)
featureObject!.counter = returnedFeatures[feature.layer.id]
} else {
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
sources[feature.layer['source-layer']].push(feature)
sources[sourceKey] = sources[sourceKey] || []
sources[sourceKey].push(feature)
returnedFeatures[feature.layer.id] = 1
}
@@ -40,29 +41,21 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
try {
const paintProps = feature.layer.paint;
let propName;
if(Object.prototype.hasOwnProperty.call(paintProps, "text-color") && paintProps["text-color"]) {
propName = "text-color";
if("text-color" in paintProps && paintProps["text-color"]) {
return String(paintProps["text-color"]);
}
else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-color") && paintProps["fill-color"]) {
propName = "fill-color";
if ("fill-color" in paintProps && paintProps["fill-color"]) {
return String(paintProps["fill-color"]);
}
else if (Object.prototype.hasOwnProperty.call(paintProps, "line-color") && paintProps["line-color"]) {
propName = "line-color";
if ("line-color" in paintProps && paintProps["line-color"]) {
return String(paintProps["line-color"]);
}
else if (Object.prototype.hasOwnProperty.call(paintProps, "fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
propName = "fill-extrusion-color";
}
if(propName) {
const color = feature.layer.paint[propName];
return String(color);
}
else {
// Default color
return "black";
if ("fill-extrusion-color" in paintProps && paintProps["fill-extrusion-color"]) {
return String(paintProps["fill-extrusion-color"]);
}
// Default color
return "black";
}
// This is quite complex, just incase there's an edgecase we're missing
// always return black if we get an unexpected error.

View File

@@ -1,9 +1,11 @@
import React from 'react'
import {throttle} from 'lodash';
import { WithTranslation, withTranslation } from 'react-i18next';
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup';
import 'ol/ol.css'
//@ts-ignore
import {apply} from 'ol-mapbox-style';
import {Map, View, Overlay} from 'ol';
@@ -22,7 +24,7 @@ function renderCoords (coords: string[]) {
}
}
type MapOpenLayersProps = {
type MapOpenLayersInternalProps = {
onDataChange?(...args: unknown[]): unknown
mapStyle: object
accessToken?: string
@@ -31,7 +33,7 @@ type MapOpenLayersProps = {
debugToolbox: boolean
replaceAccessTokens(...args: unknown[]): unknown
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
type MapOpenLayersState = {
zoom: string
@@ -41,7 +43,7 @@ type MapOpenLayersState = {
selectedFeatures?: any[]
};
export default class MapOpenLayers extends React.Component<MapOpenLayersProps, MapOpenLayersState> {
class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps, MapOpenLayersState> {
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
@@ -53,7 +55,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
overlay: Overlay | undefined;
popupContainer: HTMLElement | null = null;
constructor(props: MapOpenLayersProps) {
constructor(props: MapOpenLayersInternalProps) {
super(props);
this.state = {
zoom: "0",
@@ -72,7 +74,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
apply(this.map, newMapStyle);
}
componentDidUpdate(prevProps: MapOpenLayersProps) {
componentDidUpdate(prevProps: MapOpenLayersInternalProps) {
if (this.props.mapStyle !== prevProps.mapStyle) {
this.updateStyle(
this.props.replaceAccessTokens(this.props.mapStyle)
@@ -150,6 +152,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
}
render() {
const t = this.props.t;
return <div className="maputnik-ol-container">
<div
ref={x => this.popupContainer = x}
@@ -159,7 +162,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
<button
className="maplibregl-popup-close-button"
onClick={this.closeOverlay}
aria-label="Close popup"
aria-label={t("Close popup")}
>
×
</button>
@@ -169,20 +172,20 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
/>
</div>
<div className="maputnik-ol-zoom">
Zoom: {this.state.zoom}
{t("Zoom:")} {this.state.zoom}
</div>
{this.props.debugToolbox &&
<div className="maputnik-ol-debug">
<div>
<label>cursor: </label>
<label>{t("cursor:")} </label>
<span>{renderCoords(this.state.cursor)}</span>
</div>
<div>
<label>center: </label>
<label>{t("center:")} </label>
<span>{renderCoords(this.state.center)}</span>
</div>
<div>
<label>rotation: </label>
<label>{t("rotation:")} </label>
<span>{this.state.rotation}</span>
</div>
</div>
@@ -191,7 +194,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
className="maputnik-ol"
ref={x => this.container = x}
role="region"
aria-label="Map view"
aria-label={t("Map view")}
style={{
...this.props.style,
}}>
@@ -200,3 +203,5 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
}
}
const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
export default MapOpenLayers;

View File

@@ -1,10 +1,10 @@
import React from 'react'
import React, { PropsWithChildren } from 'react'
import {MdClose} from 'react-icons/md'
import AriaModal from 'react-aria-modal'
import classnames from 'classnames';
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalProps = {
type ModalInternalProps = PropsWithChildren & {
"data-wd-key"?: string
isOpen: boolean
title: string
@@ -12,15 +12,15 @@ type ModalProps = {
underlayClickExits?: boolean
underlayProps?: any
className?: string
};
} & WithTranslation;
export default class Modal extends React.Component<ModalProps> {
class ModalInternal extends React.Component<ModalInternalProps> {
static defaultProps = {
underlayClickExits: true
}
// See <https://github.com/maputnik/editor/issues/416>
// See <https://github.com/maplibre/maputnik/issues/416>
onClose = () => {
if (document.activeElement) {
(document.activeElement as HTMLElement).blur();
@@ -32,6 +32,7 @@ export default class Modal extends React.Component<ModalProps> {
}
render() {
const t = this.props.t;
if(this.props.isOpen) {
return <AriaModal
titleText={this.props.title}
@@ -49,7 +50,7 @@ export default class Modal extends React.Component<ModalProps> {
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
<span className="maputnik-modal-header-space"></span>
<button className="maputnik-modal-header-toggle"
title="Close modal"
title={t("Close modal")}
onClick={this.onClose}
data-wd-key={this.props["data-wd-key"]+".close-modal"}
>
@@ -68,3 +69,5 @@ export default class Modal extends React.Component<ModalProps> {
}
}
const Modal = withTranslation()(ModalInternal);
export default Modal;

View File

@@ -7,15 +7,16 @@ import FieldId from './FieldId'
import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer'
import type {LayerSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalAddProps = {
type ModalAddInternalProps = {
layers: LayerSpecification[]
onLayersChange(layers: LayerSpecification[]): unknown
isOpen: boolean
onOpenToggle(open: boolean): unknown
// A dict of source id's and the available source layers
sources: any
};
} & WithTranslation;
type ModalAddState = {
type: LayerSpecification["type"]
@@ -24,7 +25,7 @@ type ModalAddState = {
'source-layer'?: string
};
export default class ModalAdd extends React.Component<ModalAddProps, ModalAddState> {
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
addLayer = () => {
const changedLayers = this.props.layers.slice(0)
const layer: ModalAddState = {
@@ -45,7 +46,7 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
this.props.onOpenToggle(false)
}
constructor(props: ModalAddProps) {
constructor(props: ModalAddInternalProps) {
super(props)
const state: ModalAddState = {
type: 'fill',
@@ -54,12 +55,12 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
if(props.sources.length > 0) {
state.source = Object.keys(this.props.sources)[0];
state['source-layer'] = this.props.sources[state.source as keyof ModalAddProps["sources"]][0]
state['source-layer'] = this.props.sources[state.source as keyof ModalAddInternalProps["sources"]][0]
}
this.state = state;
}
componentDidUpdate(_prevProps: ModalAddProps, prevState: ModalAddState) {
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
// Check if source is valid for new type
const oldType = prevState.type;
const newType = this.state.type;
@@ -125,13 +126,14 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
render() {
const t = this.props.t;
const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source!);
return <Modal
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Add Layer'}
title={t('Add Layer')}
data-wd-key="modal:add-layer"
className="maputnik-add-modal"
>
@@ -169,10 +171,12 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
onClick={this.addLayer}
data-wd-key="add-layer"
>
Add Layer
{t("Add Layer")}
</InputButton>
</div>
</Modal>
}
}
const ModalAdd = withTranslation()(ModalAddInternal);
export default ModalAdd;

View File

@@ -1,9 +1,10 @@
import React from 'react'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal'
type ModalDebugProps = {
type ModalDebugInternalProps = {
isOpen: boolean
renderer: string
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
@@ -18,12 +19,12 @@ type ModalDebugProps = {
lat: number
}
}
};
} & WithTranslation;
export default class ModalDebug extends React.Component<ModalDebugProps> {
class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
render() {
const {mapView} = this.props;
const {t, mapView} = this.props;
const osmZoom = Math.round(mapView.zoom)+1;
const osmLon = +(mapView.center.lng).toFixed(5);
@@ -33,10 +34,10 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
data-wd-key="modal:debug"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Debug'}
title={t('Debug')}
>
<section className="maputnik-modal-section maputnik-modal-shortcuts">
<h1>Options</h1>
<h1>{t("Options")}</h1>
{this.props.renderer === 'mlgljs' &&
<ul>
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
@@ -63,16 +64,20 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
<section className="maputnik-modal-section">
<h1>Links</h1>
<p>
<a
target="_blank"
rel="noopener noreferrer"
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
>
Open in OSM
</a> &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;

View File

@@ -5,6 +5,7 @@ import {version} from 'maplibre-gl/package.json'
import {format} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification} from 'maplibre-gl'
import {MdFileDownload} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
import FieldString from './FieldString'
import InputButton from './InputButton'
@@ -16,15 +17,15 @@ import fieldSpecAdditional from '../libs/field-spec-additional'
const MAPLIBRE_GL_VERSION = version;
type ModalExportProps = {
type ModalExportInternalProps = {
mapStyle: StyleSpecification & { id: string }
onStyleChanged(...args: unknown[]): unknown
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
};
} & WithTranslation;
export default class ModalExport extends React.Component<ModalExportProps> {
class ModalExportInternal extends React.Component<ModalExportInternalProps> {
tokenizedStyle () {
return format(
@@ -48,7 +49,7 @@ export default class ModalExport extends React.Component<ModalExportProps> {
downloadHtml() {
const tokenStyle = this.tokenizedStyle();
const htmlTitle = this.props.mapStyle.name || "Map";
const htmlTitle = this.props.mapStyle.name || this.props.t("Map");
const html = `<!DOCTYPE html>
<html>
<head>
@@ -100,30 +101,32 @@ export default class ModalExport extends React.Component<ModalExportProps> {
render() {
const t = this.props.t;
const fsa = fieldSpecAdditional(t);
return <Modal
data-wd-key="modal:export"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Export Style'}
title={t('Export Style')}
className="maputnik-export-modal"
>
<section className="maputnik-modal-section">
<h1>Download Style</h1>
<h1>{t("Download Style")}</h1>
<p>
Download a JSON style to your computer.
{t("Download a JSON style to your computer.")}
</p>
<div>
<FieldString
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
label={fsa.maputnik.maptiler_access_token.label}
fieldSpec={fsa.maputnik.maptiler_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/>
<FieldString
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
label={fsa.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:thunderforest_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
@@ -134,14 +137,14 @@ export default class ModalExport extends React.Component<ModalExportProps> {
onClick={this.downloadStyle.bind(this)}
>
<MdFileDownload />
Download Style
{t("Download Style")}
</InputButton>
<InputButton
onClick={this.downloadHtml.bind(this)}
>
<MdFileDownload />
Download HTML
{t("Download HTML")}
</InputButton>
</div>
</section>
@@ -150,3 +153,5 @@ export default class ModalExport extends React.Component<ModalExportProps> {
}
}
const ModalExport = withTranslation()(ModalExportInternal);
export default ModalExport;

View File

@@ -2,23 +2,25 @@ import React from 'react'
import InputButton from './InputButton'
import Modal from './Modal'
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalLoadingProps = {
type ModalLoadingInternalProps = {
isOpen: boolean
onCancel(...args: unknown[]): unknown
title: string
message: React.ReactNode
};
} & WithTranslation;
export default class ModalLoading extends React.Component<ModalLoadingProps> {
class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
underlayOnClick(e: Event) {
// This stops click events falling through to underlying modals.
e.stopPropagation();
}
render() {
const t = this.props.t;
return <Modal
data-wd-key="modal:loading"
isOpen={this.props.isOpen}
@@ -35,10 +37,12 @@ export default class ModalLoading extends React.Component<ModalLoadingProps> {
</p>
<p className="maputnik-dialog__buttons">
<InputButton onClick={(e) => this.props.onCancel(e)}>
Cancel
{t("Cancel")}
</InputButton>
</p>
</Modal>
}
}
const ModalLoading = withTranslation()(ModalLoadingInternal);
export default ModalLoading;

View File

@@ -2,6 +2,7 @@ import React, { FormEvent } from 'react'
import {MdFileUpload} from 'react-icons/md'
import {MdAddCircleOutline} from 'react-icons/md'
import FileReaderInput, { Result } from 'react-file-reader-input'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import ModalLoading from './ModalLoading'
import Modal from './Modal'
@@ -42,11 +43,11 @@ class PublicStyle extends React.Component<PublicStyleProps> {
}
}
type ModalOpenProps = {
type ModalOpenInternalProps = {
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
onStyleOpen(...args: unknown[]): unknown
};
} & WithTranslation;
type ModalOpenState = {
styleUrl: string
@@ -55,8 +56,8 @@ type ModalOpenState = {
activeRequestUrl?: string | null
};
export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpenState> {
constructor(props: ModalOpenProps) {
class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> {
constructor(props: ModalOpenInternalProps) {
super(props);
this.state = {
styleUrl: ""
@@ -174,6 +175,7 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
}
render() {
const t = this.props.t;
const styleOptions = publicStyles.map(style => {
return <PublicStyle
key={style.id}
@@ -200,29 +202,31 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
data-wd-key="modal:open"
isOpen={this.props.isOpen}
onOpenToggle={() => this.onOpenToggle()}
title={'Open Style'}
title={t('Open Style')}
>
{errorElement}
<section className="maputnik-modal-section">
<h1>Upload Style</h1>
<p>Upload a JSON style from your computer.</p>
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label="Style file">
<InputButton className="maputnik-upload-button"><MdFileUpload /> Upload</InputButton>
<h1>{t("Upload Style")}</h1>
<p>{t("Upload a JSON style from your computer.")}</p>
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label={t("Style file")}>
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Upload")}</InputButton>
</FileReaderInput>
</section>
<section className="maputnik-modal-section">
<form onSubmit={this.onSubmitUrl}>
<h1>Load from URL</h1>
<h1>{t("Load from URL")}</h1>
<p>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
<Trans t={t}>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
</Trans>
</p>
<InputUrl
aria-label="Style URL"
aria-label={t("Style URL")}
data-wd-key="modal:open.url.input"
type="text"
className="maputnik-input"
default="Enter URL..."
default={t("Enter URL...")}
value={this.state.styleUrl}
onInput={this.onChangeUrl}
onChange={this.onChangeUrl}
@@ -239,9 +243,9 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
</section>
<section className="maputnik-modal-section maputnik-modal-section--shrink">
<h1>Gallery Styles</h1>
<h1>{t("Gallery Styles")}</h1>
<p>
Open one of the publicly available styles to start from.
{t("Open one of the publicly available styles to start from.")}
</p>
<div className="maputnik-style-gallery-container">
{styleOptions}
@@ -251,12 +255,14 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
<ModalLoading
isOpen={!!this.state.activeRequest}
title={'Loading style'}
title={t('Loading style')}
onCancel={(e: Event) => this.onCancelActiveRequest(e)}
message={"Loading: "+this.state.activeRequestUrl}
message={t("Loading: {{requestUrl}}", { requestUrl: this.state.activeRequestUrl })}
/>
</div>
)
}
}
const ModalOpen = withTranslation()(ModalOpenInternal);
export default ModalOpen;

View File

@@ -1,6 +1,7 @@
import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import type {LightSpecification, StyleSpecification, TransitionSpecification} from 'maplibre-gl'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import type {LightSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
import FieldArray from './FieldArray'
import FieldNumber from './FieldNumber'
@@ -12,15 +13,15 @@ import FieldColor from './FieldColor'
import Modal from './Modal'
import fieldSpecAdditional from '../libs/field-spec-additional'
type ModalSettingsProps = {
type ModalSettingsInternalProps = {
mapStyle: StyleSpecification
onStyleChanged(...args: unknown[]): unknown
onChangeMetadataProperty(...args: unknown[]): unknown
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
};
} & WithTranslation;
export default class ModalSettings extends React.Component<ModalSettingsProps> {
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
const transition = {
...this.props.mapStyle.transition,
@@ -58,6 +59,25 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
});
}
changeTerrainProperty(property: keyof TerrainSpecification, value: any) {
const terrain = {
...this.props.mapStyle.terrain,
}
if (value === undefined) {
delete terrain[property];
}
else {
// @ts-ignore
terrain[property] = value;
}
this.props.onStyleChanged({
...this.props.mapStyle,
terrain,
});
}
changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) {
const changedStyle = {
...this.props.mapStyle,
@@ -76,87 +96,86 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
render() {
const metadata = this.props.mapStyle.metadata || {} as any;
const {onChangeMetadataProperty, mapStyle} = this.props;
const inputProps = { }
const {t, onChangeMetadataProperty, mapStyle} = this.props;
const fsa = fieldSpecAdditional(t);
const light = this.props.mapStyle.light || {};
const transition = this.props.mapStyle.transition || {};
const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification;
return <Modal
data-wd-key="modal:settings"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Style Settings'}
title={t('Style Settings')}
>
<div className="modal:settings">
<FieldString {...inputProps}
label={"Name"}
<FieldString
label={t("Name")}
fieldSpec={latest.$root.name}
data-wd-key="modal:settings.name"
value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")}
/>
<FieldString {...inputProps}
label={"Owner"}
fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}
<FieldString
label={t("Owner")}
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
data-wd-key="modal:settings.owner"
value={(this.props.mapStyle as any).owner}
onChange={this.changeStyleProperty.bind(this, "owner")}
/>
<FieldUrl {...inputProps}
<FieldUrl
fieldSpec={latest.$root.sprite}
label="Sprite URL"
label={t("Sprite URL")}
data-wd-key="modal:settings.sprite"
value={this.props.mapStyle.sprite as string}
onChange={this.changeStyleProperty.bind(this, "sprite")}
/>
<FieldUrl {...inputProps}
label="Glyphs URL"
<FieldUrl
label={t("Glyphs URL")}
fieldSpec={latest.$root.glyphs}
data-wd-key="modal:settings.glyphs"
value={this.props.mapStyle.glyphs as string}
onChange={this.changeStyleProperty.bind(this, "glyphs")}
/>
<FieldString {...inputProps}
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
<FieldString
label={fsa.maputnik.maptiler_access_token.label}
fieldSpec={fsa.maputnik.maptiler_access_token}
data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
value={metadata['maputnik:openmaptiles_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/>
<FieldString {...inputProps}
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
<FieldString
label={fsa.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token}
data-wd-key="modal:settings.maputnik:thunderforest_access_token"
value={metadata['maputnik:thunderforest_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
<FieldArray
label={"Center"}
label={t("Center")}
fieldSpec={latest.$root.center}
length={2}
type="number"
value={mapStyle.center || []}
default={latest.$root.center.default || [0, 0]}
default={[0, 0]}
onChange={this.changeStyleProperty.bind(this, "center")}
/>
<FieldNumber
{...inputProps}
label={"Zoom"}
label={t("Zoom")}
fieldSpec={latest.$root.zoom}
value={mapStyle.zoom}
default={latest.$root.zoom.default || 0}
default={0}
onChange={this.changeStyleProperty.bind(this, "zoom")}
/>
<FieldNumber
{...inputProps}
label={"Bearing"}
label={t("Bearing")}
fieldSpec={latest.$root.bearing}
value={mapStyle.bearing}
default={latest.$root.bearing.default}
@@ -164,8 +183,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldNumber
{...inputProps}
label={"Pitch"}
label={t("Pitch")}
fieldSpec={latest.$root.pitch}
value={mapStyle.pitch}
default={latest.$root.pitch.default}
@@ -173,8 +191,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldEnum
{...inputProps}
label={"Light anchor"}
label={t("Light anchor")}
fieldSpec={latest.light.anchor}
name="light-anchor"
value={light.anchor as string}
@@ -184,8 +201,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldColor
{...inputProps}
label={"Light color"}
label={t("Light color")}
fieldSpec={latest.light.color}
value={light.color as string}
default={latest.light.color.default}
@@ -193,8 +209,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldNumber
{...inputProps}
label={"Light intensity"}
label={t("Light intensity")}
fieldSpec={latest.light.intensity}
value={light.intensity as number}
default={latest.light.intensity.default}
@@ -202,8 +217,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldArray
{...inputProps}
label={"Light position"}
label={t("Light position")}
fieldSpec={latest.light.position}
type="number"
length={latest.light.position.length}
@@ -212,9 +226,24 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
onChange={this.changeLightProperty.bind(this, "position")}
/>
<FieldString
label={t("Terrain source")}
fieldSpec={latest.terrain.source}
data-wd-key="modal:settings.maputnik:terrain_source"
value={terrain.source}
onChange={this.changeTerrainProperty.bind(this, "source")}
/>
<FieldNumber
{...inputProps}
label={"Transition delay"}
label={t("Terrain exaggeration")}
fieldSpec={latest.terrain.exaggeration}
value={terrain.exaggeration}
default={latest.terrain.exaggeration.default}
onChange={this.changeTerrainProperty.bind(this, "exaggeration")}
/>
<FieldNumber
label={t("Transition delay")}
fieldSpec={latest.transition.delay}
value={transition.delay}
default={latest.transition.delay.default}
@@ -222,21 +251,20 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldNumber
{...inputProps}
label={"Transition duration"}
label={t("Transition duration")}
fieldSpec={latest.transition.duration}
value={transition.duration}
default={latest.transition.duration.default}
onChange={this.changeTransitionProperty.bind(this, "duration")}
/>
<FieldSelect {...inputProps}
label={fieldSpecAdditional.maputnik.style_renderer.label}
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
<FieldSelect
label={fsa.maputnik.style_renderer.label}
fieldSpec={fsa.maputnik.style_renderer}
data-wd-key="modal:settings.maputnik:renderer"
options={[
['mlgljs', 'MapLibreGL JS'],
['ol', 'Open Layers (experimental)'],
['ol', t('Open Layers (experimental)')],
]}
value={metadata['maputnik:renderer'] || 'mlgljs'}
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')}
@@ -246,3 +274,5 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
}
}
const ModalSettings = withTranslation()(ModalSettingsInternal)
export default ModalSettings;

View File

@@ -1,48 +1,50 @@
import React from 'react'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal'
type ModalShortcutsProps = {
type ModalShortcutsInternalProps = {
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
};
} & WithTranslation;
export default class ModalShortcuts extends React.Component<ModalShortcutsProps> {
class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps> {
render() {
const t = this.props.t;
const help = [
{
key: <kbd>?</kbd>,
text: "Shortcuts menu"
text: t("Shortcuts menu")
},
{
key: <kbd>o</kbd>,
text: "Open modal"
text: t("Open modal")
},
{
key: <kbd>e</kbd>,
text: "Export modal"
text: t("Export modal")
},
{
key: <kbd>d</kbd>,
text: "Data Sources modal"
text: t("Data Sources modal")
},
{
key: <kbd>s</kbd>,
text: "Style Settings modal"
text: t("Style Settings modal")
},
{
key: <kbd>i</kbd>,
text: "Toggle inspect"
text: t("Toggle inspect")
},
{
key: <kbd>m</kbd>,
text: "Focus map"
text: t("Focus map")
},
{
key: <kbd>!</kbd>,
text: "Debug modal"
text: t("Debug modal")
},
]
@@ -50,51 +52,51 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
const mapShortcuts = [
{
key: <kbd>+</kbd>,
text: "Increase the zoom level by 1.",
text: t("Increase the zoom level by 1.",)
},
{
key: <><kbd>Shift</kbd> + <kbd>+</kbd></>,
text: "Increase the zoom level by 2.",
text: t("Increase the zoom level by 2.",)
},
{
key: <kbd>-</kbd>,
text: "Decrease the zoom level by 1.",
text: t("Decrease the zoom level by 1.",)
},
{
key: <><kbd>Shift</kbd> + <kbd>-</kbd></>,
text: "Decrease the zoom level by 2.",
text: t("Decrease the zoom level by 2.",)
},
{
key: <kbd>Up</kbd>,
text: "Pan up by 100 pixels.",
text: t("Pan up by 100 pixels.",)
},
{
key: <kbd>Down</kbd>,
text: "Pan down by 100 pixels.",
text: t("Pan down by 100 pixels.",)
},
{
key: <kbd>Left</kbd>,
text: "Pan left by 100 pixels.",
text: t("Pan left by 100 pixels.",)
},
{
key: <kbd>Right</kbd>,
text: "Pan right by 100 pixels.",
text: t("Pan right by 100 pixels.",)
},
{
key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>,
text: "Increase the rotation by 15 degrees.",
text: t("Increase the rotation by 15 degrees.",)
},
{
key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>,
text: "Decrease the rotation by 15 degrees."
text: t("Decrease the rotation by 15 degrees.")
},
{
key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>,
text: "Increase the pitch by 10 degrees."
text: t("Increase the pitch by 10 degrees.")
},
{
key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>,
text: "Decrease the pitch by 10 degrees."
text: t("Decrease the pitch by 10 degrees.")
},
]
@@ -103,11 +105,13 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
data-wd-key="modal:shortcuts"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Shortcuts'}
title={t('Shortcuts')}
>
<section className="maputnik-modal-section maputnik-modal-shortcuts">
<p>
Press <code>ESC</code> to lose focus of any active elements, then press one of:
<Trans t={t}>
Press <code>ESC</code> to lose focus of any active elements, then press one of:
</Trans>
</p>
<dl>
{help.map((item, idx) => {
@@ -117,7 +121,7 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
</div>
})}
</dl>
<p>If the Map is in focused you can use the following shortcuts</p>
<p>{t("If the Map is in focused you can use the following shortcuts")}</p>
<ul>
{mapShortcuts.map((item, idx) => {
return <li key={idx}>
@@ -130,3 +134,5 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
}
}
const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
export default ModalShortcuts;

View File

@@ -1,7 +1,8 @@
import React from 'react'
import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal'
import InputButton from './InputButton'
@@ -74,16 +75,17 @@ type ActiveModalSourcesTypeEditorProps = {
source: SourceSpecification
onDelete(...args: unknown[]): unknown
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTypeEditorProps> {
render() {
const t = this.props.t;
return <div className="maputnik-active-source-type-editor">
<div className="maputnik-active-source-type-editor-header">
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
<span className="maputnik-space" />
<InputButton
aria-label={`Remove '${this.props.sourceId}' source`}
aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})}
className="maputnik-active-source-type-editor-header-delete"
onClick={()=> this.props.onDelete(this.props.sourceId)}
style={{backgroundColor: 'transparent'}}
@@ -104,7 +106,7 @@ class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTyp
type AddSourceProps = {
onAdd(...args: unknown[]): unknown
};
} & WithTranslation;
type AddSourceState = {
mode: EditorMode
@@ -134,7 +136,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
case 'geojson_json': return {
type: 'geojson',
cluster: (source as GeoJSONSourceSpecification).cluster || false,
data: {}
data: ''
}
case 'tilejson_vector': return {
type: 'vector',
@@ -202,6 +204,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
}
render() {
const t = this.props.t;
// Kind of a hack because the type changes, however maputnik has 1..n
// options per type, for example
//
@@ -215,25 +218,25 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
return <div className="maputnik-add-source">
<FieldString
label={"Source ID"}
fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}
label={t("Source ID")}
fieldSpec={{doc: t("Unique ID that identifies the source and is used in the layer to reference the source.")}}
value={this.state.sourceId}
onChange={(v: string) => this.setState({ sourceId: v})}
/>
<FieldSelect
label={"Source Type"}
label={t("Source Type")}
fieldSpec={sourceTypeFieldSpec}
options={[
['geojson_json', 'GeoJSON (JSON)'],
['geojson_url', 'GeoJSON (URL)'],
['tilejson_vector', 'Vector (TileJSON URL)'],
['tilexyz_vector', 'Vector (XYZ URLs)'],
['tilejson_raster', 'Raster (TileJSON URL)'],
['tilexyz_raster', 'Raster (XYZ URL)'],
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
['image', 'Image'],
['video', 'Video'],
['geojson_json', t('GeoJSON (JSON)')],
['geojson_url', t('GeoJSON (URL)')],
['tilejson_vector', t('Vector (TileJSON URL)')],
['tilexyz_vector', t('Vector (XYZ URLs)')],
['tilejson_raster', t('Raster (TileJSON URL)')],
['tilexyz_raster', t('Raster (XYZ URL)')],
['tilejson_raster-dem', t('Raster DEM (TileJSON URL)')],
['tilexyz_raster-dem', t('Raster DEM (XYZ URLs)')],
['image', t('Image')],
['video', t('Video')],
]}
onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
value={this.state.mode as string}
@@ -247,20 +250,20 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
className="maputnik-add-source-button"
onClick={this.onAdd}
>
Add Source
{t("Add Source")}
</InputButton>
</div>
}
}
type ModalSourcesProps = {
type ModalSourcesInternalProps = {
mapStyle: StyleSpecification
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
onStyleChanged(...args: unknown[]): unknown
};
} & WithTranslation;
export default class ModalSources extends React.Component<ModalSourcesProps> {
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
const strippedSource = {...source}
delete strippedSource['title']
@@ -268,7 +271,8 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
}
render() {
const mapStyle = this.props.mapStyle
const {t, mapStyle} = this.props;
const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady};
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
const source = mapStyle.sources[sourceId]
return <ActiveModalSourcesTypeEditor
@@ -277,6 +281,7 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
source={source}
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
{...i18nProps}
/>
})
@@ -295,17 +300,17 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
data-wd-key="modal:sources"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Sources'}
title={t('Sources')}
>
<section className="maputnik-modal-section">
<h1>Active Sources</h1>
<h1>{t("Active Sources")}</h1>
{activeSources}
</section>
<section className="maputnik-modal-section">
<h1>Choose Public Source</h1>
<h1>{t("Choose Public Source")}</h1>
<p>
Add one of the publicly available sources to your style.
{t("Add one of the publicly available sources to your style.")}
</p>
<div className="maputnik-public-sources" style={{maxWidth: 500}}>
{tilesetOptions}
@@ -313,13 +318,16 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
</section>
<section className="maputnik-modal-section">
<h1>Add New Source</h1>
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
<h1>{t("Add New Source")}</h1>
<p>{t("Add a new source to your style. You can only choose the source type and id at creation time!")}</p>
<AddSource
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
{...i18nProps}
/>
</section>
</Modal>
}
}
const ModalSources = withTranslation()(ModalSourcesInternal);
export default ModalSources;

View File

@@ -8,6 +8,8 @@ import FieldDynamicArray from './FieldDynamicArray'
import FieldArray from './FieldArray'
import FieldJson from './FieldJson'
import FieldCheckbox from './FieldCheckbox'
import { WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next'
export type EditorMode = "video" | "image" | "tilejson_vector" | "tilexyz_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "tilexyz_vector" | "geojson_url" | "geojson_json" | null;
@@ -17,14 +19,15 @@ type TileJSONSourceEditorProps = {
}
onChange(...args: unknown[]): unknown
children?: React.ReactNode
};
} & WithTranslation;
class TileJSONSourceEditor extends React.Component<TileJSONSourceEditorProps> {
render() {
const t = this.props.t;
return <div>
<FieldUrl
label={"TileJSON URL"}
label={t("TileJSON URL")}
fieldSpec={latest.source_vector.url}
value={this.props.source.url}
onChange={url => this.props.onChange({
@@ -45,7 +48,7 @@ type TileURLSourceEditorProps = {
}
onChange(...args: unknown[]): unknown
children?: React.ReactNode
};
} & WithTranslation;
class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
changeTileUrls(tiles: string[]) {
@@ -58,7 +61,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
renderTileUrls() {
const tiles = this.props.source.tiles || [];
return <FieldDynamicArray
label={"Tile URL"}
label={this.props.t("Tile URL")}
fieldSpec={latest.source_vector.tiles}
type="url"
value={tiles}
@@ -67,10 +70,11 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
}
render() {
const t = this.props.t;
return <div>
{this.renderTileUrls()}
<FieldNumber
label={"Min Zoom"}
label={t("Min Zoom")}
fieldSpec={latest.source_vector.minzoom}
value={this.props.source.minzoom || 0}
onChange={minzoom => this.props.onChange({
@@ -79,7 +83,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
})}
/>
<FieldNumber
label={"Max Zoom"}
label={t("Max Zoom")}
fieldSpec={latest.source_vector.maxzoom}
value={this.props.source.maxzoom || 22}
onChange={maxzoom => this.props.onChange({
@@ -93,16 +97,24 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
}
}
const createCornerLabels: (t: TFunction) => { label: string, key: string }[] = (t) => ([
{ label: t("Coord top left"), key: "top left" },
{ label: t("Coord top right"), key: "top right" },
{ label: t("Coord bottom right"), key: "bottom right" },
{ label: t("Coord bottom left"), key: "bottom left" },
]);
type ImageSourceEditorProps = {
source: {
coordinates: [number, number][]
url: string
}
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
render() {
const t = this.props.t;
const changeCoord = (idx: number, val: [number, number]) => {
const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
@@ -115,7 +127,7 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
return <div>
<FieldUrl
label={"Image URL"}
label={t("Image URL")}
fieldSpec={latest.source_image.url}
value={this.props.source.url}
onChange={url => this.props.onChange({
@@ -123,11 +135,11 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
url,
})}
/>
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
{createCornerLabels(t).map(({label, key}, idx) => {
return (
<FieldArray
label={`Coord ${label}`}
key={label}
label={label}
key={key}
length={2}
type="number"
value={this.props.source.coordinates[idx]}
@@ -146,10 +158,11 @@ type VideoSourceEditorProps = {
urls: string[]
}
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
render() {
const t = this.props.t;
const changeCoord = (idx: number, val: [number, number]) => {
const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
@@ -169,18 +182,18 @@ class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
return <div>
<FieldDynamicArray
label={"Video URL"}
label={t("Video URL")}
fieldSpec={latest.source_video.urls}
type="string"
value={this.props.source.urls}
default={[]}
onChange={changeUrls}
/>
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
{createCornerLabels(t).map(({label, key}, idx) => {
return (
<FieldArray
label={`Coord ${label}`}
key={label}
label={label}
key={key}
length={2}
type="number"
value={this.props.source.coordinates[idx]}
@@ -198,12 +211,13 @@ type GeoJSONSourceUrlEditorProps = {
data: string
}
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
class GeoJSONSourceUrlEditor extends React.Component<GeoJSONSourceUrlEditorProps> {
render() {
const t = this.props.t;
return <FieldUrl
label={"GeoJSON URL"}
label={t("GeoJSON URL")}
fieldSpec={latest.source_geojson.data}
value={this.props.source.data}
onChange={data => this.props.onChange({
@@ -220,12 +234,13 @@ type GeoJSONSourceFieldJsonEditorProps = {
cluster: boolean
}
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJsonEditorProps> {
render() {
const t = this.props.t;
return <div>
<Block label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}>
<FieldJson
layer={this.props.source.data}
maxHeight={200}
@@ -243,7 +258,7 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
/>
</Block>
<FieldCheckbox
label={'Cluster'}
label={t('Cluster')}
value={this.props.source.cluster}
onChange={cluster => {
this.props.onChange({
@@ -256,18 +271,22 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
}
}
type ModalSourcesTypeEditorProps = {
type ModalSourcesTypeEditorInternalProps = {
mode: EditorMode
source: any
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
export default class ModalSourcesTypeEditor extends React.Component<ModalSourcesTypeEditorProps> {
class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEditorInternalProps> {
render() {
const t = this.props.t;
const commonProps = {
source: this.props.source,
onChange: this.props.onChange,
}
t: this.props.t,
i18n: this.props.i18n,
tReady: this.props.tReady,
};
switch(this.props.mode) {
case 'geojson_url': return <GeoJSONSourceUrlEditor {...commonProps} />
case 'geojson_json': return <GeoJSONSourceFieldJsonEditor {...commonProps} />
@@ -278,7 +297,7 @@ export default class ModalSourcesTypeEditor extends React.Component<ModalSources
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
<FieldSelect
label={"Encoding"}
label={t("Encoding")}
fieldSpec={latest.source_raster_dem.encoding}
options={Object.keys(latest.source_raster_dem.encoding.values)}
onChange={encoding => this.props.onChange({
@@ -295,3 +314,5 @@ export default class ModalSourcesTypeEditor extends React.Component<ModalSources
}
}
const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
export default ModalSourcesTypeEditor;

View File

@@ -1,38 +0,0 @@
import React from 'react'
import InputButton from './InputButton'
import Modal from './Modal'
// @ts-ignore
import logoImage from 'maputnik-design/logos/logo-color.svg'
type ModalSurveyProps = {
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
};
export default class ModalSurvey extends React.Component<ModalSurveyProps> {
onClick = () => {
window.open('https://gregorywolanski.typeform.com/to/cPgaSY', '_blank');
this.props.onOpenToggle();
}
render() {
return <Modal
data-wd-key="modal:survey"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title="Maputnik Survey"
>
<div className="maputnik-modal-survey">
<img src={logoImage} className="maputnik-modal-survey__logo" />
<h1>You + Maputnik = Maputnik better for you</h1>
<p className="maputnik-modal-survey__description">We dont track you, so we dont know how you use Maputnik. Help us make Maputnik better for you by completing a 7minute survey carried out by our contributing designer.</p>
<InputButton onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</InputButton>
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
</div>
</Modal>
}
}

View File

@@ -1,18 +1,22 @@
import React from 'react'
import { WithTranslation, withTranslation } from 'react-i18next';
import './SmallError.scss';
type SmallErrorProps = {
type SmallErrorInternalProps = {
children?: React.ReactNode
};
} & WithTranslation;
export default class SmallError extends React.Component<SmallErrorProps> {
class SmallErrorInternal extends React.Component<SmallErrorInternalProps> {
render () {
const t = this.props.t;
return (
<div className="SmallError">
Error: {this.props.children}
{t("Error:")} {this.props.children}
</div>
);
}
}
}
const SmallError = withTranslation()(SmallErrorInternal);
export default SmallError;

View File

@@ -1,6 +1,6 @@
import React from 'react'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import {latest} from '@maplibre/maplibre-gl-style-spec'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import InputButton from './InputButton'
import InputSpec from './InputSpec'
@@ -11,13 +11,14 @@ import Block from './Block'
import docUid from '../libs/document-uid'
import sortNumerically from '../libs/sort-numerically'
import {findDefaultFromSpec} from '../libs/spec-helper';
import { WithTranslation, withTranslation } from 'react-i18next';
import labelFromFieldName from '../libs/label-from-field-name'
import DeleteStopButton from './_DeleteStopButton'
function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState) {
// This is initialsed below only if required to improved performance.
let newRefs: {[key: number]: string} | undefined;
@@ -35,7 +36,7 @@ function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
return newRefs;
}
type DataPropertyProps = {
type DataPropertyInternalProps = {
onChange?(fieldName: string, value: any): unknown
onDeleteStop?(...args: unknown[]): unknown
onAddStop?(...args: unknown[]): unknown
@@ -46,7 +47,7 @@ type DataPropertyProps = {
fieldSpec?: object
value?: DataPropertyValue
errors?: object
};
} & WithTranslation;
type DataPropertyState = {
refs: {[key: number]: string}
@@ -65,7 +66,7 @@ export type Stop = [{
value: number
}, number]
export default class DataProperty extends React.Component<DataPropertyProps, DataPropertyState> {
class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> {
state = {
refs: {} as {[key: number]: string}
}
@@ -80,7 +81,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
}
}
static getDerivedStateFromProps(props: DataPropertyProps, state: DataPropertyState) {
static getDerivedStateFromProps(props: Readonly<DataPropertyInternalProps>, state: DataPropertyState) {
const newRefs = setStopRefs(props, state);
if(newRefs) {
return {
@@ -213,6 +214,8 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
}
render() {
const t = this.props.t;
if (typeof this.props.value?.type === "undefined") {
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec)
}
@@ -227,8 +230,8 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
const dataProps = {
'aria-label': "Input value",
label: "Data value",
'aria-label': t("Input value"),
label: t("Data value"),
value: dataLevel as any,
onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
}
@@ -263,7 +266,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
</td>
<td>
<InputSpec
aria-label="Output value"
aria-label={t("Output value")}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={value}
@@ -282,27 +285,27 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner">
<Block
label={"Function"}
label={t("Function")}
key="function"
>
<div className="maputnik-data-spec-property-input">
<InputSelect
value={this.props.value!.type}
onChange={(propVal: string) => this.changeDataType(propVal)}
title={"Select a type of data scale (default is 'categorical')."}
title={t("Select a type of data scale (default is 'categorical').")}
options={this.getDataFunctionTypes(this.props.fieldSpec)}
/>
</div>
</Block>
{this.props.value?.type !== "identity" &&
<Block
label={"Base"}
label={t("Base")}
key="base"
>
<div className="maputnik-data-spec-property-input">
<InputSpec
fieldName={"base"}
fieldSpec={latest.function.base}
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
value={this.props.value?.base}
onChange={(_, newValue) => this.changeBase(newValue as number)}
/>
@@ -316,14 +319,14 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
<div className="maputnik-data-spec-property-input">
<InputString
value={this.props.value?.property}
title={"Input a data property to base styles off of."}
title={t("Input a data property to base styles off of.")}
onChange={propVal => this.changeDataProperty("property", propVal)}
/>
</div>
</Block>
{dataFields &&
<Block
label={"Default"}
label={t("Default")}
key="default"
>
<InputSpec
@@ -337,12 +340,12 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
{dataFields &&
<div className="maputnik-function-stop">
<table className="maputnik-function-stop-table">
<caption>Stops</caption>
<caption>{t("Stops")}</caption>
<thead>
<tr>
<th>Zoom</th>
<th>Input value</th>
<th rowSpan={2}>Output value</th>
<th>{t("Zoom")}</th>
<th>{t("Input value")}</th>
<th rowSpan={2}>{t("Output value")}</th>
</tr>
</thead>
<tbody>
@@ -359,7 +362,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add stop
</svg> {t("Add stop")}
</InputButton>
}
<InputButton
@@ -368,7 +371,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> Convert to expression
</svg> {t("Convert to expression")}
</InputButton>
</div>
</div>
@@ -376,3 +379,6 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
</div>
}
}
const DataProperty = withTranslation()(DataPropertyInternal);
export default DataProperty;

View File

@@ -2,21 +2,26 @@ import React from 'react'
import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
type DeleteStopButtonProps = {
type DeleteStopButtonInternalProps = {
onClick?(...args: unknown[]): unknown
};
} & WithTranslation;
export default class DeleteStopButton extends React.Component<DeleteStopButtonProps> {
class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalProps> {
render() {
const t = this.props.t;
return <InputButton
className="maputnik-delete-stop"
onClick={this.props.onClick}
title={"Remove zoom level from stop"}
title={t("Remove zoom level from stop")}
>
<MdDelete />
</InputButton>
}
}
const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
export default DeleteStopButton;

View File

@@ -1,6 +1,7 @@
import React from 'react'
import {MdDelete, MdUndo} from 'react-icons/md'
import stringifyPretty from 'json-stringify-pretty-compact'
import { WithTranslation, withTranslation } from 'react-i18next';
import Block from './Block'
import InputButton from './InputButton'
@@ -8,7 +9,7 @@ import labelFromFieldName from '../libs/label-from-field-name'
import FieldJson from './FieldJson'
type ExpressionPropertyProps = {
type ExpressionPropertyInternalProps = {
onDelete?(...args: unknown[]): unknown
fieldName: string
fieldType?: string
@@ -20,20 +21,20 @@ type ExpressionPropertyProps = {
canUndo?(...args: unknown[]): unknown
onFocus?(...args: unknown[]): unknown
onBlur?(...args: unknown[]): unknown
};
} & WithTranslation;
type ExpressionPropertyState = {
jsonError: boolean
};
export default class ExpressionProperty extends React.Component<ExpressionPropertyProps, ExpressionPropertyState> {
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps, ExpressionPropertyState> {
static defaultProps = {
errors: {},
onFocus: () => {},
onBlur: () => {},
}
constructor (props:ExpressionPropertyProps) {
constructor(props: ExpressionPropertyInternalProps) {
super(props);
this.state = {
jsonError: false,
@@ -53,7 +54,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
}
render() {
const {errors, fieldName, fieldType, value, canUndo} = this.props;
const {t, errors, fieldName, fieldType, value, canUndo} = this.props;
const {jsonError} = this.state;
const undoDisabled = canUndo ? !canUndo() : true;
@@ -65,7 +66,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
onClick={this.props.onUndo}
disabled={undoDisabled}
className="maputnik-delete-stop"
title="Revert from expression"
title={t("Revert from expression")}
>
<MdUndo />
</InputButton>
@@ -74,7 +75,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
key="delete_action"
onClick={this.props.onDelete}
className="maputnik-delete-stop"
title="Delete expression"
title={t("Delete expression")}
>
<MdDelete />
</InputButton>
@@ -112,7 +113,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
// this feels like an incorrect type...? `foundErrors` is an array of objects, not a single object
error={foundErrors as any}
fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(this.props.fieldName)}
label={t(labelFromFieldName(this.props.fieldName))}
action={deleteStopBtn}
wideMode={true}
>
@@ -137,3 +138,6 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
</Block>
}
}
const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
export default ExpressionProperty;

View File

@@ -3,16 +3,18 @@ import React from 'react'
import InputButton from './InputButton'
import {MdFunctions, MdInsertChart} from 'react-icons/md'
import {mdiFunctionVariant} from '@mdi/js';
import { WithTranslation, withTranslation } from 'react-i18next';
type FunctionInputButtonsProps = {
type FunctionInputButtonsInternalProps = {
fieldSpec?: any
onZoomClick?(...args: unknown[]): unknown
onDataClick?(...args: unknown[]): unknown
onExpressionClick?(...args: unknown[]): unknown
};
} & WithTranslation;
export default class FunctionInputButtons extends React.Component<FunctionInputButtonsProps> {
class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> {
render() {
const t = this.props.t;
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
@@ -20,7 +22,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
<InputButton
className="maputnik-make-zoom-function"
onClick={this.props.onExpressionClick}
title="Convert to expression"
title={t("Convert to expression")}
>
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
@@ -31,7 +33,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
makeZoomInputButton = <InputButton
className="maputnik-make-zoom-function"
onClick={this.props.onZoomClick}
title="Convert property into a zoom function"
title={t("Convert property into a zoom function")}
>
<MdFunctions />
</InputButton>
@@ -40,7 +42,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
makeDataInputButton = <InputButton
className="maputnik-make-data-function"
onClick={this.props.onDataClick}
title="Convert property to data function"
title={t("Convert property to data function")}
>
<MdInsertChart />
</InputButton>
@@ -56,3 +58,6 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
}
}
}
const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
export default FunctionInputButtons;

View File

@@ -1,6 +1,7 @@
import React from 'react'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import {latest} from '@maplibre/maplibre-gl-style-spec'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import { WithTranslation, withTranslation } from 'react-i18next';
import InputButton from './InputButton'
import InputSpec from './InputSpec'
@@ -20,7 +21,7 @@ import sortNumerically from '../libs/sort-numerically'
*
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
*/
function setStopRefs(props: ZoomPropertyProps, state: ZoomPropertyState) {
function setStopRefs(props: ZoomPropertyInternalProps, state: ZoomPropertyState) {
// This is initialsed below only if required to improved performance.
let newRefs: {[key: number]: string} = {};
@@ -45,7 +46,7 @@ type ZoomWithStops = {
}
type ZoomPropertyProps = {
type ZoomPropertyInternalProps = {
onChange?(...args: unknown[]): unknown
onChangeToDataFunction?(...args: unknown[]): unknown
onDeleteStop?(...args: unknown[]): unknown
@@ -59,13 +60,13 @@ type ZoomPropertyProps = {
}
errors?: object
value?: ZoomWithStops
};
} & WithTranslation;
type ZoomPropertyState = {
refs: {[key: number]: string}
}
export default class ZoomProperty extends React.Component<ZoomPropertyProps, ZoomPropertyState> {
class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> {
static defaultProps = {
errors: {},
}
@@ -84,7 +85,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
}
}
static getDerivedStateFromProps(props: ZoomPropertyProps, state: ZoomPropertyState) {
static getDerivedStateFromProps(props: Readonly<ZoomPropertyInternalProps>, state: ZoomPropertyState) {
const newRefs = setStopRefs(props, state);
if(newRefs) {
return {
@@ -152,17 +153,17 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
}
render() {
const t = this.props.t;
const zoomFields = this.props.value?.stops.map((stop, idx) => {
const zoomLevel = stop[0]
const key = this.state.refs[idx];
const value = stop[1]
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
return <tr
key={key}
key={`${stop[0]}-${stop[1]}`}
>
<td>
<InputNumber
aria-label="Zoom"
aria-label={t("Zoom")}
value={zoomLevel}
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
min={0}
@@ -171,7 +172,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
</td>
<td>
<InputSpec
aria-label="Output value"
aria-label={t("Output value")}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec as any}
value={value}
@@ -190,24 +191,24 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner">
<Block
label={"Function"}
label={t("Function")}
>
<div className="maputnik-data-spec-property-input">
<InputSelect
value={"interpolate"}
onChange={(propVal: string) => this.changeDataType(propVal)}
title={"Select a type of data scale (default is 'categorical')."}
title={t("Select a type of data scale (default is 'categorical').")}
options={this.getDataFunctionTypes(this.props.fieldSpec!)}
/>
</div>
</Block>
<Block
label={"Base"}
label={t("Base")}
>
<div className="maputnik-data-spec-property-input">
<InputSpec
fieldName={"base"}
fieldSpec={latest.function.base}
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
value={this.props.value?.base}
onChange={(_, newValue) => this.changeBase(newValue as number | undefined)}
/>
@@ -215,11 +216,11 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
</Block>
<div className="maputnik-function-stop">
<table className="maputnik-function-stop-table maputnik-function-stop-table--zoom">
<caption>Stops</caption>
<caption>{t("Stops")}</caption>
<thead>
<tr>
<th>Zoom</th>
<th rowSpan={2}>Output value</th>
<th>{t("Zoom")}</th>
<th rowSpan={2}>{t("Output value")}</th>
</tr>
</thead>
<tbody>
@@ -234,7 +235,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add stop
</svg> {t("Add stop")}
</InputButton>
<InputButton
className="maputnik-add-stop"
@@ -242,7 +243,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> Convert to expression
</svg> {t("Convert to expression")}
</InputButton>
</div>
</div>
@@ -262,3 +263,6 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
}
}
}
const ZoomProperty = withTranslation()(ZoomPropertyInternal);
export default ZoomProperty;

View File

@@ -1,52 +1,28 @@
[
{
"id": "osm-liberty",
"title": "OSM Liberty",
"url": "https://maputnik.github.io/osm-liberty/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png"
},
{
"id": "maptiler-basic-gl-style",
"title": "Maptiler Basic",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
"id": "0-empty-style",
"title": "Empty Style",
"url": "https://cdn.jsdelivr.net/gh/maputnik/editor@9cf74ca405d2be0608b57db8109cf3a6af5b9f49/src/config/empty-style.json",
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
},
{
"id": "dark-matter",
"title": "Dark Matter",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.8/style.json",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
},
{
"id": "positron",
"title": "Positron",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.8/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
},
{
"id": "osm-bright",
"title": "OSM Bright",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
"id": "maptiler-basic-gl-style",
"title": "Maptiler Basic",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.10/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
},
{
"id": "maptiler-toner-gl-style",
"title": "Toner",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@339e5b7/style.json",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@v1.0/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
},
{
"id": "os-zoomstack-outdoor",
"title": "Zoomstack Outdoor",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-outdoor/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-outdoor.png"
},
{
"id": "os-zoomstack-road",
"title": "Zoomstack Road",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-road/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
},
{
"id": "os-zoomstack-light",
"title": "Zoomstack Light",
@@ -60,9 +36,45 @@
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
},
{
"id": "empty-style",
"title": "Empty Style",
"url": "https://cdn.jsdelivr.net/gh/maputnik/editor@9cf74ca405d2be0608b57db8109cf3a6af5b9f49/src/config/empty-style.json",
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
"id": "os-zoomstack-outdoor",
"title": "Zoomstack Outdoor",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-outdoor/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-outdoor.png"
},
{
"id": "os-zoomstack-road",
"title": "Zoomstack Road",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-road/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
},
{
"id": "osm-bright",
"title": "OSM Bright",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.11/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
},
{
"id": "osm-liberty",
"title": "OSM Liberty",
"url": "https://maputnik.github.io/osm-liberty/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png"
},
{
"id": "positron",
"title": "Positron",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
},
{
"id": "stadia-outdoors",
"title": "Stadia Outdoors",
"url": "https://tiles.stadiamaps.com/styles/outdoors.json",
"thumbnail": "https://tiles.stadiamaps.com/static/outdoors.png?size=480x320&center=47.350259,8.49035&zoom=16"
},
{
"id": "versatiles-colorful",
"title": "Versatiles Colorful",
"url": "https://tiles.versatiles.org/assets/styles/colorful.json",
"thumbnail": "https://github.com/maplibre/maputnik/assets/649392/6cd69818-c541-46e4-a920-65fb4f654931"
}
]

View File

@@ -1,7 +1,7 @@
{
"openmaptiles": {
"type": "vector",
"url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}",
"url": "https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key={key}",
"title": "OpenMapTiles v3"
},
"thunderforest_transport": {

View File

@@ -1,4 +1,4 @@
{
"openmaptiles": "KDhMfHvorAFkFe64wlZb",
"openmaptiles": "get_your_own_OpIi9ZULNHzrESv6T2vL",
"thunderforest": "b71f7f0ba4064f5eb9e903859a9cf5c6"
}

40
src/i18n.ts Normal file
View File

@@ -0,0 +1,40 @@
import i18n from "i18next";
import detector from "i18next-browser-languagedetector";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next";
export const supportedLanguages = {
"en": "English",
"ja": "日本語",
"he": "עברית",
"zh": "简体中文"
} as const;
i18n
.use(detector) // detect user language from browser settings
.use(
resourcesToBackend((lang: string, ns: string) => {
if (lang === "en") {
// English is the default language, so we don't need to load any resources for it.
return {};
}
return import(`./locales/${lang}/${ns}.json`);
})
)
.use(initReactI18next) // required to initialize react-i18next
.init({
supportedLngs: Object.keys(supportedLanguages),
keySeparator: false, // we do not use keys in form messages.welcome
nsSeparator: false,
interpolation: {
escapeValue: false // React already escapes for us
},
saveMissing: true, // this needs to be set for missingKeyHandler to work
fallbackLng: false, // we set the fallback to false so we can get the correct language in the missingKeyHandler
missingKeyHandler: (lngs, _ns, key) => {
if (lngs[0] === "en") { return; }
console.warn(`Missing translation for "${key}" in "${lngs.join(", ")}"`);
}
});
export default i18n;

View File

@@ -1,15 +1,16 @@
import { IconContext } from "react-icons";
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import './favicon.ico'
import './styles/index.scss'
import './i18n';
import App from './components/App';
ReactDOM.render(
const root = createRoot(document.querySelector("#app"));
root.render(
<IconContext.Provider value={{className: 'react-icons'}}>
<App/>
</IconContext.Provider>,
document.querySelector("#app")
</IconContext.Provider>
);
// Hide the loader.

View File

@@ -2,7 +2,7 @@
import jsonlint from 'jsonlint';
import CodeMirror, { MarkerRange } from 'codemirror';
import jsonToAst from 'json-to-ast';
import {expression, validate} from '@maplibre/maplibre-gl-style-spec';
import {expression, validateStyleMin} from '@maplibre/maplibre-gl-style-spec';
type MarkerRangeWithMessage = MarkerRange & {message: string};
@@ -102,7 +102,7 @@ CodeMirror.registerHelper("lint", "mgl", (text: string, opts: any, doc: any) =>
let out: ReturnType<typeof expression.createExpression> | null = null;
if (context === "layer") {
// Just an empty style so we can validate a layer.
const errors = validate({
const errors = validateStyleMin({
"version": 8,
"name": "Empty Style",
"metadata": {},

View File

@@ -1,18 +1,20 @@
const spec = {
import { TFunction } from "i18next";
const spec = (t: TFunction) => ({
maputnik: {
maptiler_access_token: {
label: "MapTiler Access Token",
doc: "Public access token for MapTiler Cloud."
label: t("MapTiler Access Token"),
doc: t("Public access token for MapTiler Cloud.")
},
thunderforest_access_token: {
label: "Thunderforest Access Token",
doc: "Public access token for Thunderforest services."
label: t("Thunderforest Access Token"),
doc: t("Public access token for Thunderforest services.")
},
style_renderer: {
label: "Style Renderer",
doc: "Choose the default Maputnik renderer for this style.",
label: t("Style Renderer"),
doc: t("Choose the default Maputnik renderer for this style."),
},
}
}
})
export default spec;

View File

@@ -1,4 +1,4 @@
import {latest} from '@maplibre/maplibre-gl-style-spec'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
export const combiningFilterOps = ['all', 'any', 'none'];
export const setFilterOps = ['in', '!in'];

View File

@@ -1,7 +1,5 @@
// @ts-ignore
import stylegen from 'mapbox-gl-inspect/lib/stylegen'
// @ts-ignore
import colors from 'mapbox-gl-inspect/lib/colors'
import stylegen from '@maplibre/maplibre-gl-inspect/lib/stylegen'
import colors from '@maplibre/maplibre-gl-inspect/lib/colors'
import type {FilterSpecification,LayerSpecification } from 'maplibre-gl'
export type HighlightedLayer = LayerSpecification & {filter?: FilterSpecification};

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