Skip to content
Permalink
Browse files

Curved path text rendering (#1112)

  • Loading branch information...
Adrian Batzill authored and devemux86 committed Mar 28, 2019
1 parent b1d9359 commit d5359eb716df92c309d440cf607305cd53e07d1a
@@ -4,6 +4,7 @@
* Copyright 2016-2019 devemux86
* Copyright 2017 usrusr
* Copyright 2019 cpt1gl0
* Copyright 2019 Adrian Batzill
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
@@ -41,6 +42,8 @@ void drawBitmap(Bitmap bitmap, int srcLeft, int srcTop, int srcRight, int srcBot

void drawPath(Path path, Paint paint);

void drawPathText(String text, Path path, Paint paint);

void drawText(String text, int x, int y, Paint paint);

void drawTextRotated(String text, int x1, int y1, int x2, int y2, Paint paint);
@@ -2,6 +2,7 @@
* Copyright 2010, 2011, 2012, 2013 mapsforge.org
* Copyright 2014 Ludwig M Brinckmann
* Copyright 2016 devemux86
* Copyright 2019 Adrian Batzill
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
@@ -19,48 +20,48 @@
import org.mapsforge.core.graphics.Canvas;
import org.mapsforge.core.graphics.Display;
import org.mapsforge.core.graphics.Filter;
import org.mapsforge.core.graphics.GraphicFactory;
import org.mapsforge.core.graphics.GraphicUtils;
import org.mapsforge.core.graphics.Matrix;
import org.mapsforge.core.graphics.Paint;
import org.mapsforge.core.graphics.Path;
import org.mapsforge.core.model.LineSegment;
import org.mapsforge.core.model.LineString;
import org.mapsforge.core.model.Point;
import org.mapsforge.core.model.Rectangle;

public class WayTextContainer extends MapElementContainer {
private final GraphicFactory graphicFactory;
private final LineString lineString;
private final Paint paintFront;
private final Paint paintBack;
private final String text;
private final Point end;

public WayTextContainer(Point point, Point end, Display display, int priority, String text, Paint paintFront, Paint paintBack, double textHeight) {
super(point, display, priority);
public WayTextContainer(GraphicFactory graphicFactory, LineString lineString, Display display, int priority, String text, Paint paintFront, Paint paintBack, double textHeight) {
super(lineString.segments.get(0).start, display, priority);
this.graphicFactory = graphicFactory;
this.lineString = lineString;
this.text = text;
this.paintFront = paintFront;
this.paintBack = paintBack;
this.end = end;

this.boundary = null;
// a way text container should always run left to right, but I leave this in because it might matter
// if we support right-to-left text.
// we also need to make the container larger by textHeight as otherwise the end points do
// not correctly reflect the size of the text on screen
this.boundaryAbsolute = new Rectangle(Math.min(point.x, end.x), Math.min(point.y, end.y),
Math.max(point.x, end.x), Math.max(point.y, end.y)).enlarge(0d, textHeight / 2d, 0d, textHeight / 2d);
this.boundaryAbsolute = lineString.getBounds().enlarge(textHeight / 2d, textHeight / 2d, textHeight / 2d, textHeight / 2d);
}

@Override
public void draw(Canvas canvas, Point origin, Matrix matrix, Filter filter) {
Point adjustedStart = xy.offset(-origin.x, -origin.y);
Point adjustedEnd = end.offset(-origin.x, -origin.y);
Path path = generatePath(origin);

if (this.paintBack != null) {
int color = this.paintBack.getColor();
if (filter != Filter.NONE) {
this.paintBack.setColor(GraphicUtils.filterColor(color, filter));
}
canvas.drawTextRotated(text, (int) (adjustedStart.x),
(int) (adjustedStart.y),
(int) (adjustedEnd.x),
(int) (adjustedEnd.y), this.paintBack);
canvas.drawPathText(this.text, path, this.paintBack);
if (filter != Filter.NONE) {
this.paintBack.setColor(color);
}
@@ -69,15 +70,38 @@ public void draw(Canvas canvas, Point origin, Matrix matrix, Filter filter) {
if (filter != Filter.NONE) {
this.paintFront.setColor(GraphicUtils.filterColor(color, filter));
}
canvas.drawTextRotated(text, (int) (adjustedStart.x),
(int) (adjustedStart.y),
(int) (adjustedEnd.x),
(int) (adjustedEnd.y), this.paintFront);
canvas.drawPathText(this.text, path, this.paintFront);
if (filter != Filter.NONE) {
this.paintFront.setColor(color);
}
}

private Path generatePath(Point origin) {
LineSegment firstSegment = this.lineString.segments.get(0);
// So text isn't upside down
boolean doInvert = firstSegment.end.x <= firstSegment.start.x;
Path path = this.graphicFactory.createPath();

if (!doInvert) {
Point start = firstSegment.start.offset(-origin.x, -origin.y);
path.moveTo((float) start.x, (float) start.y);
for (int i = 0; i < this.lineString.segments.size(); i++) {
LineSegment segment = this.lineString.segments.get(i);
Point end = segment.end.offset(-origin.x, -origin.y);
path.lineTo((float) end.x, (float) end.y);
}
} else {
Point end = this.lineString.segments.get(this.lineString.segments.size() - 1).end.offset(-origin.x, -origin.y);
path.moveTo((float) end.x, (float) end.y);
for (int i = this.lineString.segments.size() - 1; i >= 0; i--) {
LineSegment segment = this.lineString.segments.get(i);
Point start = segment.start.offset(-origin.x, -origin.y);
path.lineTo((float) start.x, (float) start.y);
}
}
return path;
}

@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
@@ -1,5 +1,6 @@
/*
* Copyright 2014 Ludwig M Brinckmann
* Copyright 2019 Adrian Batzill
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
@@ -51,6 +52,25 @@ public LineSegment(Point start, Point direction, double distance) {
this.end = new LineSegment(start, direction).pointAlongLineSegment(distance);
}

/**
* Computes the angle between this LineSegment and another one.
*
* @param other the other LineSegment
* @return angle in degrees
*/
public double angleTo(LineSegment other) {
double angle1 = Math.atan2(this.start.y - this.end.y, this.start.x - this.end.x);
double angle2 = Math.atan2(other.start.y - other.end.y, other.start.x - other.end.x);
double angle = Math.toDegrees(angle1 - angle2);
if (angle <= -180) {
angle += 360;
}
if (angle >= 180) {
angle -= 360;
}
return angle;
}

/**
* Intersection of this LineSegment with the Rectangle as another LineSegment.
* <p/>
@@ -0,0 +1,132 @@
/*
* Copyright 2019 Adrian Batzill
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mapsforge.core.model;

import java.util.ArrayList;
import java.util.List;

public class LineString {
public final List<LineSegment> segments = new ArrayList<>();

public void LineString() {
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof LineString)) {
return false;
}
LineString other = (LineString) obj;
if (other.segments.size() != segments.size()) {
return false;
}
for (int i = 0; i < segments.size(); i++) {
if (!segments.get(i).equals(other.segments.get(i))) {
return false;
}
}
return true;
}

/**
* Creates a new LineString that consists of only the part between startDistance and endDistance.
*/
public LineString extractPart(double startDistance, double endDistance) {
LineString result = new LineString();

for (int i = 0; i < this.segments.size(); startDistance -= this.segments.get(i).length(), endDistance -= this.segments.get(i).length(), i++) {
LineSegment segment = this.segments.get(i);

// Skip first segments that we don't need
double length = segment.length();
if (length < startDistance) {
continue;
}

Point startPoint = null, endPoint = null;
if (startDistance >= 0) {
// This will be our starting point
startPoint = segment.pointAlongLineSegment(startDistance);
}
if (endDistance < length) {
// this will be our ending point
endPoint = segment.pointAlongLineSegment(endDistance);
}

if (startPoint != null && endPoint == null) {
// This ist the starting segment, end will come in a later segment
result.segments.add(new LineSegment(startPoint, segment.end));
} else if (startPoint == null && endPoint == null) {
// Center segment between start and end segment, add completely
result.segments.add(segment);
} else if (startPoint == null && endPoint != null) {
// End segment, start was in earlier segment
result.segments.add(new LineSegment(segment.start, endPoint));
} else if (startPoint != null && endPoint != null) {
// Start and end on same segment
result.segments.add(new LineSegment(startPoint, endPoint));
}

if (endPoint != null)
break;
}

return result;
}

public Rectangle getBounds() {
double minX = Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double maxX = Double.MIN_VALUE;
double maxY = Double.MIN_VALUE;

for (LineSegment segment : this.segments) {
minX = Math.min(minX, Math.min(segment.start.x, segment.end.x));
minY = Math.min(minY, Math.min(segment.start.y, segment.end.y));
maxX = Math.max(maxX, Math.max(segment.start.x, segment.end.x));
maxY = Math.max(maxY, Math.max(segment.start.y, segment.end.y));
}
return new Rectangle(minX, minY, maxX, maxY);
}

/**
* Interpolates on the segment and returns the coordinate of the interpolation result.
* Returns null if distance is < 0 or > length().
*/
public Point interpolate(double distance) {
if (distance < 0) {
return null;
}

for (LineSegment segment : this.segments) {
double length = segment.length();
if (distance <= length) {
return segment.pointAlongLineSegment(distance);
}
distance -= length;
}
return null;
}

public double length() {
double result = 0;
for (LineSegment segment : this.segments) {
result += segment.length();
}
return result;
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2019 Adrian Batzill
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mapsforge.core.model;

import org.junit.Assert;
import org.junit.Test;

public class LineStringTest {
@Test
public void boundsTest() {
Point point1 = new Point(0, 0);
Point point2 = new Point(1, 0);
Point point3 = new Point(1, 1);

LineString lineString = new LineString();
lineString.segments.add(new LineSegment(point1, point2));
lineString.segments.add(new LineSegment(point2, point3));

Assert.assertEquals(new Rectangle(0, 0, 1, 1), lineString.getBounds());
}

@Test
public void extractPartTest() {
Point point1 = new Point(0, 0);
Point point2 = new Point(1, 0);
Point point3 = new Point(1, 1);

LineString lineString = new LineString();
lineString.segments.add(new LineSegment(point1, point2));
lineString.segments.add(new LineSegment(point2, point3));

LineString extract1 = new LineString();
extract1.segments.add(new LineSegment(new Point(0, 0), new Point(0.5, 0)));

LineString extract2 = new LineString();
extract2.segments.add(new LineSegment(new Point(0.5, 0), new Point(1, 0)));
extract2.segments.add(new LineSegment(new Point(1, 0), new Point(1, 0.5)));

LineString extract3 = new LineString();
extract3.segments.add(new LineSegment(new Point(1, 0.5), new Point(1, 1)));

Assert.assertEquals(extract1, lineString.extractPart(0, 0.5));
Assert.assertEquals(extract2, lineString.extractPart(0.5, 1.5));
Assert.assertEquals(extract3, lineString.extractPart(1.5, 2));
}

@Test
public void interpolateTest() {
Point point1 = new Point(0, 0);
Point point2 = new Point(1, 0);
Point point3 = new Point(1, 1);

LineString lineString = new LineString();
lineString.segments.add(new LineSegment(point1, point2));
lineString.segments.add(new LineSegment(point2, point3));

Assert.assertEquals(point1, lineString.interpolate(0));
Assert.assertEquals(new Point(0.5, 0), lineString.interpolate(0.5));
Assert.assertEquals(point2, lineString.interpolate(1));
Assert.assertEquals(new Point(1, 0.5), lineString.interpolate(1.5));
Assert.assertEquals(point3, lineString.interpolate(2));
}

@Test
public void lengthTest() {
Point point1 = new Point(0, 0);
Point point2 = new Point(1, 0);
Point point3 = new Point(1, 1);
Point point4 = new Point(3, 1);

LineString lineString = new LineString();
lineString.segments.add(new LineSegment(point1, point2));
lineString.segments.add(new LineSegment(point2, point3));
lineString.segments.add(new LineSegment(point3, point4));

Assert.assertEquals(4, lineString.length(), 0.0001);
Assert.assertEquals(3, lineString.segments.size());
}
}
Oops, something went wrong.

0 comments on commit d5359eb

Please sign in to comment.
You can’t perform that action at this time.