Polygon label position enhancements (#204), closes #80

This commit is contained in:
Andrey Novikov 2016-10-14 23:54:52 +03:00 committed by Emux
parent a322768f8a
commit ca841f5181
9 changed files with 319 additions and 29 deletions

View File

@ -145,7 +145,8 @@
<xs:attribute name="stroke-width" default="0" type="tns:nonNegativeFloat" use="optional" />
<!-- priority for label placement, 0 = highest priority -->
<xs:attribute name="priority" default="0" type="xs:integer" use="optional" />
<!-- polygon area expressed as a ratio to tile area, e.g. 0.1 for 10% of tile area -->
<xs:attribute name="area-size" default="0" type="tns:nonNegativeFloat" use="optional" />
<!-- symbol src name in atlas -->
<xs:attribute name="symbol" type="tns:src" use="optional" />
</xs:complexType>

View File

@ -260,6 +260,7 @@
-->
<m v="parking" zoom-min="15">
<area fill="#f4f4f4" stroke="#d4d4d4" stroke-width="0.2" />
<symbol src="assets:symbols/transport/parking.svg" />
</m>
<m closed="yes" v="fountain">
<area fill="#b4cbdc" stroke="#000080" stroke-width="0.15" />
@ -401,6 +402,7 @@
<m e="way">
<m closed="yes" k="natural" v="water">
<area use="water" />
<caption k="name" fill="#777744" size="16" stroke="#aaffffff" stroke-width="2.0" area-size="0.4" />
<!--<line use="water:outline" />-->
</m>
@ -521,7 +523,18 @@
<!-- runways areas -->
<m k="aeroway">
<m closed="yes" v="aerodrome">
<area fill="#e8ecde" />
<m zoom-min="12">
<area fill="#e8ecde" />
</m>
<m zoom-min="12">
<caption dy="18" fill="#000000" k="name" priority="5" size="19" stroke="#ffffff"
stroke-width="2.0" area-size="0.1"/>
</m>
<m zoom-max="11">
<caption dy="18" fill="#000000" k="ref" priority="5" size="19" stroke="#ffffff"
stroke-width="2.0" />
</m>
<symbol src="assets:symbols/transport/airport.svg" />
</m>
<!-- A place where planes are parked -->
<m v="apron">
@ -554,7 +567,7 @@
<m zoom-min="17">
<caption style="bold" fill="#4040ff" k="name" priority="9" size="14"
stroke="#ffffff" stroke-width="2.0" />
<caption style="bold" fill="#606060" k="addr:housenumber" priority="10" size="10"
<caption style="bold" fill="#606060" k="addr:housenumber" priority="10" size="16"
stroke="#ffffff" stroke-width="2.0" />
</m>
</m>
@ -929,7 +942,7 @@
</m>
<m k="highway" v="track">
<m k="areay" v="yes">
<m k="area" v="yes">
<area fill="#aaff0000" />
</m>
</m>
@ -984,6 +997,7 @@
<!-- place -->
<m k="place">
<circle radius="5.0" fill="#ff0000"/>
<m v="suburb" zoom-max="14">
<caption style="italic" fill="#606060" k="name" priority="4" size="17"
stroke="#ffffff" stroke-width="2.0" />
@ -998,7 +1012,7 @@
</m>
<m v="town">
<caption dy="14" fill="#000000" k="name" priority="2" size="19" stroke="#ffffff"
stroke-width="2.0" symbol="assets:symbols/dot_white.svg" />
stroke-width="2.0" />
</m>
<m v="city">
<m zoom-min="7">
@ -1039,7 +1053,7 @@
<m k="aeroway" v="helipad" zoom-min="16">
<symbol src="assets:symbols/transport/helicopter_pad.svg" />
</m>
<m k="aeroway" v="aerodrome|airport">
<m k="aeroway" v="aerodrome|airport" zoom-min="9">
<symbol src="assets:symbols/transport/airport.svg" />
</m>
</m>

View File

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

View File

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

View File

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

View File

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

View File

@ -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<TextStyle> {
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<TextStyle> {
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<TextStyle> {
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<TextStyle> {
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<TextStyle> {
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<TextStyle> {
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<TextStyle> {
public final boolean caption;
public final float dy;
public final int priority;
public final float areaSize;
public float fontHeight;
public float fontDescent;

View File

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

View File

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