Add an option for the Visvalingam simplification algorithm ()

* First attempt at porting Paul Mach's Visvalingam implementation

* Make indent

* Mostly working

* Approximate equivalence from Douglas-Peucker to Visvalingam levels

* Don't simplify away tile boundary crossing points

* Add a command-line option and test for Visvalingam simplification

* Update changelog

* Cleanup in response to review feedback

* Include <stdio.h> to fix compiler warnings
This commit is contained in:
Erica Fischer 2022-09-30 12:00:07 -07:00 committed by GitHub
parent 6de00c15ec
commit 4ef430b913
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 4282 additions and 4 deletions

@ -1,3 +1,7 @@
## 2.7.0
* Add the option to use the Visvalingam simplification algorithm
## 2.6.4
* Update tests that should have been updated in 2.6.2

@ -47,7 +47,7 @@ C = $(wildcard *.c) $(wildcard *.cpp)
INCLUDES = -I/usr/local/include -I.
LIBS = -L/usr/local/lib
tippecanoe: geojson.o jsonpull/jsonpull.o tile.o pool.o mbtiles.o geometry.o projection.o memfile.o mvt.o serial.o main.o text.o dirtiles.o plugin.o read_json.o write_json.o geobuf.o flatgeobuf.o evaluator.o geocsv.o csv.o geojson-loop.o json_logger.o
tippecanoe: geojson.o jsonpull/jsonpull.o tile.o pool.o mbtiles.o geometry.o projection.o memfile.o mvt.o serial.o main.o text.o dirtiles.o plugin.o read_json.o write_json.o geobuf.o flatgeobuf.o evaluator.o geocsv.o csv.o geojson-loop.o json_logger.o visvalingam.o
$(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread
tippecanoe-enumerate: enumerate.o

@ -483,6 +483,7 @@ the same layer, enclose them in an `all` expression so they will all be evaluate
* `-pn` or `--no-simplification-of-shared-nodes`: Don't simplify away nodes that appear in more than one feature or are used multiple times within the same feature, so that the intersection node will not be lost from intersecting roads. (This will not be effective if you also use `--coalesce` or `--detect-shared-borders`.)
* `-pt` or `--no-tiny-polygon-reduction`: Don't combine the area of very small polygons into small squares that represent their combined area.
* `--tiny-polygon-size=`_size_: Use the specified _size_ for tiny polygons instead of the default 2. Anything above 6 or so will lead to visible artifacts with the default tile detail.
* `-av` or `--visvalingam`: Use Visvalingam's simplification algorithm rather than Douglas-Peucker's.
### Attempts to improve shared polygon boundaries

@ -849,8 +849,19 @@ drawvec simplify_lines(drawvec &geom, int z, int detail, bool mark_tile_bounds,
geom[i].necessary = 1;
geom[j - 1].necessary = 1;
// empirical mapping from douglas-peucker simplifications
// to visvalingam simplifications that yield similar
// output sizes
double sim = simplification * (0.1596 * z + 0.878);
double scale = (res * sim) * (res * sim);
scale = exp(1.002 * log(scale) + 0.3043);
if (j - i > 1) {
douglas_peucker(geom, i, j - i, res * simplification, 2, retain);
if (additional[A_VISVALINGAM]) {
visvalingam(geom, i, j, scale, retain);
} else {
douglas_peucker(geom, i, j - i, res * simplification, 2, retain);
}
}
i = j - 1;
}

@ -4,6 +4,7 @@
#include <vector>
#include <atomic>
#include <sqlite3.h>
#include <stdio.h>
#define VT_POINT 1
#define VT_LINE 2
@ -79,5 +80,6 @@ double get_mp_area(drawvec &geom);
drawvec simple_clip_poly(drawvec &geom, long long x1, long long y1, long long x2, long long y2);
drawvec clip_lines(drawvec &geom, long long x1, long long y1, long long x2, long long y2);
drawvec clip_point(drawvec &geom, long long x1, long long y1, long long x2, long long y2);
void visvalingam(drawvec &ls, size_t start, size_t end, double threshold, size_t retain);
#endif

@ -2730,6 +2730,7 @@ int main(int argc, char **argv) {
{"no-tiny-polygon-reduction", no_argument, &prevent[P_TINY_POLYGON_REDUCTION], 1},
{"tiny-polygon-size", required_argument, 0, '~'},
{"no-simplification-of-shared-nodes", no_argument, &prevent[P_SIMPLIFY_SHARED_NODES], 1},
{"visvalingam", no_argument, &additional[A_VISVALINGAM], 1},
{"Attempts to improve shared polygon boundaries", 0, 0, 0},
{"detect-shared-borders", no_argument, &additional[A_DETECT_SHARED_BORDERS], 1},

@ -35,7 +35,7 @@ For this fork you will need to build from the source repository:
.PP
.RS
.nf
$ git clone https://github.com/protomaps/tippecanoe.git
$ git clone https://github.com/felt/tippecanoe.git
$ cd tippecanoe
$ make \-j
$ make install
@ -616,6 +616,8 @@ the line or polygon within one tile unit of its proper location. You can probabl
\fB\fC\-pt\fR or \fB\fC\-\-no\-tiny\-polygon\-reduction\fR: Don't combine the area of very small polygons into small squares that represent their combined area.
.IP \(bu 2
\fB\fC\-\-tiny\-polygon\-size=\fR\fIsize\fP: Use the specified \fIsize\fP for tiny polygons instead of the default 2. Anything above 6 or so will lead to visible artifacts with the default tile detail.
.IP \(bu 2
\fB\fC\-av\fR or \fB\fC\-\-visvalingam\fR: Use Visvalingam's simplification algorithm rather than Douglas\-Peucker's.
.RE
.SS Attempts to improve shared polygon boundaries
.RS

@ -25,6 +25,7 @@
#define A_GENERATE_IDS ((int) 'i')
#define A_CONVERT_NUMERIC_IDS ((int) 'I')
#define A_HILBERT ((int) 'h')
#define A_VISVALINGAM ((int) 'v')
#define P_SIMPLIFY ((int) 's')
#define P_SIMPLIFY_LOW ((int) 'S')

File diff suppressed because one or more lines are too long

@ -1,6 +1,6 @@
#ifndef VERSION_HPP
#define VERSION_HPP
#define VERSION "v2.6.4"
#define VERSION "v2.7.0"
#endif

230
visvalingam.cpp Normal file

@ -0,0 +1,230 @@
// Adapted from
// https://github.com/paulmach/orb/blob/dcade4901baea0727377ccf7c4aab2addd92d152/simplify/visvalingam.go
// The MIT License (MIT)
//
// Copyright (c) 2017 Paul Mach
//
// 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.
#include <vector>
#include <cmath>
#include "geometry.hpp"
// Stuff to create the priority queue, or min heap.
// Rewriting it here, vs using the std lib, resulted in a 50% performance bump!
struct visItem {
double area = 0; // triangle area
int pointIndex = 0; // index of point in original path
// to keep a virtual linked list to help rebuild the triangle areas as we remove points.
visItem *next = NULL;
visItem *previous = NULL;
int index = 0; // interal index in heap, for removal and update
};
struct minHeap {
std::vector<visItem *> h;
void Push(visItem *item) {
item->index = h.size();
h.push_back(item);
up(item->index);
}
visItem *Pop() {
visItem *removed = h[0];
visItem *lastItem = h[h.size() - 1];
h.pop_back();
if (h.size() > 0) {
lastItem->index = 0;
h[0] = lastItem;
down(0);
}
return removed;
}
void Update(visItem *item, double area) {
if (item->area > area) {
// area got smaller
item->area = area;
up(item->index);
} else {
// area got larger
item->area = area;
down(item->index);
}
}
void up(int i) {
visItem *object = h[i];
while (i > 0) {
int up = ((i + 1) >> 1) - 1;
visItem *parent = h[up];
if (parent->area <= object->area) {
// parent is smaller so we're done fixing up the heap.
break;
}
// swap nodes
parent->index = i;
h[i] = parent;
object->index = up;
h[up] = object;
i = up;
}
}
void down(int i) {
visItem *object = h[i];
while (1) {
size_t right = (i + 1) << 1;
size_t left = right - 1;
int down = i;
visItem *child = h[down];
// swap with smallest child
if (left < h.size() && h[left]->area < child->area) {
down = left;
child = h[down];
}
if (right < h.size() && h[right]->area < child->area) {
down = right;
child = h[down];
}
// non smaller, so quit
if (down == i) {
break;
}
// swap the nodes
child->index = i;
h[child->index] = child;
object->index = down;
h[down] = object;
i = down;
}
}
};
static double doubleTriangleArea(drawvec const &ls, int start, int i1, int i2, int i3) {
draw a = ls[i1 + start];
draw b = ls[i2 + start];
draw c = ls[i3 + start];
return std::abs((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x));
}
void visvalingam(drawvec &ls, size_t start, size_t end, double threshold, size_t retain) {
// edge cases checked, get on with it
int removed = 0;
threshold *= 2;
// build the initial minheap linked list.
minHeap heap;
visItem linkedListStart;
linkedListStart.area = INFINITY;
linkedListStart.pointIndex = 0;
heap.Push(&linkedListStart);
// internal path items
std::vector<visItem> items;
items.resize((end - start));
{
visItem *previous = &linkedListStart;
for (size_t i = 1; i < (end - start) - 1; i++) {
visItem *item = &items[i];
item->area = doubleTriangleArea(ls, start, i - 1, i, i + 1);
item->pointIndex = i;
item->previous = previous;
heap.Push(item);
previous->next = item;
previous = item;
}
// final item
visItem *endItem = &items[(end - start) - 1];
endItem->area = INFINITY;
endItem->pointIndex = (end - start) - 1;
endItem->previous = previous;
previous->next = endItem;
heap.Push(endItem);
}
// run through the reduction process
while (heap.h.size() > 0) {
visItem *current = heap.Pop();
if (current->area > threshold || (end - start) - removed <= retain) {
break;
}
visItem *next = current->next;
visItem *previous = current->previous;
// remove current element from linked list
previous->next = current->next;
next->previous = current->previous;
removed++;
// figure out the new areas
if (previous->previous != NULL) {
double area = doubleTriangleArea(ls, start,
previous->previous->pointIndex,
previous->pointIndex,
next->pointIndex);
area = std::max(area, current->area);
heap.Update(previous, area);
}
if (next->next != NULL) {
double area = doubleTriangleArea(ls, start,
previous->pointIndex,
next->pointIndex,
next->next->pointIndex);
area = std::max(area, current->area);
heap.Update(next, area);
}
}
visItem *item = &linkedListStart;
while (item != NULL) {
ls[item->pointIndex + start].necessary = 1;
item = item->next;
}
}