mapshaper 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mapshaper.js CHANGED
@@ -52127,7 +52127,7 @@ ${svg}
52127
52127
  });
52128
52128
  }
52129
52129
 
52130
- var version = "0.7.1";
52130
+ var version = "0.7.2";
52131
52131
 
52132
52132
  // Parse command line args into commands and run them
52133
52133
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "A tool for editing vector datasets for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
@@ -26,6 +26,7 @@
26
26
  "test": "mocha test",
27
27
  "build": "rollup --config",
28
28
  "docs": "node build-docs.mjs",
29
+ "roadmap": "node build-roadmap.mjs",
29
30
  "lint": "eslint --ext mjs src/",
30
31
  "prepublishOnly": "npm test; ./pre-publish",
31
32
  "postpublish": "./release_web_ui; ./release_github_version",
package/www/.nojekyll ADDED
File without changes
@@ -40,10 +40,15 @@ code, pre, .mono {
40
40
  .page-header {
41
41
  background-color: #1385B7;
42
42
  color: white;
43
- padding: 0 14px;
44
- height: 36px;
43
+ /* Asymmetric vertical padding (4px top, 8px bottom) optically centers the
44
+ baseline-aligned row: ascenders are taller than descenders, so equal
45
+ padding makes the text look ~2px low. */
46
+ padding: 2px 14px 8px;
47
+ height: 32px;
45
48
  display: flex;
46
- align-items: center;
49
+ /* baseline-align so the 18px logo and the 14px nav text share a baseline,
50
+ not just a vertical center */
51
+ align-items: baseline;
47
52
  gap: 14px;
48
53
  }
49
54
 
@@ -76,7 +81,10 @@ code, pre, .mono {
76
81
  color: white;
77
82
  text-decoration: none;
78
83
  font-size: 14px;
79
- padding: 6px 10px;
84
+ /* line-height: 1 keeps each anchor's hover background tight enough to fit
85
+ inside the 36px header once the row is baseline-aligned. */
86
+ line-height: 1;
87
+ padding: 6px 6px;
80
88
  border-radius: 3px;
81
89
  }
82
90
 
@@ -165,7 +173,9 @@ code, pre, .mono {
165
173
 
166
174
  .docs-nav a {
167
175
  display: block;
168
- padding: 4px 8px;
176
+ /* Asymmetric vertical padding (6px top, 2px bottom) optically centers
177
+ the text inside the active/hover background. */
178
+ padding: 5px 8px 2px;
169
179
  border-radius: 3px;
170
180
  color: #333;
171
181
  font-size: 14px;
@@ -33,7 +33,7 @@
33
33
  <article class="docs-article">
34
34
  <div class="docs-breadcrumbs"><a href="/docs/">Docs</a> &rsaquo; Examples &rsaquo; Basics</div>
35
35
  <h1 id="basics">Basics</h1>
36
- <p>Short Mapshaper recipes for common GIS tasks. The filenames and field names are illustrative &mdash; substitute your own. Most recipes also work in the <a href="/docs/essentials/web-app.html">web app&#39;s</a> console: drop the leading <code>mapshaper</code> and the input filename, since the GUI already has the layer loaded.</p>
36
+ <p>Short Mapshaper examples for common tasks. The filenames and field names are illustrative &mdash; substitute your own. Most recipes also work in the <a href="/docs/essentials/web-app.html">web app&#39;s</a> console: drop the leading <code>mapshaper</code> and the input filename; the GUI should already your data loaded.</p>
37
37
  <blockquote>
38
38
  <p>Looking for the syntax of a particular command or option? See the <a href="/docs/reference.html">command reference</a>. For the JS expression context used by <code>-each</code>, <code>-filter</code>, <code>calc=</code> and other commands, see <a href="/docs/guides/expressions.html">JavaScript expressions</a>.</p>
39
39
  </blockquote>
@@ -248,7 +248,6 @@ mapshaper
248
248
  <ul>
249
249
  <li><a href="/docs/reference.html">Command reference</a> &mdash; every command and option</li>
250
250
  <li><a href="/docs/guides/expressions.html">JavaScript expressions</a> &mdash; the syntax and context used by <code>-each</code>, <code>-filter</code>, <code>calc=</code>, etc.</li>
251
- <li><a href="/docs/guides/">Guides</a> &mdash; longer-form walk-throughs of simplification, topology cleaning, and more</li>
252
251
  </ul>
253
252
 
254
253
  <div class="edit-link">
@@ -5,7 +5,7 @@ description: A scannable collection of short Mapshaper recipes for common GIS ta
5
5
 
6
6
  # Basics
7
7
 
8
- Short Mapshaper recipes for common GIS tasks. The filenames and field names are illustrative &mdash; substitute your own. Most recipes also work in the [web app's](/docs/essentials/web-app.html.md) console: drop the leading `mapshaper` and the input filename, since the GUI already has the layer loaded.
8
+ Short Mapshaper examples for common tasks. The filenames and field names are illustrative &mdash; substitute your own. Most recipes also work in the [web app's](/docs/essentials/web-app.html.md) console: drop the leading `mapshaper` and the input filename; the GUI should already your data loaded.
9
9
 
10
10
  > Looking for the syntax of a particular command or option? See the [command reference](/docs/reference.html.md). For the JS expression context used by `-each`, `-filter`, `calc=` and other commands, see [JavaScript expressions](/docs/guides/expressions.html.md).
11
11
 
@@ -368,4 +368,3 @@ mapshaper-xl 16gb counties_5m.shp -simplify 10% -o counties_5m.topojson
368
368
 
369
369
  - [Command reference](/docs/reference.html.md) &mdash; every command and option
370
370
  - [JavaScript expressions](/docs/guides/expressions.html.md) &mdash; the syntax and context used by `-each`, `-filter`, `calc=`, etc.
371
- - [Guides](/docs/guides/index.html.md) &mdash; longer-form walk-throughs of simplification, topology cleaning, and more
Binary file
Binary file
@@ -46,7 +46,6 @@ mapshaper provinces.shp -o provinces.fgb
46
46
  <p>There are no FlatGeobuf-specific <code>-o</code> options. The format honors the general flags (<code>precision=</code>, <code>gzip</code>, <code>zip</code>, etc.) where they apply.</p>
47
47
  <h3 id="practical-notes">Practical notes</h3>
48
48
  <ul>
49
- <li>FlatGeobuf is single-layer per file. Use <a href="/docs/formats/geopackage.html">GeoPackage</a> or <a href="/docs/formats/topojson.html">TopoJSON</a> when you need to package multiple layers in one file.</li>
50
49
  <li>Mapshaper reads CRS metadata from the file header when it&#39;s encoded as an EPSG code. WKT2-only definitions can&#39;t be parsed and produce an &quot;Unable to import WKT2 CRS from FlatGeobuf&quot; warning &mdash; the layer comes in without a CRS.</li>
51
50
  <li>On output, Mapshaper embeds an EPSG code in the FlatGeobuf header whenever it can derive one from the source: a round-tripped FlatGeobuf or GeoPackage CRS, an <code>epsg:NNNN</code> string passed to <code>-proj</code>, an <code>AUTHORITY[&quot;EPSG&quot;, N]</code> clause in a Shapefile <code>.prj</code>, or any encoding of WGS-84 or Web Mercator (which covers most GeoJSON, CSV-with-lat/lon and <code>-proj wgs84</code>/<code>-proj webmercator</code> outputs).</li>
52
51
  <li>Mapshaper cannot yet convert an arbitrary proj4 definition (such as a custom Albers projection set with <code>-proj +proj=aea ...</code>) to an EPSG code. In that case the file is written with no CRS in the header and a warning is printed: <em>&quot;Wrote <code>foo.fgb</code> without a CRS in the FlatGeobuf header...&quot;</em>. Re-run the file through <code>ogr2ogr</code> if you need the CRS embedded for downstream tools.</li>
@@ -27,7 +27,6 @@ There are no FlatGeobuf-specific `-o` options. The format honors the general fla
27
27
 
28
28
  ### Practical notes
29
29
 
30
- - FlatGeobuf is single-layer per file. Use [GeoPackage](/docs/formats/geopackage.html.md) or [TopoJSON](/docs/formats/topojson.html.md) when you need to package multiple layers in one file.
31
30
  - Mapshaper reads CRS metadata from the file header when it's encoded as an EPSG code. WKT2-only definitions can't be parsed and produce an "Unable to import WKT2 CRS from FlatGeobuf" warning &mdash; the layer comes in without a CRS.
32
31
  - On output, Mapshaper embeds an EPSG code in the FlatGeobuf header whenever it can derive one from the source: a round-tripped FlatGeobuf or GeoPackage CRS, an `epsg:NNNN` string passed to `-proj`, an `AUTHORITY["EPSG", N]` clause in a Shapefile `.prj`, or any encoding of WGS-84 or Web Mercator (which covers most GeoJSON, CSV-with-lat/lon and `-proj wgs84`/`-proj webmercator` outputs).
33
32
  - Mapshaper cannot yet convert an arbitrary proj4 definition (such as a custom Albers projection set with `-proj +proj=aea ...`) to an EPSG code. In that case the file is written with no CRS in the header and a warning is printed: *"Wrote `foo.fgb` without a CRS in the FlatGeobuf header..."*. Re-run the file through `ogr2ogr` if you need the CRS embedded for downstream tools.
@@ -163,7 +163,6 @@
163
163
  <ul>
164
164
  <li><strong>Auto-detection by extension.</strong> You usually don&#39;t need to tell Mapshaper what format a file is &mdash; it picks the right reader from the file extension. Use <code>format=</code> on <code>-i</code> or <code>-o</code> to override.</li>
165
165
  <li><strong>TopoJSON is the only interchange format that preserves topology</strong> in the file itself. Topology-aware operations like <a href="/docs/reference.html#-dissolve"><code>-dissolve</code></a>, <a href="/docs/reference.html#-clean"><code>-clean</code></a> and <a href="/docs/reference.html#-simplify"><code>-simplify</code></a> work correctly regardless of the input format, but only TopoJSON keeps shared boundaries between adjacent polygons from being duplicated on disk. (Mapshaper&#39;s own <a href="/docs/formats/snapshot.html"><code>.msx</code></a> snapshots also preserve topology, but they&#39;re not readable by other tools.)</li>
166
- <li><strong>Most formats are single-layer per file.</strong> Multi-layer GeoJSON, Shapefile and FlatGeobuf datasets are conventionally split across multiple files (in the same directory on the CLI; selected together or zipped in the <a href="/docs/essentials/web-app.html">web app</a>); TopoJSON, GeoPackage and KML can hold many layers in one file.</li>
167
166
  <li><strong>Encoding.</strong> The <code>encoding=</code> option on <code>-i</code> and <code>-o</code> applies to Shapefile, DBF and CSV/TSV i/o (UTF-8 is the default) &mdash; the other formats are UTF-8-only.</li>
168
167
  </ul>
169
168
 
@@ -31,5 +31,4 @@ A few things worth knowing across all formats:
31
31
 
32
32
  - **Auto-detection by extension.** You usually don't need to tell Mapshaper what format a file is &mdash; it picks the right reader from the file extension. Use `format=` on `-i` or `-o` to override.
33
33
  - **TopoJSON is the only interchange format that preserves topology** in the file itself. Topology-aware operations like [`-dissolve`](/docs/reference.html.md#-dissolve), [`-clean`](/docs/reference.html.md#-clean) and [`-simplify`](/docs/reference.html.md#-simplify) work correctly regardless of the input format, but only TopoJSON keeps shared boundaries between adjacent polygons from being duplicated on disk. (Mapshaper's own [`.msx`](/docs/formats/snapshot.html.md) snapshots also preserve topology, but they're not readable by other tools.)
34
- - **Most formats are single-layer per file.** Multi-layer GeoJSON, Shapefile and FlatGeobuf datasets are conventionally split across multiple files (in the same directory on the CLI; selected together or zipped in the [web app](/docs/essentials/web-app.html.md)); TopoJSON, GeoPackage and KML can hold many layers in one file.
35
34
  - **Encoding.** The `encoding=` option on `-i` and `-o` applies to Shapefile, DBF and CSV/TSV i/o (UTF-8 is the default) &mdash; the other formats are UTF-8-only.
@@ -45,6 +45,10 @@
45
45
  <li>A county boundary file from the California Open Data Portal &mdash; or, in this case, a <a href="https://geodata.lib.berkeley.edu/catalog/ark28722-s7hs4j">version clipped to the shoreline</a> from UC Berkeley&#39;s Geo Data Commons. The shoreline-clipped version reads more naturally on a map than the legal-boundary version, which extends into the bay.</li>
46
46
  <li>A statewide <a href="https://geodata.lib.berkeley.edu/catalog/ark28722-s7bp4z">places boundary file</a> for city boundaries.</li>
47
47
  </ul>
48
+ <p>The legal county boundaries from the Census TIGER files extend straight across the water &mdash; San Francisco even reaches out to include the Farallon Islands:</p>
49
+ <p><img src="/docs/images/tiger-counties.png" alt="Bay Area counties from the Census TIGER file, with boundaries cutting across the bay and extending offshore"></p>
50
+ <p>The Berkeley library&#39;s version is clipped to the shoreline, so the bay shows as empty space between the counties:</p>
51
+ <p><img src="/docs/images/cal-counties.png" alt="The same Bay Area counties, with each polygon clipped to the coastline so the bay is visible between them"></p>
48
52
  <p>Download both as <code>.zip</code> files.</p>
49
53
  <h2 id="open-them-in-mapshaper">Open them in Mapshaper</h2>
50
54
  <p>Drag the two <code>.zip</code> files onto <a href="/">mapshaper.org</a>, or run the web app locally with <code>mapshaper-gui</code>. You&#39;ll end up with two layers loaded into the session.</p>
@@ -20,6 +20,14 @@ We'll use two source files:
20
20
  - A county boundary file from the California Open Data Portal &mdash; or, in this case, a [version clipped to the shoreline](https://geodata.lib.berkeley.edu/catalog/ark28722-s7hs4j) from UC Berkeley's Geo Data Commons. The shoreline-clipped version reads more naturally on a map than the legal-boundary version, which extends into the bay.
21
21
  - A statewide [places boundary file](https://geodata.lib.berkeley.edu/catalog/ark28722-s7bp4z) for city boundaries.
22
22
 
23
+ The legal county boundaries from the Census TIGER files extend straight across the water &mdash; San Francisco even reaches out to include the Farallon Islands:
24
+
25
+ ![Bay Area counties from the Census TIGER file, with boundaries cutting across the bay and extending offshore](/docs/images/tiger-counties.png)
26
+
27
+ The Berkeley library's version is clipped to the shoreline, so the bay shows as empty space between the counties:
28
+
29
+ ![The same Bay Area counties, with each polygon clipped to the coastline so the bay is visible between them](/docs/images/cal-counties.png)
30
+
23
31
  Download both as `.zip` files.
24
32
 
25
33
  ## Open them in Mapshaper
@@ -39,10 +39,10 @@
39
39
  <ul>
40
40
  <li><strong><code>dp</code></strong> &mdash; Douglas-Peucker (also known as Ramer–Douglas–Peucker). Guarantees that simplified lines stay within a fixed distance of the original. Good for stripping excess vertices to reduce file size, but tends to grow visible spikes at high simplification.</li>
41
41
  <li><strong><code>visvalingam</code></strong> &mdash; The Visvalingam algorithm. Iteratively removes the point that forms the smallest triangle with its two neighbors.</li>
42
- <li><strong><code>weighted_visvalingam</code></strong> (Mapshaper&#39;s default) &mdash; Visvalingam&#39;s effective-area algorithm with a custom weighting that underweights points at sharp angles, so they are removed earlier than in standard Visvalingam. The result is visibly smoother lines and fewer jagged spikes at high simplification. This method can be effective at generalizing very detailed source files, but be careful that it doesn&#39;t remove long, thin geographic features that you want to keep.</li>
42
+ <li><strong><code>weighted_visvalingam</code></strong> (Mapshaper&#39;s default) &mdash; Visvalingam&#39;s effective-area algorithm with a custom weighting that underweights points at sharp angles, so they are removed earlier than in standard Visvalingam. The result is visibly smoother lines and fewer jagged spikes at high simplification.</li>
43
43
  </ul>
44
- <p>Weighted Visvalingam is the default because it has proven to be versatile and effective at reducing detail in highly detailed source data. If you are only interested in minimizing file size, Douglas-Peucker is generally the better choice.</p>
45
- <p>You can control the amount of weighting used by Weighted Visvalingam with the <code>weighting=</code> option (default is 0.7).</p>
44
+ <p>Weighted Visvalingam is the default because it has proven to be versatile and effective at reducing detail in highly detailed source data. This method can be effective at generalizing very detailed source files, but be careful that it doesn&#39;t remove long, thin geographic features that you want to keep.You can control the amount of weighting used by Weighted Visvalingam with the <code>weighting=</code> option (default is 0.7).</p>
45
+ <p>If you are only interested in minimizing file size, Douglas-Peucker is generally the better choice.</p>
46
46
  <p><strong>Figures</strong></p>
47
47
  <p>Natural Earth 10m coastlines, simplified with modified Visvalingam at 5% point retention.
48
48
  <img src="/docs/images/simplification-mod2.png" alt="image"></p>
@@ -13,11 +13,13 @@ Mapshaper offers three simplification methods, selectable as flags to `-simplify
13
13
 
14
14
  - **`dp`** &mdash; Douglas-Peucker (also known as Ramer–Douglas–Peucker). Guarantees that simplified lines stay within a fixed distance of the original. Good for stripping excess vertices to reduce file size, but tends to grow visible spikes at high simplification.
15
15
  - **`visvalingam`** &mdash; The Visvalingam algorithm. Iteratively removes the point that forms the smallest triangle with its two neighbors.
16
- - **`weighted_visvalingam`** (Mapshaper's default) &mdash; Visvalingam's effective-area algorithm with a custom weighting that underweights points at sharp angles, so they are removed earlier than in standard Visvalingam. The result is visibly smoother lines and fewer jagged spikes at high simplification. This method can be effective at generalizing very detailed source files, but be careful that it doesn't remove long, thin geographic features that you want to keep.
16
+ - **`weighted_visvalingam`** (Mapshaper's default) &mdash; Visvalingam's effective-area algorithm with a custom weighting that underweights points at sharp angles, so they are removed earlier than in standard Visvalingam. The result is visibly smoother lines and fewer jagged spikes at high simplification.
17
+
18
+ Weighted Visvalingam is the default because it has proven to be versatile and effective at reducing detail in highly detailed source data. This method can be effective at generalizing very detailed source files, but be careful that it doesn't remove long, thin geographic features that you want to keep.You can control the amount of weighting used by Weighted Visvalingam with the `weighting=` option (default is 0.7).
19
+
20
+ If you are only interested in minimizing file size, Douglas-Peucker is generally the better choice.
17
21
 
18
- Weighted Visvalingam is the default because it has proven to be versatile and effective at reducing detail in highly detailed source data. If you are only interested in minimizing file size, Douglas-Peucker is generally the better choice.
19
22
 
20
- You can control the amount of weighting used by Weighted Visvalingam with the `weighting=` option (default is 0.7).
21
23
 
22
24
  **Figures**
23
25
 
Binary file
@@ -455,13 +455,13 @@ mapshaper counties.shp \
455
455
  <p><code>holes</code> Delete any holes from a polygon layer.</p>
456
456
  <p><code>target=</code> Layer(s) to target.</p>
457
457
  </section>
458
- <section class="cmd-section" data-id="-each" data-name="-each" data-options="this expression where target calc weight radius bbox"><h3 id="-each">-each</h3>
458
+ <section class="cmd-section" data-id="-each" data-name="-each" data-options="this expression where target"><h3 id="-each">-each</h3>
459
459
  <p>Apply a JavaScript expression to each feature in a layer. Data properties are available as local variables; the feature&#39;s geometry-derived properties are available on the <code>this</code> object (e.g. <code>this.area</code>, <code>this.centroidX</code>, <code>this.bbox</code>).</p>
460
460
  <p><strong>Tip:</strong> Enclose JS expressions in single quotes when using the bash shell (Mac and Linux) to avoid shell expansion of <code>!</code> and other special characters. Using the Windows command interpreter, enclose JS expressions in double quotes.</p>
461
461
  <p><code>&lt;expression&gt;</code> or <code>expression=</code> JavaScript expression to apply to each feature.</p>
462
462
  <p><code>where=</code> Secondary boolean JS expression for targetting a subset of features.</p>
463
463
  <p><code>target=</code> Layer to target.</p>
464
- <p>The same expression syntax and execution context are used by <code>-each</code>, <code>-filter</code>, <code>-sort</code>, <code>-inspect</code>, <code>-split</code>, <code>-subdivide</code>, <code>-calc</code> (and <code>calc=</code> options on <code>-dissolve</code>, <code>-join</code>, etc.), <code>-style</code>, <code>-symbols</code>, and any <code>where=</code>, <code>weight=</code>, <code>radius=</code> or <code>bbox=</code> option. See <a href="/docs/guides/expressions.html">JavaScript expressions</a> for the full reference: the available <code>this.*</code> properties for each geometry type, helper functions like <code>round()</code>, <code>sprintf()</code> and <code>format_dms()</code>, the <code>-calc</code> aggregation functions, the <code>A</code>/<code>B</code> pair context used by <code>-lines</code>, and common pitfalls. The <a href="/docs/examples/basics.html">Basics</a> page has practical recipes that put expressions to work.</p>
464
+ <p>The same expression syntax and execution context are used by all commands that support expressions. See <a href="/docs/guides/expressions.html">JavaScript expressions</a> for the full reference. The <a href="/docs/examples/basics.html">Basics</a> page has practical recipes that put expressions to work.</p>
465
465
  <p><strong>Examples</strong></p>
466
466
  <pre><code class="hljs language-bash"><span class="hljs-comment"># Create two fields</span>
467
467
  mapshaper counties.shp \
@@ -681,7 +681,7 @@ Apply a JavaScript expression to each feature in a layer. Data properties are av
681
681
 
682
682
  `target=` Layer to target.
683
683
 
684
- The same expression syntax and execution context are used by `-each`, `-filter`, `-sort`, `-inspect`, `-split`, `-subdivide`, `-calc` (and `calc=` options on `-dissolve`, `-join`, etc.), `-style`, `-symbols`, and any `where=`, `weight=`, `radius=` or `bbox=` option. See [JavaScript expressions](/docs/guides/expressions.html.md) for the full reference: the available `this.*` properties for each geometry type, helper functions like `round()`, `sprintf()` and `format_dms()`, the `-calc` aggregation functions, the `A`/`B` pair context used by `-lines`, and common pitfalls. The [Basics](/docs/examples/basics.html.md) page has practical recipes that put expressions to work.
684
+ The same expression syntax and execution context are used by all commands that support expressions. See [JavaScript expressions](/docs/guides/expressions.html.md) for the full reference. The [Basics](/docs/examples/basics.html.md) page has practical recipes that put expressions to work.
685
685
 
686
686
  **Examples**
687
687
 
@@ -35,7 +35,8 @@
35
35
  <h1 id="whats-new">What&#39;s new</h1>
36
36
  <p>This is a curated list of recently added features. For the full list of changes, including bug fixes and internal work, see the <a href="https://github.com/mbloch/mapshaper/blob/master/CHANGELOG.md">changelog</a> on GitHub.</p>
37
37
  <h2 id="april-2026">April 2026</h2>
38
- <div class="whats-new-entry"><p><strong>Command files.</strong> A sequence of Mapshaper commands can be written to a <code>.txt</code> file with <code>#</code> comments and no shell quoting, and run with <code>-run &lt;file&gt;</code> (or just <code>mapshaper commands.txt</code>). Command files can also be written in a shell-compatible way, if you want to be able to paste the commands into the terminal or add them to a shell script. In a future release, these files will also be runnable in the browser.</p>
38
+ <div class="whats-new-entry"><p><strong>Undo/redo buttons.</strong> The web UI now has an undo/redo toolbar. Undo and redo were already supported in the geometry and attribute editing modes; the toolbar makes them more discoverable for users who didn&#39;t know the keyboard shortcuts (<strong>⌘Z</strong> / <strong>⌘⇧Z</strong>) existed.</p>
39
+ </div><div class="whats-new-entry"><p><strong>Command files.</strong> A sequence of Mapshaper commands can be written to a <code>.txt</code> file with <code>#</code> comments and no shell quoting, and run with <code>-run &lt;file&gt;</code> (or just <code>mapshaper commands.txt</code>). Command files can also be written in a shell-compatible way, if you want to be able to paste the commands into the terminal or add them to a shell script. In a future release, these files will also be runnable in the browser.</p>
39
40
  <pre><code class="hljs language-bash">mapshaper build.txt
40
41
  </code></pre>
41
42
  <p>→ See <a href="/docs/reference.html#command-files">Command files</a> in the reference.</p>
@@ -11,6 +11,12 @@ This is a curated list of recently added features. For the full list of changes,
11
11
 
12
12
  <div class="whats-new-entry">
13
13
 
14
+ **Undo/redo buttons.** The web UI now has an undo/redo toolbar. Undo and redo were already supported in the geometry and attribute editing modes; the toolbar makes them more discoverable for users who didn't know the keyboard shortcuts (**⌘Z** / **⌘⇧Z**) existed.
15
+
16
+ </div>
17
+
18
+ <div class="whats-new-entry">
19
+
14
20
  **Command files.** A sequence of Mapshaper commands can be written to a `.txt` file with `#` comments and no shell quoting, and run with `-run <file>` (or just `mapshaper commands.txt`). Command files can also be written in a shell-compatible way, if you want to be able to paste the commands into the terminal or add them to a shell script. In a future release, these files will also be runnable in the browser.
15
21
 
16
22
  ```bash
package/www/index.html CHANGED
@@ -43,6 +43,14 @@
43
43
  <polygon points="8 14.5 13 19 13 4 3 4 3 19 8 14.5" fill="#30d4ef"/>
44
44
  </svg>
45
45
 
46
+ <svg id="undo-icon" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
47
+ <path d="M1.5,8 L7,4.5 L7,7 C10,7 12,9 12,11 L12,13 L10,13 L10,11 C10,10 9,9 7,9 L7,11.5 Z"/>
48
+ </svg>
49
+
50
+ <svg id="redo-icon" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
51
+ <path d="M14.5,8 L9,4.5 L9,7 C6,7 4,9 4,11 L4,13 L6,13 L6,11 C6,10 7,9 9,9 L9,11.5 Z"/>
52
+ </svg>
53
+
46
54
  </div>
47
55
 
48
56
  <div class="page-header">
@@ -60,10 +68,17 @@
60
68
  </div></div>
61
69
  <div id="mode-buttons" class="page-header-buttons">
62
70
  <!-- <span class="project-btn header-btn btn">Project</span><span class="separator"> -->
63
- <span class="console-btn header-btn btn">Console</span><span class="display-btn header-btn btn">Display</span><span class="separator"></span><span class="separator"></span></span><span class="simplify-btn header-btn btn">Simplify</span><span class="separator"></span><span class="export-btn header-btn btn">Export</span>
71
+ <span class="console-btn header-btn btn">Console</span><span class="display-btn header-btn btn">Display</span><span class="separator"></span><span class="separator"></span></span><span class="simplify-btn header-btn btn">Simplify</span><span class="separator"></span><span class="export-btn header-btn btn">Export</span><span class="separator"></span><span id="header-menu-btn" class="header-btn btn" role="button" aria-label="More links" aria-haspopup="true" aria-expanded="false" tabindex="0"><svg viewBox="0 0 16 16" aria-hidden="true" width="16" height="14"><rect x="1" y="3" width="14" height="2"/><rect x="1" y="7" width="14" height="2"/><rect x="1" y="11" width="14" height="2"/></svg></span>
72
+ <div id="header-menu-dropdown" class="hidden" role="menu" aria-labelledby="header-menu-btn">
73
+ <a class="header-menu-item" href="/docs/" target="_blank" rel="noopener" role="menuitem">Docs</a>
74
+ <a class="header-menu-item" href="https://github.com/mbloch/mapshaper" target="_blank" rel="noopener" role="menuitem">GitHub</a>
75
+ <a class="header-menu-item survey-en-link" href="https://tally.so/r/44Njok" target="_blank" rel="noopener" role="menuitem">1-minute survey</a>
76
+ <a class="header-menu-item survey-zh-link" href="https://tally.so/r/vGx7KQ" target="_blank" rel="noopener" lang="zh-CN" style="display:none" role="menuitem">意见反馈</a>
77
+ <a class="header-menu-item header-menu-sponsor" href="sponsor.html" target="_blank" rel="noopener" role="menuitem"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>Become a sponsor</a>
78
+ </div>
64
79
  </div>
65
80
  <div id="splash-buttons" class="page-header-buttons">
66
- <a href="/docs/"><span id="wiki-btn" class="header-btn btn">Docs</span></a><span class="separator"></span><a href="https://github.com/mbloch/mapshaper"><span id="github-btn" class="header-btn btn">GitHub</span></a><span class="separator"></span><a id="survey-en-link" href="https://tally.so/r/44Njok" target="_blank" rel="noopener"><span id="survey-en-btn" class="header-btn btn">1-minute survey</span></a><a id="survey-zh-link" href="https://tally.so/r/vGx7KQ" target="_blank" rel="noopener" style="display:none"><span id="survey-zh-btn" class="header-btn btn" lang="zh-CN">意见反馈</span></a><span class="separator"></span><a href="sponsor.html"><span id="sponsor-btn" class="header-btn btn"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg> Become a sponsor</span></a>
81
+ <a href="/docs/"><span id="wiki-btn" class="header-btn btn">Docs</span></a><span class="separator"></span><a href="https://github.com/mbloch/mapshaper"><span id="github-btn" class="header-btn btn">GitHub</span></a><span class="separator"></span><a class="survey-en-link" href="https://tally.so/r/44Njok" target="_blank" rel="noopener"><span id="survey-en-btn" class="header-btn btn">1-minute survey</span></a><a class="survey-zh-link" href="https://tally.so/r/vGx7KQ" target="_blank" rel="noopener" style="display:none"><span id="survey-zh-btn" class="header-btn btn" lang="zh-CN">意见反馈</span></a><span class="separator"></span><a href="sponsor.html"><span id="sponsor-btn" class="header-btn btn"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg> Become a sponsor</span></a>
67
82
  </div>
68
83
  </div>
69
84
 
@@ -351,10 +366,11 @@ encoding=big5</span>. Click to see all options.</div></div></div>
351
366
  if (/^zh(-Hans)?-CN\b/i.test(langs[i])) isChina = true;
352
367
  }
353
368
  if (isChina) {
354
- var zh = document.getElementById('survey-zh-link');
355
- var en = document.getElementById('survey-en-link');
356
- if (zh) zh.style.display = '';
357
- if (en) en.style.display = 'none';
369
+ // Swap every copy of the survey link (splash bar + hamburger menu)
370
+ var zhLinks = document.querySelectorAll('.survey-zh-link');
371
+ var enLinks = document.querySelectorAll('.survey-en-link');
372
+ for (var j = 0; j < zhLinks.length; j++) zhLinks[j].style.display = '';
373
+ for (var k = 0; k < enLinks.length; k++) enLinks[k].style.display = 'none';
358
374
  }
359
375
  })();
360
376
  </script>
package/www/llms-full.txt CHANGED
@@ -63,6 +63,12 @@ This is a curated list of recently added features. For the full list of changes,
63
63
 
64
64
  <div class="whats-new-entry">
65
65
 
66
+ **Undo/redo buttons.** The web UI now has an undo/redo toolbar. Undo and redo were already supported in the geometry and attribute editing modes; the toolbar makes them more discoverable for users who didn't know the keyboard shortcuts (**⌘Z** / **⌘⇧Z**) existed.
67
+
68
+ </div>
69
+
70
+ <div class="whats-new-entry">
71
+
66
72
  **Command files.** A sequence of Mapshaper commands can be written to a `.txt` file with `#` comments and no shell quoting, and run with `-run <file>` (or just `mapshaper commands.txt`). Command files can also be written in a shell-compatible way, if you want to be able to paste the commands into the terminal or add them to a shell script. In a future release, these files will also be runnable in the browser.
67
73
 
68
74
  ```bash
@@ -791,7 +797,7 @@ Apply a JavaScript expression to each feature in a layer. Data properties are av
791
797
 
792
798
  `target=` Layer to target.
793
799
 
794
- The same expression syntax and execution context are used by `-each`, `-filter`, `-sort`, `-inspect`, `-split`, `-subdivide`, `-calc` (and `calc=` options on `-dissolve`, `-join`, etc.), `-style`, `-symbols`, and any `where=`, `weight=`, `radius=` or `bbox=` option. See [JavaScript expressions](/docs/guides/expressions.html.md) for the full reference: the available `this.*` properties for each geometry type, helper functions like `round()`, `sprintf()` and `format_dms()`, the `-calc` aggregation functions, the `A`/`B` pair context used by `-lines`, and common pitfalls. The [Basics](/docs/examples/basics.html.md) page has practical recipes that put expressions to work.
800
+ The same expression syntax and execution context are used by all commands that support expressions. See [JavaScript expressions](/docs/guides/expressions.html.md) for the full reference. The [Basics](/docs/examples/basics.html.md) page has practical recipes that put expressions to work.
795
801
 
796
802
  **Examples**
797
803
 
@@ -2168,11 +2174,13 @@ Mapshaper offers three simplification methods, selectable as flags to `-simplify
2168
2174
 
2169
2175
  - **`dp`** &mdash; Douglas-Peucker (also known as Ramer–Douglas–Peucker). Guarantees that simplified lines stay within a fixed distance of the original. Good for stripping excess vertices to reduce file size, but tends to grow visible spikes at high simplification.
2170
2176
  - **`visvalingam`** &mdash; The Visvalingam algorithm. Iteratively removes the point that forms the smallest triangle with its two neighbors.
2171
- - **`weighted_visvalingam`** (Mapshaper's default) &mdash; Visvalingam's effective-area algorithm with a custom weighting that underweights points at sharp angles, so they are removed earlier than in standard Visvalingam. The result is visibly smoother lines and fewer jagged spikes at high simplification. This method can be effective at generalizing very detailed source files, but be careful that it doesn't remove long, thin geographic features that you want to keep.
2177
+ - **`weighted_visvalingam`** (Mapshaper's default) &mdash; Visvalingam's effective-area algorithm with a custom weighting that underweights points at sharp angles, so they are removed earlier than in standard Visvalingam. The result is visibly smoother lines and fewer jagged spikes at high simplification.
2178
+
2179
+ Weighted Visvalingam is the default because it has proven to be versatile and effective at reducing detail in highly detailed source data. This method can be effective at generalizing very detailed source files, but be careful that it doesn't remove long, thin geographic features that you want to keep.You can control the amount of weighting used by Weighted Visvalingam with the `weighting=` option (default is 0.7).
2180
+
2181
+ If you are only interested in minimizing file size, Douglas-Peucker is generally the better choice.
2172
2182
 
2173
- Weighted Visvalingam is the default because it has proven to be versatile and effective at reducing detail in highly detailed source data. If you are only interested in minimizing file size, Douglas-Peucker is generally the better choice.
2174
2183
 
2175
- You can control the amount of weighting used by Weighted Visvalingam with the `weighting=` option (default is 0.7).
2176
2184
 
2177
2185
  **Figures**
2178
2186
 
@@ -2826,6 +2834,14 @@ We'll use two source files:
2826
2834
  - A county boundary file from the California Open Data Portal &mdash; or, in this case, a [version clipped to the shoreline](https://geodata.lib.berkeley.edu/catalog/ark28722-s7hs4j) from UC Berkeley's Geo Data Commons. The shoreline-clipped version reads more naturally on a map than the legal-boundary version, which extends into the bay.
2827
2835
  - A statewide [places boundary file](https://geodata.lib.berkeley.edu/catalog/ark28722-s7bp4z) for city boundaries.
2828
2836
 
2837
+ The legal county boundaries from the Census TIGER files extend straight across the water &mdash; San Francisco even reaches out to include the Farallon Islands:
2838
+
2839
+ ![Bay Area counties from the Census TIGER file, with boundaries cutting across the bay and extending offshore](/docs/images/tiger-counties.png)
2840
+
2841
+ The Berkeley library's version is clipped to the shoreline, so the bay shows as empty space between the counties:
2842
+
2843
+ ![The same Bay Area counties, with each polygon clipped to the coastline so the bay is visible between them](/docs/images/cal-counties.png)
2844
+
2829
2845
  Download both as `.zip` files.
2830
2846
 
2831
2847
  ## Open them in Mapshaper
@@ -3009,7 +3025,6 @@ A few things worth knowing across all formats:
3009
3025
 
3010
3026
  - **Auto-detection by extension.** You usually don't need to tell Mapshaper what format a file is &mdash; it picks the right reader from the file extension. Use `format=` on `-i` or `-o` to override.
3011
3027
  - **TopoJSON is the only interchange format that preserves topology** in the file itself. Topology-aware operations like [`-dissolve`](/docs/reference.html.md#-dissolve), [`-clean`](/docs/reference.html.md#-clean) and [`-simplify`](/docs/reference.html.md#-simplify) work correctly regardless of the input format, but only TopoJSON keeps shared boundaries between adjacent polygons from being duplicated on disk. (Mapshaper's own [`.msx`](/docs/formats/snapshot.html.md) snapshots also preserve topology, but they're not readable by other tools.)
3012
- - **Most formats are single-layer per file.** Multi-layer GeoJSON, Shapefile and FlatGeobuf datasets are conventionally split across multiple files (in the same directory on the CLI; selected together or zipped in the [web app](/docs/essentials/web-app.html.md)); TopoJSON, GeoPackage and KML can hold many layers in one file.
3013
3028
  - **Encoding.** The `encoding=` option on `-i` and `-o` applies to Shapefile, DBF and CSV/TSV i/o (UTF-8 is the default) &mdash; the other formats are UTF-8-only.
3014
3029
 
3015
3030
  ---
@@ -3285,7 +3300,6 @@ There are no FlatGeobuf-specific `-o` options. The format honors the general fla
3285
3300
 
3286
3301
  ### Practical notes
3287
3302
 
3288
- - FlatGeobuf is single-layer per file. Use [GeoPackage](/docs/formats/geopackage.html.md) or [TopoJSON](/docs/formats/topojson.html.md) when you need to package multiple layers in one file.
3289
3303
  - Mapshaper reads CRS metadata from the file header when it's encoded as an EPSG code. WKT2-only definitions can't be parsed and produce an "Unable to import WKT2 CRS from FlatGeobuf" warning &mdash; the layer comes in without a CRS.
3290
3304
  - On output, Mapshaper embeds an EPSG code in the FlatGeobuf header whenever it can derive one from the source: a round-tripped FlatGeobuf or GeoPackage CRS, an `epsg:NNNN` string passed to `-proj`, an `AUTHORITY["EPSG", N]` clause in a Shapefile `.prj`, or any encoding of WGS-84 or Web Mercator (which covers most GeoJSON, CSV-with-lat/lon and `-proj wgs84`/`-proj webmercator` outputs).
3291
3305
  - Mapshaper cannot yet convert an arbitrary proj4 definition (such as a custom Albers projection set with `-proj +proj=aea ...`) to an EPSG code. In that case the file is written with no CRS in the header and a warning is printed: *"Wrote `foo.fgb` without a CRS in the FlatGeobuf header..."*. Re-run the file through `ogr2ogr` if you need the CRS embedded for downstream tools.
@@ -3605,7 +3619,7 @@ The ribbon icon in the layer panel opens the **snapshot menu**. From there you c
3605
3619
 
3606
3620
  # Basics
3607
3621
 
3608
- Short Mapshaper recipes for common GIS tasks. The filenames and field names are illustrative &mdash; substitute your own. Most recipes also work in the [web app's](/docs/essentials/web-app.html.md) console: drop the leading `mapshaper` and the input filename, since the GUI already has the layer loaded.
3622
+ Short Mapshaper examples for common tasks. The filenames and field names are illustrative &mdash; substitute your own. Most recipes also work in the [web app's](/docs/essentials/web-app.html.md) console: drop the leading `mapshaper` and the input filename; the GUI should already your data loaded.
3609
3623
 
3610
3624
  > Looking for the syntax of a particular command or option? See the [command reference](/docs/reference.html.md). For the JS expression context used by `-each`, `-filter`, `calc=` and other commands, see [JavaScript expressions](/docs/guides/expressions.html.md).
3611
3625
 
@@ -3968,7 +3982,6 @@ mapshaper-xl 16gb counties_5m.shp -simplify 10% -o counties_5m.topojson
3968
3982
 
3969
3983
  - [Command reference](/docs/reference.html.md) &mdash; every command and option
3970
3984
  - [JavaScript expressions](/docs/guides/expressions.html.md) &mdash; the syntax and context used by `-each`, `-filter`, `calc=`, etc.
3971
- - [Guides](/docs/guides/index.html.md) &mdash; longer-form walk-throughs of simplification, topology cleaning, and more
3972
3985
 
3973
3986
  ---
3974
3987
 
@@ -5655,6 +5655,56 @@
5655
5655
  }
5656
5656
  }
5657
5657
 
5658
+ // Hamburger menu in the top-right of the header. After the user starts
5659
+ // editing, the splash bar (Docs / GitHub / Survey / Sponsor) is hidden by
5660
+ // gui.mjs; this menu is the post-edit replacement, exposing the same links
5661
+ // from a single button so the editing controls don't have to share the row.
5662
+
5663
+
5664
+ function HeaderMenu() {
5665
+ var btn = El('#header-menu-btn');
5666
+ var dropdown = El('#header-menu-dropdown');
5667
+ if (!btn.node() || !dropdown.node()) return; // markup missing; nothing to do
5668
+
5669
+ var open = false;
5670
+
5671
+ function setOpen(state) {
5672
+ if (state === open) return;
5673
+ open = state;
5674
+ dropdown.classed('hidden', !open);
5675
+ btn.attr('aria-expanded', open ? 'true' : 'false');
5676
+ }
5677
+
5678
+ btn.on('click', function(e) {
5679
+ e.stopPropagation();
5680
+ setOpen(!open);
5681
+ });
5682
+
5683
+ // Keyboard activation matches button semantics for the role="button" span.
5684
+ btn.on('keydown', function(e) {
5685
+ if (e.key === 'Enter' || e.key === ' ') {
5686
+ e.preventDefault();
5687
+ setOpen(!open);
5688
+ }
5689
+ });
5690
+
5691
+ // Outside click closes the menu. Clicks on menu items also close it (they
5692
+ // navigate or open in a new tab; either way the menu shouldn't linger).
5693
+ document.addEventListener('click', function(e) {
5694
+ if (!open) return;
5695
+ var target = e.target;
5696
+ if (btn.node().contains(target)) return;
5697
+ setOpen(false);
5698
+ });
5699
+
5700
+ document.addEventListener('keydown', function(e) {
5701
+ if (open && e.key === 'Escape') {
5702
+ setOpen(false);
5703
+ btn.node().focus();
5704
+ }
5705
+ });
5706
+ }
5707
+
5658
5708
  function SessionHistory(gui) {
5659
5709
  var commands = [];
5660
5710
  // index of first command after the last "save" boundary; commands at indices
@@ -5993,6 +6043,15 @@
5993
6043
 
5994
6044
  this.clear = function() {
5995
6045
  reset();
6046
+ fireHistoryChange();
6047
+ };
6048
+
6049
+ this.canUndo = function() {
6050
+ return history.length - offset > 0;
6051
+ };
6052
+
6053
+ this.canRedo = function() {
6054
+ return offset > 0;
5996
6055
  };
5997
6056
 
5998
6057
  function addHistoryState(undo, redo) {
@@ -6001,6 +6060,14 @@
6001
6060
  offset = 0;
6002
6061
  }
6003
6062
  history.push({undo, redo});
6063
+ fireHistoryChange();
6064
+ }
6065
+
6066
+ function fireHistoryChange() {
6067
+ gui.dispatchEvent('history_change', {
6068
+ canUndo: history.length - offset > 0,
6069
+ canRedo: offset > 0
6070
+ });
6004
6071
  }
6005
6072
 
6006
6073
  this.undo = function() {
@@ -6013,6 +6080,7 @@
6013
6080
  item.undo();
6014
6081
  gui.dispatchEvent('undo_redo_post', {type: 'undo'});
6015
6082
  gui.dispatchEvent('map-needs-refresh');
6083
+ fireHistoryChange();
6016
6084
  }
6017
6085
  };
6018
6086
 
@@ -6024,6 +6092,7 @@
6024
6092
  item.redo();
6025
6093
  gui.dispatchEvent('undo_redo_post', {type: 'redo'});
6026
6094
  gui.dispatchEvent('map-needs-refresh');
6095
+ fireHistoryChange();
6027
6096
  };
6028
6097
 
6029
6098
  function getHistoryItem() {
@@ -6337,6 +6406,10 @@
6337
6406
  return ['info', 'selection', 'data', 'box', 'labels', 'edit_points', 'rectangles'].includes(mode);
6338
6407
  };
6339
6408
 
6409
+ this.modeSupportsUndo = function(mode) {
6410
+ return ['data', 'labels', 'edit_points', 'edit_lines', 'edit_polygons', 'vertices', 'rectangles'].includes(mode);
6411
+ };
6412
+
6340
6413
  this.getMode = getInteractionMode;
6341
6414
 
6342
6415
  this.setMode = function(mode) {
@@ -6479,6 +6552,214 @@
6479
6552
  }
6480
6553
  }
6481
6554
 
6555
+ // A reusable floating toolbar anchored at the bottom-center of the map area.
6556
+ //
6557
+ // Multiple toolbars stack vertically inside a shared container, so additional
6558
+ // per-mode toolbars (e.g. feature styling) can coexist with the edit toolbar.
6559
+ //
6560
+ // The DOM is structured to leave room for a future drag handle without
6561
+ // requiring rework: the toolbar element is a flex row with a content slot,
6562
+ // and a sibling drag-handle slot can be added later.
6563
+ //
6564
+ // Constructor options:
6565
+ // name: optional CSS class added to the toolbar element
6566
+ // transition: ms for the show/hide transition (default 150)
6567
+ //
6568
+ // API:
6569
+ // toolbar.addButton(iconRef, opts) -> ToolbarButton
6570
+ // toolbar.addSeparator()
6571
+ // toolbar.show()
6572
+ // toolbar.hide()
6573
+ // toolbar.visible()
6574
+ // toolbar.node()
6575
+
6576
+ function FloatingToolbar(gui, opts) {
6577
+ opts = opts || {};
6578
+ var transitionMs = opts.transition || 150;
6579
+ var root = gui.container.findChild('.mshp-main-map');
6580
+ var stack = root.findChild('.floating-toolbar-stack');
6581
+ if (!stack) {
6582
+ stack = El('div').addClass('floating-toolbar-stack').appendTo(root);
6583
+ }
6584
+ var el = El('div').addClass('floating-toolbar');
6585
+ if (opts.name) el.addClass(opts.name);
6586
+ var content = El('div').addClass('floating-toolbar-content').appendTo(el);
6587
+ var visible = false;
6588
+ var hideTimer = null;
6589
+
6590
+ el.appendTo(stack);
6591
+ el.css('display', 'none');
6592
+
6593
+ // Hide when this gui instance becomes inactive (e.g. multi-instance mode)
6594
+ gui.on('active', updateVisibility);
6595
+ gui.on('inactive', updateVisibility);
6596
+
6597
+ this.addButton = function(iconRef, btnOpts) {
6598
+ return new ToolbarButton(content, iconRef, btnOpts || {});
6599
+ };
6600
+
6601
+ this.addSeparator = function() {
6602
+ return El('div').addClass('floating-toolbar-separator').appendTo(content);
6603
+ };
6604
+
6605
+ this.show = function() {
6606
+ if (visible) return;
6607
+ visible = true;
6608
+ updateVisibility();
6609
+ };
6610
+
6611
+ this.hide = function() {
6612
+ if (!visible) return;
6613
+ visible = false;
6614
+ updateVisibility();
6615
+ };
6616
+
6617
+ this.visible = function() {
6618
+ return visible;
6619
+ };
6620
+
6621
+ this.node = function() {
6622
+ return el.node();
6623
+ };
6624
+
6625
+ function updateVisibility() {
6626
+ var shouldShow = visible && GUI.isActiveInstance(gui);
6627
+ if (shouldShow) {
6628
+ clearTimeout(hideTimer);
6629
+ hideTimer = null;
6630
+ el.css('display', 'flex');
6631
+ // wait one frame so the browser registers the initial state before
6632
+ // the transition kicks in
6633
+ requestAnimationFrame(function() {
6634
+ el.addClass('visible');
6635
+ });
6636
+ } else {
6637
+ el.removeClass('visible');
6638
+ // wait for the transition to finish before hiding completely
6639
+ clearTimeout(hideTimer);
6640
+ hideTimer = setTimeout(function() {
6641
+ if (!(visible && GUI.isActiveInstance(gui))) {
6642
+ el.css('display', 'none');
6643
+ }
6644
+ }, transitionMs);
6645
+ }
6646
+ }
6647
+ }
6648
+
6649
+ function ToolbarButton(parent, iconRef, opts) {
6650
+ var btn = El('div').addClass('floating-toolbar-btn').appendTo(parent);
6651
+ if (iconRef) {
6652
+ var iconNode = El('body').findChild(iconRef);
6653
+ if (iconNode) {
6654
+ var icon = iconNode.node().cloneNode(true);
6655
+ if (icon.hasAttribute('id')) icon.removeAttribute('id');
6656
+ btn.node().appendChild(icon);
6657
+ }
6658
+ }
6659
+ var enabled = true;
6660
+ var clickHandlers = [];
6661
+
6662
+ if (opts.tooltip) setTooltip(opts.tooltip);
6663
+
6664
+ // Block native dblclick to avoid the map's double-click zoom
6665
+ btn.on('dblclick', function(e) { e.stopPropagation(); });
6666
+
6667
+ btn.on('click', function(e) {
6668
+ if (!enabled) return;
6669
+ for (var i = 0; i < clickHandlers.length; i++) {
6670
+ clickHandlers[i](e);
6671
+ }
6672
+ });
6673
+
6674
+ this.on = function(event, fn) {
6675
+ if (event == 'click') {
6676
+ clickHandlers.push(fn);
6677
+ } else {
6678
+ btn.on(event, fn);
6679
+ }
6680
+ return this;
6681
+ };
6682
+
6683
+ this.enable = function() { return this.setEnabled(true); };
6684
+ this.disable = function() { return this.setEnabled(false); };
6685
+
6686
+ this.setEnabled = function(b) {
6687
+ enabled = !!b;
6688
+ btn.classed('disabled', !enabled);
6689
+ return this;
6690
+ };
6691
+
6692
+ this.setTooltip = setTooltip;
6693
+
6694
+ this.node = function() {
6695
+ return btn.node();
6696
+ };
6697
+
6698
+ function setTooltip(text) {
6699
+ btn.attr('data-tooltip', text);
6700
+ }
6701
+ }
6702
+
6703
+ // Floating toolbar that exposes undo/redo while the user is in an editing
6704
+ // interaction mode. The toolbar is the first consumer of FloatingToolbar;
6705
+ // future per-mode toolbars (e.g. feature styling) can follow the same pattern.
6706
+
6707
+ function EditToolbar(gui) {
6708
+ var toolbar = new FloatingToolbar(gui, { name: 'edit-toolbar' });
6709
+ var isMac = navigator.userAgent.includes('Mac');
6710
+ var modKey = isMac ? '\u2318' : 'Ctrl+';
6711
+ var shiftKey = isMac ? '\u21e7' : 'Shift+';
6712
+
6713
+ var undoBtn = toolbar.addButton('#undo-icon', {
6714
+ tooltip: 'Undo (' + modKey + 'Z)'
6715
+ }).on('click', function() {
6716
+ gui.undo.undo();
6717
+ });
6718
+
6719
+ var redoBtn = toolbar.addButton('#redo-icon', {
6720
+ tooltip: 'Redo (' + shiftKey + modKey + 'Z)'
6721
+ }).on('click', function() {
6722
+ gui.undo.redo();
6723
+ });
6724
+
6725
+ updateButtons();
6726
+
6727
+ gui.on('interaction_mode_change', function() {
6728
+ updateVisibility();
6729
+ // history is cleared on mode change; refresh button states next tick
6730
+ updateButtons();
6731
+ });
6732
+
6733
+ gui.on('history_change', function(e) {
6734
+ undoBtn.setEnabled(!!e.canUndo);
6735
+ redoBtn.setEnabled(!!e.canRedo);
6736
+ // Visibility may also depend on history (e.g. attribute edits via popup
6737
+ // happen in modes that don't otherwise support undo).
6738
+ updateVisibility();
6739
+ });
6740
+
6741
+ updateVisibility();
6742
+
6743
+ function updateVisibility() {
6744
+ if (!gui.interaction) {
6745
+ toolbar.hide();
6746
+ return;
6747
+ }
6748
+ var mode = gui.interaction.getMode();
6749
+ var hasHistory = gui.undo.canUndo() || gui.undo.canRedo();
6750
+ if (gui.interaction.modeSupportsUndo(mode) || hasHistory) {
6751
+ toolbar.show();
6752
+ } else {
6753
+ toolbar.hide();
6754
+ }
6755
+ }
6756
+
6757
+ function updateButtons() {
6758
+ undoBtn.setEnabled(gui.undo.canUndo());
6759
+ redoBtn.setEnabled(gui.undo.canRedo());
6760
+ }
6761
+ }
6762
+
6482
6763
  function Model(gui) {
6483
6764
  var self = new internal.Catalog();
6484
6765
  var deleteLayer = self.deleteLayer;
@@ -9576,6 +9857,10 @@
9576
9857
 
9577
9858
  box.setDataCoords = function(bbox) {
9578
9859
  boxCoords = bbox;
9860
+ // Box is being placed programmatically (e.g. around a pinned rectangle);
9861
+ // enable pointer events on the handles so they can be dragged. Without
9862
+ // this, mousedowns fall through to the map and trigger pan.
9863
+ el.classed('hittable', true);
9579
9864
  redraw();
9580
9865
  };
9581
9866
 
@@ -9599,6 +9884,7 @@
9599
9884
  // remove the current box (if any)
9600
9885
  box.hide = function() {
9601
9886
  el.hide();
9887
+ el.classed('hittable', false);
9602
9888
  boxCoords = null;
9603
9889
  _visible = false;
9604
9890
  clickStartPoint = null;
@@ -10949,8 +11235,8 @@
10949
11235
  function showInstructions() {
10950
11236
  var isMac = navigator.userAgent.includes('Mac');
10951
11237
  var symbol = isMac ? '⌘' : '^';
10952
- var msg = `Instructions: Click on the map to add points. Move points by dragging. Type ${symbol}Z/${symbol}Y to undo/redo.`;
10953
- alert = showPopupAlert(msg, null, { non_blocking: true, max_width: '360px'});
11238
+ var msg = `Instructions: click on the map to add points. Move points by dragging.`;
11239
+ alert = showPopupAlert(msg, null, { non_blocking: true, max_width: '290px'});
10954
11240
  }
10955
11241
 
10956
11242
  gui.on('interaction_mode_change', function(e) {
@@ -11167,9 +11453,9 @@
11167
11453
  function showInstructions() {
11168
11454
  var isMac = navigator.userAgent.includes('Mac');
11169
11455
  var undoKey = isMac ? '⌘' : '^';
11170
- var msg = `Instructions: click to start a path, click or drag to keep drawing. Drag vertices to reshape a path. Type ${undoKey}Z/${undoKey}Y to undo/redo.`;
11456
+ var msg = `Instructions: click to start a path, click or drag to keep drawing. Drag vertices to reshape a path.`;
11171
11457
  alert = showPopupAlert(msg, null, {
11172
- non_blocking: true, max_width: '388px'});
11458
+ non_blocking: true, max_width: '350px'});
11173
11459
  }
11174
11460
 
11175
11461
  function hideInstructions() {
@@ -14631,6 +14917,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
14631
14917
  new SessionSnapshots(gui);
14632
14918
  }
14633
14919
  gui.interaction = new InteractionMode(gui);
14920
+ gui.editToolbar = new EditToolbar(gui);
14634
14921
 
14635
14922
  gui.showProgressMessage = function(msg) {
14636
14923
  if (!gui.progressMessage) {
@@ -14755,6 +15042,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
14755
15042
  new ImportControl(gui, importOpts);
14756
15043
  new ExportControl(gui);
14757
15044
  new LayerControl(gui);
15045
+ HeaderMenu();
14758
15046
  gui.console = new Console(gui);
14759
15047
 
14760
15048
  startEditing = function() {};
package/www/mapshaper.js CHANGED
@@ -52127,7 +52127,7 @@ ${svg}
52127
52127
  });
52128
52128
  }
52129
52129
 
52130
- var version = "0.7.1";
52130
+ var version = "0.7.2";
52131
52131
 
52132
52132
  // Parse command line args into commands and run them
52133
52133
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
package/www/page.css CHANGED
@@ -170,7 +170,7 @@ body.map-view {
170
170
  position: absolute;
171
171
  top: 0px;
172
172
  right: 0px;
173
- margin: 0 6px 3px 0;
173
+ margin: 0 3px 3px 0;
174
174
  }
175
175
 
176
176
  .btn.header-btn {
@@ -207,6 +207,63 @@ body.map-view {
207
207
  vertical-align: -1px;
208
208
  }
209
209
 
210
+ /* --- Hamburger menu (post-edit overflow links) --- */
211
+
212
+ #header-menu-btn {
213
+ cursor: pointer;
214
+ padding: 8px 7px 4px 7px;
215
+ user-select: none;
216
+ /* All header buttons are 29px tall, so top-align the hamburger to keep its
217
+ row baseline-neutral. Without this, the SVG-only button (no text) makes
218
+ its inline-block baseline = bottom edge, which drags the surrounding
219
+ text-containing buttons down to match. */
220
+ vertical-align: top;
221
+ }
222
+
223
+ #header-menu-btn svg {
224
+ display: block;
225
+ fill: currentColor;
226
+ }
227
+
228
+ #header-menu-dropdown {
229
+ position: absolute;
230
+ top: 30px;
231
+ right: 0;
232
+ min-width: 180px;
233
+ background-color: #fff;
234
+ color: #333;
235
+ border: 1px solid #ccc;
236
+ border-radius: 3px;
237
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
238
+ padding: 4px 0;
239
+ z-index: 50;
240
+ }
241
+
242
+ .header-menu-item {
243
+ display: block;
244
+ padding: 4px 14px;
245
+ color: #333;
246
+ font-size: 14px;
247
+ line-height: 1.3;
248
+ text-decoration: none;
249
+ white-space: nowrap;
250
+ }
251
+
252
+ .header-menu-item:hover,
253
+ .header-menu-item:focus {
254
+ background-color: #e6f7ff;
255
+ color: #1A6A96;
256
+ outline: none;
257
+ }
258
+
259
+ .header-menu-item.header-menu-sponsor svg {
260
+ width: 12px;
261
+ height: 12px;
262
+ fill: #d63384;
263
+ vertical-align: -1px;
264
+ margin-right: 6px;
265
+ }
266
+
210
267
  .separator {
211
268
  border-left: 1px solid white;
212
269
  height: 10px;
@@ -427,6 +484,14 @@ div.alert-box {
427
484
  margin: 0 0 5px 0;
428
485
  }
429
486
 
487
+ input[type=text]:focus,
488
+ input[type=number]:focus,
489
+ textarea:focus {
490
+ outline: none;
491
+ border-color: #d3aa00;
492
+ box-shadow: 0 0 0 2px rgba(212, 168, 0, 0.4);
493
+ }
494
+
430
495
  ::placeholder {
431
496
  color: #aaa;
432
497
  opacity: 1;
@@ -495,13 +560,6 @@ div.alert-box {
495
560
  padding-left: 12px;
496
561
  }
497
562
 
498
- .info-box,.popup {
499
- border-radius: 9px;
500
- border: 1px solid #d5d5d5;
501
- box-shadow: 0 1px 7px rgba(0,0,0,0.18);
502
- background-color: #fff;
503
- }
504
-
505
563
  .contextmenu {
506
564
  position: absolute;
507
565
  border-radius: 6px;
@@ -518,6 +576,12 @@ div.alert-box {
518
576
  padding: 6px;
519
577
  }
520
578
 
579
+ .info-box,.popup {
580
+ border-radius: 11px;
581
+ border: 1px solid #d5d5d5;
582
+ box-shadow: 0 1px 7px rgba(0,0,0,0.18);
583
+ background-color: #fff;
584
+ }
521
585
 
522
586
  .info-box {
523
587
  min-width: 230px;
@@ -525,7 +589,8 @@ div.alert-box {
525
589
  text-align: left;
526
590
  margin-top: 12px;
527
591
  margin-right: 20px;
528
- padding: 12px 16px 12px 18px;
592
+ /* padding: 12px 16px 12px 18px; */
593
+ padding: 15px 17px 13px 19px;
529
594
  vertical-align: top;
530
595
  display: inline-block;
531
596
  /* border: 1px solid #aaa; */
@@ -575,7 +640,7 @@ div.alert-box {
575
640
  .info-box p {
576
641
  white-space: pre-line;
577
642
  margin: 0 0 6px 0;
578
- line-height: 1.2;
643
+ line-height: 1.25;
579
644
  /* font-size: 90%; */
580
645
  }
581
646
 
@@ -1552,6 +1617,119 @@ body.pan.panning .map-layers:not(.drawing) {
1552
1617
  cursor: default;
1553
1618
  }
1554
1619
 
1620
+ /* --- FLOATING TOOLBAR --- */
1621
+
1622
+ /*
1623
+ * Anchored at bottom-center of the map area. Multiple toolbars stack
1624
+ * vertically (newest on top). Designed to host buttons and other small
1625
+ * controls; the inner content slot leaves room for a future drag handle.
1626
+ */
1627
+
1628
+ .floating-toolbar-stack {
1629
+ position: absolute;
1630
+ z-index: 25;
1631
+ bottom: 24px;
1632
+ left: 50%;
1633
+ transform: translateX(-50%);
1634
+ display: flex;
1635
+ flex-direction: column-reverse;
1636
+ align-items: center;
1637
+ gap: 8px;
1638
+ pointer-events: none;
1639
+ }
1640
+
1641
+ .floating-toolbar {
1642
+ pointer-events: auto;
1643
+ display: flex;
1644
+ align-items: center;
1645
+ background-color: rgba(255, 255, 255, 0.96);
1646
+ border: 1px solid #ddd;
1647
+ border-radius: 8px;
1648
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
1649
+ padding: 4px;
1650
+ opacity: 0;
1651
+ transform: translateY(6px);
1652
+ transition: opacity 150ms ease, transform 150ms ease;
1653
+ }
1654
+
1655
+ .floating-toolbar.visible {
1656
+ opacity: 1;
1657
+ transform: translateY(0);
1658
+ }
1659
+
1660
+ .floating-toolbar-content {
1661
+ display: flex;
1662
+ align-items: center;
1663
+ gap: 0;
1664
+ }
1665
+
1666
+ .floating-toolbar-separator {
1667
+ width: 1px;
1668
+ height: 18px;
1669
+ margin: 0 4px;
1670
+ background-color: #ddd;
1671
+ }
1672
+
1673
+ .floating-toolbar-btn {
1674
+ position: relative;
1675
+ display: flex;
1676
+ align-items: center;
1677
+ justify-content: center;
1678
+ width: 28px;
1679
+ height: 28px;
1680
+ border-radius: 5px;
1681
+ cursor: pointer;
1682
+ user-select: none;
1683
+ }
1684
+
1685
+ .floating-toolbar-btn svg {
1686
+ width: 24px;
1687
+ height: 24px;
1688
+ }
1689
+
1690
+ .floating-toolbar-btn svg * {
1691
+ fill: #1385B7;
1692
+ transition: fill 100ms ease;
1693
+ }
1694
+
1695
+ .floating-toolbar-btn:hover:not(.disabled) {
1696
+ background-color: #e6f7ff;
1697
+ }
1698
+
1699
+ .floating-toolbar-btn:hover:not(.disabled) svg * {
1700
+ fill: #166689;
1701
+ }
1702
+
1703
+ .floating-toolbar-btn:active:not(.disabled) {
1704
+ background-color: #cdebf9;
1705
+ }
1706
+
1707
+ .floating-toolbar-btn.disabled {
1708
+ cursor: default;
1709
+ }
1710
+
1711
+ .floating-toolbar-btn.disabled svg * {
1712
+ fill: #b8c4ca;
1713
+ }
1714
+
1715
+ /* Tooltip shown above the button on hover */
1716
+ .floating-toolbar-btn[data-tooltip]:hover:not(.disabled)::after {
1717
+ content: attr(data-tooltip);
1718
+ position: absolute;
1719
+ bottom: calc(100% + 8px);
1720
+ left: 50%;
1721
+ transform: translateX(-50%);
1722
+ background-color: rgba(40, 40, 40, 0.92);
1723
+ color: #fff;
1724
+ font-size: 11px;
1725
+ line-height: 1;
1726
+ white-space: nowrap;
1727
+ padding: 5px 7px;
1728
+ border-radius: 4px;
1729
+ pointer-events: none;
1730
+ z-index: 1;
1731
+ }
1732
+
1555
1733
  .zoom-box {
1556
1734
  position: absolute;
1557
1735
  border: 1px solid #cc6acc;