From ca841f5181cfd07d4e2b69458a45a72efe9ff11c Mon Sep 17 00:00:00 2001 From: Andrey Novikov Date: Fri, 14 Oct 2016 23:54:52 +0300 Subject: [PATCH] Polygon label position enhancements (#204), closes #80 --- resources/rendertheme.xsd | 3 +- .../resources/assets/styles/default.xml | 24 ++- vtm/src/org/oscim/core/GeometryBuffer.java | 22 ++ vtm/src/org/oscim/core/MapElement.java | 6 + .../vector/labeling/LabelTileLoaderHook.java | 55 +++-- vtm/src/org/oscim/theme/XmlThemeBuilder.java | 4 + vtm/src/org/oscim/theme/styles/TextStyle.java | 12 ++ .../tiling/source/mapfile/MapDatabase.java | 24 ++- vtm/src/org/oscim/utils/geom/PolyLabel.java | 198 ++++++++++++++++++ 9 files changed, 319 insertions(+), 29 deletions(-) create mode 100644 vtm/src/org/oscim/utils/geom/PolyLabel.java diff --git a/resources/rendertheme.xsd b/resources/rendertheme.xsd index fe4af0f0..4d11511b 100644 --- a/resources/rendertheme.xsd +++ b/resources/rendertheme.xsd @@ -145,7 +145,8 @@ - + + diff --git a/vtm-themes/resources/assets/styles/default.xml b/vtm-themes/resources/assets/styles/default.xml index a18ff1d5..3c0c5246 100644 --- a/vtm-themes/resources/assets/styles/default.xml +++ b/vtm-themes/resources/assets/styles/default.xml @@ -260,6 +260,7 @@ --> + @@ -401,6 +402,7 @@ + @@ -521,7 +523,18 @@ - + + + + + + + + + + @@ -554,7 +567,7 @@ - @@ -929,7 +942,7 @@ - + @@ -984,6 +997,7 @@ + @@ -998,7 +1012,7 @@ + stroke-width="2.0" /> @@ -1039,7 +1053,7 @@ - + diff --git a/vtm/src/org/oscim/core/GeometryBuffer.java b/vtm/src/org/oscim/core/GeometryBuffer.java index d9dc57fc..9e2c1ec1 100644 --- a/vtm/src/org/oscim/core/GeometryBuffer.java +++ b/vtm/src/org/oscim/core/GeometryBuffer.java @@ -1,5 +1,6 @@ /* * Copyright 2013 Hannes Janetzek + * Copyright 2016 Andrey Novikov * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -411,6 +412,27 @@ public class GeometryBuffer { } } + /** + * Calculates geometry area, only polygon outer ring is taken into account. + * + * @return polygon area, 0 for other geometries + */ + public float area() { + if (isPoint() || isLine() || getNumPoints() < 3) + return 0f; + + float area = 0f; + // use only outer ring + int n = index[0]; + + for (int i = 0; i < n - 2; i += 2) { + area = area + (points[i] * points[i+3]) - (points[i+1] * points[i+2]); + } + area = area + (points[n-2] * points[1]) - (points[n-1] * points[0]); + + return 0.5f * area; + } + public String toString() { StringBuffer sb = new StringBuffer(); int o = 0; diff --git a/vtm/src/org/oscim/core/MapElement.java b/vtm/src/org/oscim/core/MapElement.java index fd2187f6..a0258181 100644 --- a/vtm/src/org/oscim/core/MapElement.java +++ b/vtm/src/org/oscim/core/MapElement.java @@ -33,6 +33,8 @@ public class MapElement extends GeometryBuffer { public final TagSet tags = new TagSet(); + public PointF labelPosition; + public MapElement() { super(1024, 16); } @@ -45,6 +47,10 @@ public class MapElement extends GeometryBuffer { this.layer = layer; } + public void setLabelPosition(float x, float y) { + labelPosition = new PointF(x, y); + } + @Override public MapElement clear() { layer = 5; diff --git a/vtm/src/org/oscim/layers/tile/vector/labeling/LabelTileLoaderHook.java b/vtm/src/org/oscim/layers/tile/vector/labeling/LabelTileLoaderHook.java index 88282388..d4bec050 100644 --- a/vtm/src/org/oscim/layers/tile/vector/labeling/LabelTileLoaderHook.java +++ b/vtm/src/org/oscim/layers/tile/vector/labeling/LabelTileLoaderHook.java @@ -1,5 +1,6 @@ /* * Copyright 2016 devemux86 + * Copyright 2016 Andrey Novikov * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -18,6 +19,7 @@ package org.oscim.layers.tile.vector.labeling; import org.oscim.core.MapElement; import org.oscim.core.PointF; +import org.oscim.core.Tile; import org.oscim.layers.tile.MapTile; import org.oscim.layers.tile.vector.VectorTileLayer.TileLoaderThemeHook; import org.oscim.renderer.bucket.RenderBuckets; @@ -26,6 +28,7 @@ import org.oscim.renderer.bucket.TextItem; import org.oscim.theme.styles.RenderStyle; import org.oscim.theme.styles.SymbolStyle; import org.oscim.theme.styles.TextStyle; +import org.oscim.utils.geom.PolyLabel; import static org.oscim.core.GeometryBuffer.GeometryType.LINE; import static org.oscim.core.GeometryBuffer.GeometryType.POINT; @@ -70,23 +73,25 @@ public class LabelTileLoaderHook implements TileLoaderThemeHook { offset += length; } } else if (element.type == POLY) { - // TODO place somewhere on polygon String value = element.tags.getValue(text.textKey); if (value == null || value.length() == 0) return false; - float x = 0; - float y = 0; - int n = element.index[0]; - - for (int i = 0; i < n; ) { - x += element.points[i++]; - y += element.points[i++]; + if (text.areaSize > 0f) { + float area = element.area(); + float ratio = area / (Tile.SIZE * Tile.SIZE); // we can't use static as it's recalculated based on dpi + if (ratio < text.areaSize) + return false; } - x /= (n / 2); - y /= (n / 2); - ld.labels.push(TextItem.pool.get().set(x, y, value, text)); + PointF label = element.labelPosition; + if (label == null) + label = PolyLabel.get(element, 5f); + + if (label.x < 0 || label.x > Tile.SIZE || label.y < 0 || label.y > Tile.SIZE) + return false; + + ld.labels.push(TextItem.pool.get().set(label.x, label.y, value, text)); } else if (element.type == POINT) { String value = element.tags.getValue(text.textKey); if (value == null || value.length() == 0) @@ -97,7 +102,7 @@ public class LabelTileLoaderHook implements TileLoaderThemeHook { ld.labels.push(TextItem.pool.get().set(p.x, p.y, value, text)); } } - } else if ((element.type == POINT) && (style instanceof SymbolStyle)) { + } else if (style instanceof SymbolStyle) { SymbolStyle symbol = (SymbolStyle) style; if (symbol.bitmap == null && symbol.texture == null) @@ -105,14 +110,32 @@ public class LabelTileLoaderHook implements TileLoaderThemeHook { LabelTileData ld = get(tile); - for (int i = 0, n = element.getNumPoints(); i < n; i++) { - PointF p = element.getPoint(i); + if (element.type == POINT) { + for (int i = 0, n = element.getNumPoints(); i < n; i++) { + PointF p = element.getPoint(i); + + SymbolItem it = SymbolItem.pool.get(); + if (symbol.bitmap != null) + it.set(p.x, p.y, symbol.bitmap, true); + else + it.set(p.x, p.y, symbol.texture, true); + ld.symbols.push(it); + } + } else if (element.type == LINE) { + //TODO: implement + } else if (element.type == POLY) { + PointF centroid = element.labelPosition; + if (centroid == null) + return false; + + if (centroid.x < 0 || centroid.x > Tile.SIZE || centroid.y < 0 || centroid.y > Tile.SIZE) + return false; SymbolItem it = SymbolItem.pool.get(); if (symbol.bitmap != null) - it.set(p.x, p.y, symbol.bitmap, true); + it.set(centroid.x, centroid.y, symbol.bitmap, true); else - it.set(p.x, p.y, symbol.texture, true); + it.set(centroid.x, centroid.y, symbol.texture, true); ld.symbols.push(it); } } diff --git a/vtm/src/org/oscim/theme/XmlThemeBuilder.java b/vtm/src/org/oscim/theme/XmlThemeBuilder.java index 3e4d1f0b..4439cef0 100644 --- a/vtm/src/org/oscim/theme/XmlThemeBuilder.java +++ b/vtm/src/org/oscim/theme/XmlThemeBuilder.java @@ -3,6 +3,7 @@ * Copyright 2013 Hannes Janetzek * Copyright 2016 devemux86 * Copyright 2016 Longri + * Copyright 2016 Andrey Novikov * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -863,6 +864,9 @@ public class XmlThemeBuilder extends DefaultHandler { else if ("priority".equals(name)) b.priority = Integer.parseInt(value); + else if ("area-size".equals(name)) + b.areaSize = Float.parseFloat(value); + else if ("dy".equals(name)) // NB: minus.. b.dy = -Float.parseFloat(value) * CanvasAdapter.dpi / 160; diff --git a/vtm/src/org/oscim/theme/styles/TextStyle.java b/vtm/src/org/oscim/theme/styles/TextStyle.java index e961c854..c2b79cdf 100644 --- a/vtm/src/org/oscim/theme/styles/TextStyle.java +++ b/vtm/src/org/oscim/theme/styles/TextStyle.java @@ -1,6 +1,7 @@ /* * Copyright 2013 Hannes Janetzek * Copyright 2016 devemux86 + * Copyright 2016 Andrey Novikov * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -36,6 +37,7 @@ public final class TextStyle extends RenderStyle { public boolean caption; public float dy; public int priority; + public float areaSize; public Bitmap bitmap; public TextureRegion texture; public FontFamily fontFamily; @@ -49,6 +51,7 @@ public final class TextStyle extends RenderStyle { fontSize = 0; caption = false; priority = Integer.MAX_VALUE; + areaSize = 0f; bitmap = null; texture = null; fillColor = Color.BLACK; @@ -98,6 +101,11 @@ public final class TextStyle extends RenderStyle { return self(); } + public T areaSize(float areaSize) { + this.areaSize = areaSize; + return self(); + } + public T bitmap(Bitmap bitmap) { this.bitmap = bitmap; return self(); @@ -126,6 +134,7 @@ public final class TextStyle extends RenderStyle { fontSize = other.fontSize; caption = other.caption; priority = other.priority; + areaSize = other.areaSize; bitmap = other.bitmap; texture = other.texture; fillColor = other.fillColor; @@ -141,6 +150,7 @@ public final class TextStyle extends RenderStyle { this.caption = style.caption; this.dy = style.dy; this.priority = style.priority; + this.areaSize = style.areaSize; this.bitmap = style.bitmap; this.texture = style.texture; this.fillColor = style.paint.getColor(); @@ -159,6 +169,7 @@ public final class TextStyle extends RenderStyle { this.caption = tb.caption; this.dy = tb.dy; this.priority = tb.priority; + this.areaSize = tb.areaSize; this.bitmap = tb.bitmap; this.texture = tb.texture; @@ -193,6 +204,7 @@ public final class TextStyle extends RenderStyle { public final boolean caption; public final float dy; public final int priority; + public final float areaSize; public float fontHeight; public float fontDescent; diff --git a/vtm/src/org/oscim/tiling/source/mapfile/MapDatabase.java b/vtm/src/org/oscim/tiling/source/mapfile/MapDatabase.java index 500f332f..b2529a12 100644 --- a/vtm/src/org/oscim/tiling/source/mapfile/MapDatabase.java +++ b/vtm/src/org/oscim/tiling/source/mapfile/MapDatabase.java @@ -2,6 +2,7 @@ * Copyright 2010, 2011, 2012 mapsforge.org * Copyright 2013, 2014 Hannes Janetzek * Copyright 2016 devemux86 + * Copyright 2016 Andrey Novikov * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -840,9 +841,11 @@ public class MapDatabase implements ITileDataSource { e.tags.add(new Tag(Tag.KEY_REF, str, false)); } } - if ((featureByte & WAY_FEATURE_LABEL_POSITION) != 0) - // labelPosition = - readOptionalLabelPosition(); + + int[] labelPosition = null; + if ((featureByte & WAY_FEATURE_LABEL_POSITION) != 0) { + labelPosition = readOptionalLabelPosition(); + } if ((featureByte & WAY_FEATURE_DATA_BLOCKS_BYTE) != 0) { wayDataBlocks = mReadBuffer.readUnsignedInt(); @@ -870,6 +873,8 @@ public class MapDatabase implements ITileDataSource { continue; } + if (labelPosition != null && wayDataBlock == 0) + e.setLabelPosition(e.points[0] + labelPosition[0], e.points[1] + labelPosition[1]); mTileProjection.project(e); if (!e.tags.containsKey("building")) @@ -879,6 +884,7 @@ public class MapDatabase implements ITileDataSource { e.simplify(1, true); e.setLayer(layer); + mapDataSink.process(e); } } @@ -886,14 +892,14 @@ public class MapDatabase implements ITileDataSource { return true; } - private float[] readOptionalLabelPosition() { - float[] labelPosition = new float[2]; + private int[] readOptionalLabelPosition() { + int[] labelPosition = new int[2]; /* get the label position latitude offset (VBE-S) */ - labelPosition[1] = mTileLatitude + mReadBuffer.readSignedInt(); + labelPosition[1] = mReadBuffer.readSignedInt(); /* get the label position longitude offset (VBE-S) */ - labelPosition[0] = mTileLongitude + mReadBuffer.readSignedInt(); + labelPosition[0] = mReadBuffer.readSignedInt(); return labelPosition; } @@ -1021,6 +1027,10 @@ public class MapDatabase implements ITileDataSource { indices[idx] = (short) cnt; } } + if (e.labelPosition != null) { + e.labelPosition.x = projectLon(e.labelPosition.x); + e.labelPosition.y = projectLat(e.labelPosition.y); + } } } } diff --git a/vtm/src/org/oscim/utils/geom/PolyLabel.java b/vtm/src/org/oscim/utils/geom/PolyLabel.java new file mode 100644 index 00000000..74dc0e89 --- /dev/null +++ b/vtm/src/org/oscim/utils/geom/PolyLabel.java @@ -0,0 +1,198 @@ +/** + * Copyright 2016 Andrey Novikov + * Java implementation of + * https://github.com/mapbox/polylabel + * + * ISC License + * Copyright (c) 2016 Mapbox + * + * Permission to use, copy, modify, and/or distribute this software for any purpose + * with or without fee is hereby granted, provided that the above copyright notice + * and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO + * THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. + * IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR + * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA + * OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + * SOFTWARE. + */ +package org.oscim.utils.geom; + +import org.oscim.core.GeometryBuffer; +import org.oscim.core.PointF; + +import java.util.Comparator; +import java.util.PriorityQueue; + +public class PolyLabel { + private static float SQRT2 = (float) Math.sqrt(2); + + /** + * Returns pole of inaccessibility, the most distant internal point from the polygon outline. + * @param polygon polygon geometry + * @param precision calculation precision + * @return optimal label placement point + */ + public static PointF get(GeometryBuffer polygon, float precision) { + // find the bounding box of the outer ring + float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE, maxX = Float.MIN_VALUE, maxY = Float.MIN_VALUE; + + int n = polygon.index[0]; + + for (int i = 0; i < n; ) { + float x = polygon.points[i++]; + float y = polygon.points[i++]; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + + float width = maxX - minX; + float height = maxY - minY; + float cellSize = Math.min(width, height); + float h = cellSize / 2; + + // a priority queue of cells in order of their "potential" (max distance to polygon) + PriorityQueue cellQueue = new PriorityQueue<>(1, new MaxComparator()); + + // cover polygon with initial cells + for (float x = minX; x < maxX; x += cellSize) { + for (float y = minY; y < maxY; y += cellSize) { + cellQueue.add(new Cell(x + h, y + h, h, polygon)); + } + } + + // take centroid as the first best guess + Cell bestCell = getCentroidCell(polygon); + + // special case for rectangular polygons + Cell bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, polygon); + if (bboxCell.d > bestCell.d) bestCell = bboxCell; + + while (!cellQueue.isEmpty()) { + // pick the most promising cell from the queue + Cell cell = cellQueue.remove(); + + // update the best cell if we found a better one + if (cell.d > bestCell.d) + bestCell = cell; + + // do not drill down further if there's no chance of a better solution + if (cell.max - bestCell.d <= precision) continue; + + // split the cell into four cells + h = cell.h / 2; + cellQueue.add(new Cell(cell.x - h, cell.y - h, h, polygon)); + cellQueue.add(new Cell(cell.x + h, cell.y - h, h, polygon)); + cellQueue.add(new Cell(cell.x - h, cell.y + h, h, polygon)); + cellQueue.add(new Cell(cell.x + h, cell.y + h, h, polygon)); + } + + return new PointF(bestCell.x, bestCell.y); + } + + private static class MaxComparator implements Comparator + { + @Override + public int compare(Cell a, Cell b) { + return Float.compare(b.max, a.max); + } + } + + private static class Cell { + final float x; + final float y; + final float h; + final float d; + final float max; + + Cell(float x, float y, float h, GeometryBuffer polygon) { + this.x = x; // cell center x + this.y = y; // cell center y + this.h = h; // half the cell size + this.d = pointToPolygonDist(x, y, polygon); // distance from cell center to polygon + this.max = this.d + this.h * SQRT2; // max distance to polygon within a cell + } + } + + // signed distance from point to polygon outline (negative if point is outside) + private static float pointToPolygonDist(float x, float y, GeometryBuffer polygon) { + boolean inside = false; + float minDistSq = Float.POSITIVE_INFINITY; + + int pos = 0; + + for (int k = 0; k < polygon.index.length; k++) { + if (polygon.index[k] < 0) + break; + if (polygon.index[k] == 0) + continue; + + for (int i = 0, n = polygon.index[k], j = n - 2; i < n; j = i, i += 2) { + float ax = polygon.points[pos+i]; + float ay = polygon.points[pos+i+1]; + float bx = polygon.points[pos+j]; + float by = polygon.points[pos+j+1]; + + if (((ay > y) ^ (by > y)) && + (x < (bx - ax) * (y - ay) / (by - ay) + ax)) inside = !inside; + + minDistSq = Math.min(minDistSq, getSegDistSq(x, y, ax, ay, bx, by)); + } + + pos += polygon.index[k]; + } + + return (float) ((inside ? 1 : -1) * Math.sqrt(minDistSq)); + } + + // get polygon centroid + private static Cell getCentroidCell(GeometryBuffer polygon) { + float area = 0f; + float x = 0f; + float y = 0f; + + for (int i = 0, n = polygon.index[0], j = n - 2; i < n; j = i, i += 2) { + float ax = polygon.points[i]; + float ay = polygon.points[i+1]; + float bx = polygon.points[j]; + float by = polygon.points[j+1]; + float f = ax * by - bx * ay; + x += (ax + bx) * f; + y += (ay + by) * f; + area += f * 3; + } + return new Cell(x / area, y / area, 0f, polygon); + } + + // get squared distance from a point to a segment + private static float getSegDistSq(float px, float py, float ax, float ay, float bx, float by) { + + float x = ax; + float y = ay; + float dx = bx - x; + float dy = by - y; + + if (dx != 0f || dy != 0f) { + + float t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy); + + if (t > 1) { + x = bx; + y = by; + + } else if (t > 0) { + x += dx * t; + y += dy * t; + } + } + + dx = px - x; + dy = py - y; + + return dx * dx + dy * dy; + } +}