OSMデータ(xml形式)は node, way, relation という階層構造である。座標値は node にある。 way は line や polygon を表す。ここには座標値はなく node id 列がある。 relation のメンバーは node, way, relation である。
構造的には極めてシンプルである。全データをメモリに取り込める場合、 node、way、relation の構造体を定義して、そこにデータを取り込めばよい。 relation は メンバーに relation を含むため、再帰構造となる。
しかし、圧縮ファイルでも 50GB の大きさである。 上記の構造体に全データを取り込むには数百GB~数TBのメモリが必要となる。
つまり、構造はシンプルであるが、メモリに取り込めないため、その操作が非常に厄介である。
間引きでは Ramer-Douglas-Peuckerアルゴリズムが著名で、 PostGISのST_SimplifyPreserveTopologyでもこれが使われている。 PostGISには複数のpolygonが交差しているとき、交差のない部分だけを取り出す機能があり、 これを使って実質上交差を回避している。この実装は間引きよりも難しい。 特に multipolygon の交差回避が難しい。
Ramer-Douglas-Peuckerアルゴリズム(またはDouglas-Peuckerアルゴリズム)は 下に示すように、極めてシンプルなものである。
simplySection関数は線分 points[i]-points[j] の間引きを行う関数である。 許容誤差を epsilon とする。
線分 points[i]-points[j]と途中の点 points[k] との垂直距離を調べて、 その最大値を maxDistance とする。
この最大値が epsilon を超えなければ、全ての中間点は間引いて、処理は終わりである。
最大値が許容誤差を超える場合には、 この最大値を持った点 points[maxIndex] は間引かないことに決定する。 この点を境として、ふたつの線分 points[i]-points[maxIndex]および points[maxIndex]-points[j] に分けて、 それぞれについて simplySection関数を再帰コールする。
[C/C++言語]
// Returns the distance from point p to the line between p1 and p2
double perpendicular_distance(point_t p, point_t p1, point_t p2) {
double dx = p2.x - p1.x;
double dy = p2.y - p1.y;
double d = sqrt(dx * dx + dy * dy);
return fabs(p.x * dy - p.y * dx + p2.x * p1.y - p2.y * p1.x)/d;
}
void simplifySection(point_t *points, size_t i, size_t j, double epsilon) {
if ((i + 1) == j) return;
double maxDistance = -1.0;
size_t maxIndex = i;
for (size_t k = i + 1; k < j; k++) {
double distance = perpendicular_distance(points[k], points[i], points[j]);
if (distance > maxDistance) {
maxDistance = distance;
maxIndex = k;
}
}
if (maxDistance <= epsilon) {
for (size_t k = i + 1; k < j; k++) {
points[k].unused = true; // 間引く
}
} else {
simplifySection(points, i, maxIndex, epsilon);
simplifySection(points, maxIndex, j, epsilon);
}
}
[Java]
// 1度=約100km ⇒ 1m=約0.00001度
public static double PerpendicularDistance(long p, long p1, long p2) {
double E7 = 10000000.0;
double px = getX(p) / E7;
double py = getY(p) / E7;
double p1x = getX(p1) / E7;
double p1y = getY(p1) / E7;
double p2x = getX(p2) / E7;
double p2y = getY(p2) / E7;
double dx = p2x - p1x;
double dy = p2y - p1y;
double d = Math.sqrt(dx*dx + dy*dy);
return Math.abs(px*dy - py*dx + p2x*p1y - p2y*p1x)/d;
}
public static void SimplifySection(long[] pnts, int i, int j, double epsilon) {
if ((i + 1) == j) return;
double maxDistance = -1.0;
int maxIndex = i;
for (int k = i + 1; k < j; k++) {
double distance = PerpendicularDistance(pnts[k], pnts[i], pnts[j]);
if (distance > maxDistance) {
maxDistance = distance;
maxIndex = k;
}
}
if (maxDistance <= epsilon) {
for (int k = i + 1; k < j; k++) {
pnts[k] = Long.MIN_VALUE; // 間引く
}
} else {
SimplifySection(pnts, i, maxIndex, epsilon);
SimplifySection(pnts, maxIndex, j, epsilon);
}
}
このように、再帰を使うことにより、効率よく間引きを行う。