Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions CodenameOne/src/com/codename1/maps/MapView.java
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,11 @@ private void drawOverlays(Graphics g) {
for (Object markerObj : markers) {
drawMarker(g, (Marker) markerObj);
}
// Draw labels last so routes and later pins cannot paint over them.
List labelBounds = new ArrayList();
for (Object markerObj : markers) {
drawMarkerLabel(g, (Marker) markerObj, labelBounds);
}
}

private void drawPolyline(Graphics g, Polyline pl) {
Expand Down Expand Up @@ -502,6 +507,92 @@ private void drawMarker(Graphics g, Marker m) {
g.setAlpha(prevAlpha);
}

private void drawMarkerLabel(Graphics g, Marker marker, List occupied) {
String label = marker.getLabel();
if (!marker.isVisible() || label == null || label.trim().length() == 0) {
return;
}
Point point = engine.latLngToScreen(marker.getPosition());
int px = point.getX();
int py = point.getY();
if (px < 0 || py < 0 || px > getWidth() || py > getHeight()) {
return;
}
Font font = Font.getDefaultFont();
int padding = Math.max(2, CN.convertToPixels(1));
int available = getWidth() - padding * 4;
if (available <= 0 || getHeight() < font.getHeight() + padding * 2) {
return;
}
// Keep long place names inside the viewport without changing the model.
if (font.stringWidth(label) > available) {
String ellipsis = "...";
int end = label.length();
while (end > 0 && font.stringWidth(label.substring(0, end) + ellipsis) > available) {
end--;
}
if (end == 0) {
return;
}
label = label.substring(0, end) + ellipsis;
}
int width = font.stringWidth(label) + padding * 2;
int height = font.getHeight() + padding * 2;
int x = Math.max(0, Math.min(px - width / 2, getWidth() - width));
// Prefer below the pin's tip, away from its icon; flip at the bottom edge.
int y = py + padding;
if (y + height > getHeight()) {
int iconHeight = marker.getIcon() == null ? markerFont().getHeight() : marker.getIcon().getHeight();
y = Math.max(0, py - (int) (iconHeight * marker.getAnchorV()) - height - padding);
}
// Nearby stops (including a round trip's start and destination) must
// not paint their names on top of each other. Try rows below/above.
int preferredY = y;
boolean placed = false;
for (int offset = 0; offset < getHeight(); offset += height + padding) {
if (markerLabelFits(x, preferredY + offset, width, height, occupied)) {
y = preferredY + offset;
placed = true;
break;
}
if (offset > 0 && markerLabelFits(x, preferredY - offset, width, height, occupied)) {
y = preferredY - offset;
placed = true;
break;
}
}
if (!placed) {
return;
}
occupied.add(new int[]{x, y, width, height});
Font previousFont = g.getFont();
int previousAlpha = g.getAlpha();
int previousColor = g.getColor();
g.setAlpha(255);
g.setColor(0xffffff);
g.fillRoundRect(x, y, width, height, padding * 2, padding * 2);
g.setFont(font);
g.setColor(0x222222);
g.drawString(label, x + padding, y + padding);
g.setFont(previousFont);
g.setAlpha(previousAlpha);
g.setColor(previousColor);
}

private boolean markerLabelFits(int x, int y, int width, int height, List occupied) {
if (y < 0 || y + height > getHeight()) {
return false;
}
for (Object item : occupied) {
int[] box = (int[]) item;
if (x < box[0] + box[2] && x + width > box[0]
&& y < box[1] + box[3] && y + height > box[1]) {
return false;
}
}
return true;
}

private Font markerFont() {
if (markerFont == null) {
float size = CN.convertToPixels(7f);
Expand Down
8 changes: 8 additions & 0 deletions CodenameOne/src/com/codename1/maps/Marker.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public final class Marker extends MapObject {
private LatLng position;
private final EncodedImage icon;
private final String title;
private final String label;
private final String snippet;
private final float anchorU;
private final float anchorV;
Expand All @@ -45,6 +46,7 @@ public final class Marker extends MapObject {
this.position = options.getPosition();
this.icon = options.getIcon();
this.title = options.getTitle();
this.label = options.getLabel();
this.snippet = options.getSnippet();
this.anchorU = options.getAnchorU();
this.anchorV = options.getAnchorV();
Expand Down Expand Up @@ -73,6 +75,12 @@ public String getTitle() {
return title;
}

/// The persistent vector-map label, or `null` when none was supplied.
/// See [MarkerOptions#label(String)].
public String getLabel() {
return label;
}

/// The secondary text shown beneath the title (provider dependent).
public String getSnippet() {
return snippet;
Expand Down
15 changes: 15 additions & 0 deletions CodenameOne/src/com/codename1/maps/MarkerOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public final class MarkerOptions {
private LatLng position;
private EncodedImage icon;
private String title;
private String label;
private String snippet;
private float anchorU = 0.5f;
private float anchorV = 1.0f;
Expand Down Expand Up @@ -74,6 +75,16 @@ public MarkerOptions title(String title) {
return this;
}

/// Sets a persistent label beside the pin on [MapView] (including a
/// [NativeMap] using its vector fallback). Unlike [#title], this text is
/// visible without tapping. Native providers still use the info-window
/// title, so set both when the marker should work on either surface.
/// `null` or blank text leaves the pin unlabeled.
public MarkerOptions label(String label) {
this.label = label;
return this;
}

/// Sets the info-window secondary text.
public MarkerOptions snippet(String snippet) {
this.snippet = snippet;
Expand Down Expand Up @@ -116,6 +127,10 @@ String getTitle() {
return title;
}

String getLabel() {
return label;
}

String getSnippet() {
return snippet;
}
Expand Down
69 changes: 68 additions & 1 deletion CodenameOne/src/com/codename1/maps/routing/RouteRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
///
/// ```java
/// RouteRequest req = new RouteRequest(home, office)
/// .addWaypoint(daycare)
/// .setOriginLabel("Home").setDestinationLabel("Office")
/// .addWaypoint(daycare, "Daycare")
/// .setTravelMode(TravelMode.DRIVING)
/// .setAlternatives(true);
/// ```
Expand All @@ -44,6 +45,10 @@ public final class RouteRequest {
private final LatLng origin;
private final LatLng destination;
private final List waypoints = new ArrayList();
private final List waypointLabels = new ArrayList();
private String originLabel;
private String destinationLabel;
private boolean showStopLabels = true;
private TravelMode travelMode = TravelMode.DRIVING;
private boolean alternatives;
private boolean steps = true;
Expand All @@ -69,8 +74,15 @@ public LatLng getDestination() {
/// order. Backends route through waypoints in the order added; none of
/// them reorder to optimize the trip.
public RouteRequest addWaypoint(LatLng waypoint) {
return addWaypoint(waypoint, null);
}

/// Adds a stop with a display name for [Routing#showRoute]. A `null`
/// waypoint is ignored; a blank name uses the numbered default label.
public RouteRequest addWaypoint(LatLng waypoint, String label) {
if (waypoint != null) {
waypoints.add(waypoint);
waypointLabels.add(label);
}
return this;
}
Expand All @@ -83,6 +95,61 @@ public List getWaypoints() {
return Collections.unmodifiableList(waypoints);
}

/// Optional display names in the same order as [#getWaypoints()]. Entries
/// may be `null`; [Routing#showRoute] then uses "Stop 1", "Stop 2", etc.
public List getWaypointLabels() {
return Collections.unmodifiableList(waypointLabels);
}

/// The optional name displayed at the journey's start.
public String getOriginLabel() {
return originLabel;
}

/// Names the start marker. Blank or `null` uses "Start". This is display
/// text only: it does not change the coordinates sent to the service.
public RouteRequest setOriginLabel(String label) {
originLabel = label;
return this;
}

/// The optional name displayed at the destination.
public String getDestinationLabel() {
return destinationLabel;
}

/// Names the destination marker. Blank or `null` uses "Destination".
public RouteRequest setDestinationLabel(String label) {
destinationLabel = label;
return this;
}

/// Whether [Routing#showRoute] adds labeled start, stop and destination
/// markers. True by default. [Routing#findRoute] only returns route data.
public boolean isShowStopLabels() {
return showStopLabels;
}

/// Enables or disables the labeled markers added by [Routing#showRoute].
public RouteRequest setShowStopLabels(boolean show) {
showStopLabels = show;
return this;
}

// Freeze geometry and display metadata together before asynchronous routing.
RouteRequest snapshot() {
RouteRequest copy = new RouteRequest(origin, destination);
copy.waypoints.addAll(waypoints);
copy.waypointLabels.addAll(waypointLabels);
copy.originLabel = originLabel;
copy.destinationLabel = destinationLabel;
copy.showStopLabels = showStopLabels;
copy.travelMode = travelMode;
copy.alternatives = alternatives;
copy.steps = steps;
return copy;
}

/// How the traveller moves; [TravelMode#DRIVING] unless changed.
public TravelMode getTravelMode() {
return travelMode;
Expand Down
53 changes: 49 additions & 4 deletions CodenameOne/src/com/codename1/maps/routing/Routing.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@

import com.codename1.maps.LatLng;
import com.codename1.maps.MapSurface;
import com.codename1.maps.MapBounds;
import com.codename1.maps.MarkerOptions;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.CN;

import java.util.List;
Expand Down Expand Up @@ -225,7 +228,16 @@ public static void showRoute(MapSurface map, LatLng origin, LatLng destination)
/// Draws the best route for `request` on `map`, frames it, and forwards
/// the outcome to `callback`.
///
/// The polyline is added and the camera moved *before* `callback` runs, so
/// Adds labeled markers at the requested start, intermediate stops and
/// destination. Supply place names through [RouteRequest#setOriginLabel],
/// [RouteRequest#setDestinationLabel] and [RouteRequest#addWaypoint(LatLng, String)];
/// otherwise the labels are "Start", "Stop 1", etc. and "Destination"
/// (localized through the UI manager). No reverse geocoding is performed.
/// Labels stay visible on vector maps; native providers show the same
/// names as marker titles. Disable these markers with
/// [RouteRequest#setShowStopLabels]. The request is snapshotted when called.
///
/// The polyline and markers are added and the camera moved *before* `callback` runs, so
/// the callback can read the route's distance and duration to update the
/// UI. It cannot restyle the line that was drawn -- that polyline is not
/// exposed, and [Route#toPolyline()] hands back a fresh one every call. To
Expand All @@ -242,7 +254,8 @@ public static void showRoute(MapSurface map, LatLng origin, LatLng destination)
/// - `callback`: notified of the outcome, or `null` to just draw the route
public static void showRoute(final MapSurface map, RouteRequest request,
final RouteCallback callback) {
findRoute(request, new RouteCallback() {
final RouteRequest submitted = request == null ? null : request.snapshot();
findRoute(submitted, new RouteCallback() {
@Override
public void routesFound(List routes) {
if (routes == null || routes.isEmpty()) {
Expand All @@ -261,8 +274,25 @@ public void routesFound(List routes) {
}
Route best = (Route) first;
map.addPolyline(best.toPolyline());
if (best.getBounds() != null) {
map.fitBounds(best.getBounds(), CN.convertToPixels(4));
MapBounds bounds = best.getBounds();
boolean labels = submitted != null && submitted.isShowStopLabels();
if (labels) {
bounds = addStopMarker(map, submitted.getOrigin(),
stopLabel(submitted.getOriginLabel(), "Start"), bounds);
List stops = submitted.getWaypoints();
List names = submitted.getWaypointLabels();
for (int i = 0; i < stops.size(); i++) {
String name = (String) names.get(i);
if (name == null || name.trim().length() == 0) {
name = UIManager.getInstance().localize("Stop", "Stop") + " " + (i + 1);
}
bounds = addStopMarker(map, (LatLng) stops.get(i), name, bounds);
}
bounds = addStopMarker(map, submitted.getDestination(),
stopLabel(submitted.getDestinationLabel(), "Destination"), bounds);
}
if (bounds != null) {
map.fitBounds(bounds, CN.convertToPixels(labels ? 10 : 4));
}
if (callback != null) {
callback.routesFound(routes);
Expand All @@ -277,4 +307,19 @@ public void routeFailed(String message, Throwable error) {
}
});
}

private static String stopLabel(String name, String fallback) {
return name == null || name.trim().length() == 0
? UIManager.getInstance().localize(fallback, fallback) : name;
}

private static MapBounds addStopMarker(MapSurface map, LatLng position,
String label, MapBounds bounds) {
if (position == null) {
return bounds;
}
map.addMarker(new MarkerOptions(position).title(label).label(label));
return bounds == null ? new MapBounds(position, position) : bounds.extend(position);
}

}
Loading
Loading