Skip to content
Open
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
2 changes: 1 addition & 1 deletion CodenameOne/src/com/codename1/maps/MapView.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public void run() {
/// screen it must cover proportionally more physical pixels (the standard
/// slippy-map convention: mdpi = 1, hdpi = 1.5, xhdpi = 2, xxhdpi = 3, ...)
/// otherwise the viewport spans many tiles and the map shows too much area.
private static double devicePixelRatio() {
static double devicePixelRatio() {
switch (Display.getInstance().getDeviceDensity()) {
case Display.DENSITY_HIGH:
return 1.5;
Expand Down
70 changes: 67 additions & 3 deletions CodenameOne/src/com/codename1/maps/NativeMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import com.codename1.maps.spi.MapProviderRegistry;
import com.codename1.maps.vector.MapStyle;
import com.codename1.maps.vector.TileSource;
import com.codename1.maps.vector.WebMercator;
import com.codename1.util.MathUtil;
import com.codename1.ui.CN;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
Expand Down Expand Up @@ -335,9 +337,71 @@ public void fitBounds(MapBounds bounds, int paddingPixels) {
fallback.fitBounds(bounds, paddingPixels);
return;
}
// Native providers center on the bounds; precise fit is provider work.
provider.setCamera(mapId, bounds.getCenter().getLatitude(),
bounds.getCenter().getLongitude(), provider.getZoom(mapId), 0, 0);
if (bounds == null) {
return;
}
double zoom = zoomToFit(bounds, getWidth(), getHeight(), paddingPixels,
MapView.devicePixelRatio());
if (Double.isNaN(zoom)) {
// Nothing to fit to yet (no layout, or a single point): keep the
// zoom and just recenter.
zoom = provider.getZoom(mapId);
} else {
zoom = Math.max(provider.getMinZoom(mapId),
Math.min(provider.getMaxZoom(mapId), zoom));
}
// Center on the projected midpoint, not the arithmetic one: Mercator
// stretches away from the equator, so a tall band centered on the mean
// of its latitudes hangs off the top of the viewport the fit just
// sized for it.
provider.setCamera(mapId,
WebMercator.centerLatitude(bounds.getSouthWest().getLatitude(),
bounds.getNorthEast().getLatitude()),
bounds.getCenter().getLongitude(), (float) zoom, 0, 0);
}

/// The zoom at which `bounds` fills a `viewWidth` x `viewHeight` viewport
/// of device pixels, inset by `paddingPixels` on every edge, or
/// `Double.NaN` when there is nothing to fit to -- no layout yet, or bounds
/// with no extent.
///
/// `fitBounds` used to keep the current zoom and merely recenter, so a
/// region larger than the viewport stayed clipped and
/// [MapSurface#fitBounds] did not mean the same thing on a native map as
/// on a vector one. This solves for the zoom the way the vector engine
/// does, in the same Web Mercator units, so both surfaces frame a region
/// identically. Package visible so the arithmetic can be tested without a
/// native provider.
static double zoomToFit(MapBounds bounds, int viewWidth, int viewHeight,
int paddingPixels, double pixelRatio) {
if (bounds == null || viewWidth <= 0 || viewHeight <= 0 || pixelRatio <= 0) {
return Double.NaN;
}
// Native map SDKs use the slippy convention, where a zoom level spans
// 256 logical points; the component's size is in device pixels, so it
// converts through the same density ratio the vector engine uses.
double usableWidth = Math.max(1, viewWidth - 2 * paddingPixels) / pixelRatio;
double usableHeight = Math.max(1, viewHeight - 2 * paddingPixels) / pixelRatio;
Comment on lines +375 to +384
double worldWidth = Math.abs(
WebMercator.lonToWorldX(bounds.getNorthEast().getLongitude(), 0)
- WebMercator.lonToWorldX(bounds.getSouthWest().getLongitude(), 0));
// Clamped, or a bound touching a pole projects to infinity and the
// solved zoom collapses.
double worldHeight = Math.abs(
WebMercator.latToWorldY(
WebMercator.clampLatitude(bounds.getNorthEast().getLatitude()), 0)
- WebMercator.latToWorldY(
WebMercator.clampLatitude(bounds.getSouthWest().getLatitude()), 0));
if (worldWidth <= 0 && worldHeight <= 0) {
return Double.NaN;
}
double horizontal = worldWidth <= 0 ? Double.MAX_VALUE : log2(usableWidth / worldWidth);
double vertical = worldHeight <= 0 ? Double.MAX_VALUE : log2(usableHeight / worldHeight);
return Math.min(horizontal, vertical);
}

private static double log2(double v) {
return MathUtil.log(v) / MathUtil.log(2);
}

// ---- MapSurface: map objects -----------------------------------------
Expand Down
31 changes: 31 additions & 0 deletions CodenameOne/src/com/codename1/maps/Polyline.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@

/// A connected sequence of line segments drawn on a map. Add one through
/// [MapSurface#addPolyline(Polyline)].
///
/// A polyline joins the vertices you give it with *straight* segments; it
/// knows nothing about roads. To draw a route that follows the road network,
/// ask a routing service for the road geometry and draw that -- see
/// [com.codename1.maps.routing.Routing], or decode a geometry you fetched
/// yourself with [#fromEncoded(String)].
public final class Polyline extends MapObject {

private final List points;
Expand All @@ -50,6 +56,31 @@ public Polyline(LatLng[] pts) {
}
}

/// Creates a polyline from an *encoded polyline* geometry at
/// [PolylineCodec]'s default precision -- the shape virtually every
/// directions API returns for a route.
public static Polyline fromEncoded(String encoded) {
return through(PolylineCodec.decode(encoded));
}
Comment thread
shai-almog marked this conversation as resolved.
Comment thread
shai-almog marked this conversation as resolved.

/// Creates a polyline from an *encoded polyline* geometry at an explicit
/// decimal precision (5 for the classic format, 6 for `polyline6`).
///
/// Throws `IllegalArgumentException` when `precision` falls outside 1 to
/// 10; a precision that cannot scale coordinates sensibly would otherwise
/// decode into a line drawn in the wrong place. See [PolylineCodec].
public static Polyline fromEncoded(String encoded, int precision) {
return through(PolylineCodec.decode(encoded, precision));
}

/// Wraps already-decoded vertices, so neither factory has to restate the
/// codec's default precision.
private static Polyline through(List decoded) {
Polyline pl = new Polyline();
pl.points.addAll(decoded);
return pl;
}
Comment thread
shai-almog marked this conversation as resolved.

/// Appends a vertex.
public Polyline addPoint(LatLng point) {
points.add(point);
Expand Down
243 changes: 243 additions & 0 deletions CodenameOne/src/com/codename1/maps/PolylineCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Codename One in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.maps;

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

/// Reads and writes the *encoded polyline* format -- the compact ASCII
/// representation of a coordinate sequence that virtually every directions
/// and routing service uses for route geometry (Google Directions, OSRM,
/// Mapbox Directions, GraphHopper, Valhalla, OpenRouteService).
///
/// Use it to turn a route geometry straight into something drawable:
///
/// ```java
/// map.addPolyline(Polyline.fromEncoded(geometryFromMyDirectionsApi));
/// ```
///
/// The encoding stores each coordinate as a zig-zag, base-64-ish delta from
/// the previous one at a fixed decimal `precision`. Precision 5 is the
/// original Google format and the OSRM/Google/GraphHopper default; precision
/// 6 (`polyline6`) is what Valhalla and OSRM's `geometries=polyline6` emit.
/// Decoding with the wrong precision yields coordinates off by a factor of
/// ten, so pass the precision your service documents.
public final class PolylineCodec {

private static final int DEFAULT_PRECISION = 5;
/// The smallest decimal precision that still scales coordinates at all.
private static final int MIN_PRECISION = 1;
/// Comfortably past the precision 5/6/7 that real services emit, while
/// keeping the scale well inside the range a `double` holds exactly, so
/// no accepted precision can round a coordinate wrongly.
private static final int MAX_PRECISION = 10;
Comment thread
shai-almog marked this conversation as resolved.
private static final int CHUNK_BITS = 5;
private static final int CHUNK_MASK = 0x1f;
private static final int CONTINUATION_BIT = 0x20;
/// Largest legal chunk: five data bits plus the continuation bit.
private static final int MAX_CHUNK = 0x3f;
private static final int ASCII_OFFSET = 63;

private PolylineCodec() {
}

/// Decodes an encoded polyline at the default precision of 5.
///
/// #### Parameters
///
/// - `encoded`: the encoded geometry, may be `null`
///
/// #### Returns
///
/// the decoded [LatLng] vertices, empty when `encoded` is `null` or blank
public static List decode(String encoded) {
return decode(encoded, DEFAULT_PRECISION);
}

/// Decodes an encoded polyline at an explicit decimal precision (5 for the
/// classic format, 6 for `polyline6`).
///
/// Trailing bytes that do not form a complete coordinate pair -- the usual
/// symptom of a truncated response -- are dropped rather than throwing, so
/// a partial geometry still draws the coordinates it did carry. A value cut
/// in half is discarded too: half a delta is not a coordinate, and emitting
/// it would put a spurious vertex on the map and skew the route bounds.
/// Decoding likewise stops at the first character outside the encoding's
/// alphabet instead of folding it into a coordinate.
///
/// #### Parameters
///
/// - `encoded`: the encoded geometry, may be `null`
///
/// - `precision`: the number of decimal digits the encoder scaled by,
/// between 1 and 10
///
/// #### Returns
///
/// the decoded [LatLng] vertices, never `null`
///
/// #### Throws
///
/// - `IllegalArgumentException`: when `precision` is outside 1 to 10, which
/// would scale every coordinate into nonsense rather than fail
public static List decode(String encoded, int precision) {
double factor = factor(precision);
List points = new ArrayList();
if (encoded == null || encoded.length() == 0) {
return points;
}
int[] cursor = new int[1];
long[] value = new long[1];
long lat = 0;
long lng = 0;
int len = encoded.length();
while (cursor[0] < len) {
if (!readValue(encoded, cursor, value)) {
break;
}
long dLat = value[0];
if (!readValue(encoded, cursor, value)) {
// The longitude is missing or was cut in half: emitting the
// partial value would place a bogus vertex on the map.
break;
}
lat += dLat;
lng += value[0];
points.add(new LatLng(lat / factor, lng / factor));
}
return points;
}

/// Encodes [LatLng] vertices at the default precision of 5.
///
/// #### Parameters
///
/// - `points`: the vertices to encode, may be `null`
///
/// #### Returns
///
/// the encoded geometry, empty for a `null` or empty list
public static String encode(List points) {
return encode(points, DEFAULT_PRECISION);
}

/// Encodes [LatLng] vertices at an explicit decimal precision.
///
/// #### Parameters
///
/// - `points`: the vertices to encode, may be `null`
///
/// - `precision`: the number of decimal digits to scale by, between 1 and 10
///
/// #### Returns
///
/// the encoded geometry, never `null`
///
/// #### Throws
///
/// - `IllegalArgumentException`: when `precision` is outside 1 to 10
public static String encode(List points, int precision) {
double factor = factor(precision);
StringBuilder sb = new StringBuilder();
if (points == null) {
return sb.toString();
}
long prevLat = 0;
long prevLng = 0;
for (Object pointObj : points) {
LatLng p = (LatLng) pointObj;
long lat = Math.round(p.getLatitude() * factor);
long lng = Math.round(p.getLongitude() * factor);
writeValue(sb, lat - prevLat);
writeValue(sb, lng - prevLng);
prevLat = lat;
prevLng = lng;
}
return sb.toString();
}

/// The scale a `precision` of decimal digits multiplies by.
///
/// Out-of-range values are rejected rather than quietly producing garbage:
/// a precision of 0 or less leaves the scale at 1 and inflates every
/// coordinate by five orders of magnitude, and a very large one overflows
/// the scale to infinity and collapses every coordinate to zero. Both
/// decode without complaint into a map full of wrong places.
private static double factor(int precision) {
if (precision < MIN_PRECISION || precision > MAX_PRECISION) {
throw new IllegalArgumentException("precision must be between " + MIN_PRECISION
+ " and " + MAX_PRECISION + ", was " + precision);
}
double f = 1;
for (int i = 0; i < precision; i++) {
f *= 10;
}
return f;
}

/// Reads one zig-zag encoded delta into `out[0]`, advancing `cursor[0]`
/// past it.
///
/// Returns false when the value did not end cleanly -- the string ran out
/// before its terminating chunk, or a character fell outside the encoding's
/// alphabet. The accumulated bits are a fraction of the real delta in that
/// case, so the caller must discard them rather than treat them as a
/// coordinate.
private static boolean readValue(String encoded, int[] cursor, long[] out) {
int len = encoded.length();
long result = 0;
int shift = 0;
boolean terminated = false;
while (cursor[0] < len) {
int b = encoded.charAt(cursor[0]++) - ASCII_OFFSET;
if (b < 0 || b > MAX_CHUNK) {
// Outside the '?'..'~' alphabet the encoding uses. A character
// below the offset goes negative, and negative reads as a
// terminating chunk, so without this check junk in the middle
// of a geometry would end the value early and emit garbage.
out[0] = 0;
return false;
}
result |= ((long) (b & CHUNK_MASK)) << shift;
shift += CHUNK_BITS;
if (b < CONTINUATION_BIT) {
terminated = true;
break;
}
if (shift >= 64) {
break;
}
}
out[0] = (result & 1) != 0 ? ~(result >>> 1) : result >>> 1;
Comment thread
shai-almog marked this conversation as resolved.
return terminated;
}

private static void writeValue(StringBuilder sb, long value) {
long v = value < 0 ? ~(value << 1) : value << 1;
while (v >= CONTINUATION_BIT) {
sb.append((char) ((CONTINUATION_BIT | (int) (v & CHUNK_MASK)) + ASCII_OFFSET));
v >>= CHUNK_BITS;
}
sb.append((char) (v + ASCII_OFFSET));
}
}
Loading
Loading