
com.tomtom.speedtools.tilemap.TileMap Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of geo Show documentation
Show all versions of geo Show documentation
Contains a set of geometry (and geographical) classes, which work with any latitude and
longitude, without glitches around the poles or Fiji.
/*
* Copyright (C) 2012-2017. TomTom International BV (http://tomtom.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tomtom.speedtools.tilemap;
import com.tomtom.speedtools.geometry.GeoPoint;
import com.tomtom.speedtools.geometry.GeoRectangle;
import com.tomtom.speedtools.objects.Tuple;
import com.tomtom.speedtools.utils.MathUtils;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
/**
* This class represents an (abstract) cacheable map. The type parameter T is the image tile type. It is parametrized to
* allow retrieving tiles in different internal formats, such as AWT Image objects, or Processing PImage objects.
*/
public abstract class TileMap {
private final int bufferColumns; // Additional tile columns to left and right of viewport, for buffering.
private final int bufferRows; // Additional tile rows on top and bottom of viewport, for buffering.
/**
* Pre-caching means fetching tiles that might be useful later. This may speed up or slow down the application,
* depending on network performance.
*/
private boolean preCaching;
protected TileMap() {
this(0, 0);
}
protected TileMap(final int bufferColumns, final int bufferRows) {
assert bufferColumns >= 0;
assert bufferRows >= 0;
this.bufferColumns = bufferColumns;
this.bufferRows = bufferRows;
this.preCaching = false;
}
/**
* Get a map tile, given a map tile key. This function is implemented by the user of the tile.
*
* @param key Tile key.
* @return Tile, or null if missing.
*/
@Nullable
public abstract T getTile(@Nonnull final TileKey key);
/**
* Returns if the map supports caching.
*
* @return True if the map supports caching (does not say anything about the cache size).
*/
public boolean isCacheEnabled() {
return false;
}
/**
* Set pre-caching on or off, if possible. Pre-caching means fetching tiles that might be useful later. This may
* speed up or slow down the application, depending on network performance.
*
* Pre-caching can only be set if caching is enabled. Setting the value to true does not always mean pre-caching
* will be enabled. The cache itself must support it.
*
* @param preCaching True of pre-caching is enabled.
*/
public void setPreCachingHint(final boolean preCaching) {
this.preCaching = isCacheEnabled() && preCaching;
}
/**
* Returns if the map supports pre-caching of additional non-visible tiles.
*
* @return True if the map supports pre-caching (does not say anything about the cache size).
*/
public boolean isPreCacheEnabled() {
return preCaching;
}
/**
* Buffer a map tile which is not shown in the viewport, given a map tile key. This function can be used to preload
* image tiles for caching maps. Note that this function ALWAYS gets called, so only buffer the image if you do not
* already have it.
*
* @param key Tile key.
*/
public void cacheTile(@Nonnull final TileKey key) {
assert key != null;
// Empty.
}
/**
* Callback function for viewport tile processing.
*
* The template parameter T is the image class, e.g. Image or PImage.
*/
public interface ViewportTileProcessor {
/**
* Process one viewport tile.
*
* @param seqX Sequence number in X direction (0, 1, 2, ...), 0 = left.
* @param seqY Sequence number in Y direction (0, 1, 2, ...), 0 = top.
* @param tileKey Tile key.
* @param viewportX Position X from top-left of viewport.
* @param viewportY Position Y from top-left of viewport.
* @param img Tile image.
* @param tileOffsetX Crop area, position X from top-left of image.
* @param tileOffsetY Crop area, position Y from top-left of image.
* @param width Width of crop area to plot.
* @param height Height of crop area to plot.
*/
void process(
final int seqX, final int seqY,
@Nonnull final TileKey tileKey,
@Nullable final T img,
final int viewportX, final int viewportY,
final int tileOffsetX, final int tileOffsetY, final int width, final int height);
}
/**
* Process all tiles for a specific viewport. For every tile a callback function 'process()' is called. This
* function can either immediately draw the image, or for example store it in a collection.
*
* @param widthPixels Width of viewport in pixels.
* @param heightPixels Height of viewport in pixels.
* @param mapCenter Center of map, as lat/lon.
* @param zoomLevel Zoomlevel (from 0..LbsConst.MapConst.MAXIMUM_ZOOM).
* @param processor Viewport processor.
*/
@SuppressWarnings("ConstantConditions")
public void processViewportTiles(
final int widthPixels, final int heightPixels,
@Nonnull final GeoPoint mapCenter, final int zoomLevel,
@Nonnull final ViewportTileProcessor processor) {
assert widthPixels >= 0;
assert heightPixels >= 0;
assert mapCenter != null;
assert processor != null;
assert MathUtils.isBetween(zoomLevel, 0, MapConst.MAXIMUM_ZOOM) : zoomLevel;
assert MapConst.PIXELS_PER_TILE > 0;
assert processor != null;
// Calculate total number of tiles on this zoomlevel.
final long nrTiles = (1L << zoomLevel);
// Determine how many tiles top-left tile should shift to center the map.
int shiftTileIndexX = widthPixels / MapConst.PIXELS_PER_TILE / 2;
int shiftTileIndexY = heightPixels / MapConst.PIXELS_PER_TILE / 2;
// Determine offset within tile when centering tiles.
final int centerPixelX = widthPixels / 2;
final int centerPixelY = heightPixels / 2;
final int offsetCenterPixelX = centerPixelX - (shiftTileIndexX * MapConst.PIXELS_PER_TILE);
final int offsetCenterPixelY = centerPixelY - (shiftTileIndexY * MapConst.PIXELS_PER_TILE);
assert MathUtils.isBetween(offsetCenterPixelX, 0, MapConst.PIXELS_PER_TILE) : offsetCenterPixelX;
assert MathUtils.isBetween(offsetCenterPixelY, 0, MapConst.PIXELS_PER_TILE) : offsetCenterPixelY;
// Determine top-left tile.
final TileOffset centerTile = convertLatLonToTileOffset(mapCenter, zoomLevel);
long tileIndexX = (((centerTile.getKey().getTileX() - shiftTileIndexX) + nrTiles) % nrTiles);
long tileIndexY = (((centerTile.getKey().getTileY() - shiftTileIndexY) + nrTiles) % nrTiles);
assert (0 <= tileIndexX) && (tileIndexX < nrTiles) : tileIndexX;
assert (0 <= tileIndexY) && (tileIndexY < nrTiles) : tileIndexY;
// Offset within tile may require an additional tile shift.
final int offsetTilePixelX;
final int offsetTilePixelY;
if (centerTile.getOffsetX() <= offsetCenterPixelX) {
offsetTilePixelX = (MapConst.PIXELS_PER_TILE - 1) - (offsetCenterPixelX - centerTile.getOffsetX());
tileIndexX = ((tileIndexX + nrTiles) - 1) % nrTiles;
} else {
offsetTilePixelX = centerTile.getOffsetX() - offsetCenterPixelX;
}
assert MathUtils.isBetween(offsetTilePixelX, 0, MapConst.PIXELS_PER_TILE) : offsetTilePixelX;
if (centerTile.getOffsetY() <= offsetCenterPixelY) {
offsetTilePixelY = (MapConst.PIXELS_PER_TILE - 1) - (offsetCenterPixelY - centerTile.getOffsetY());
tileIndexY = ((tileIndexY + nrTiles) - 1) % nrTiles;
} else {
offsetTilePixelY = centerTile.getOffsetY() - offsetCenterPixelY;
}
assert MathUtils.isBetween(offsetTilePixelY, 0, MapConst.PIXELS_PER_TILE) : offsetTilePixelY;
// Constrain tile numbers for extreme coordinates.
final long startTileIndexX = tileIndexX;
final long startTileIndexY = tileIndexY;
// Create top-left tile object.
final TileKey topLeftKey = new TileKey(tileIndexX, tileIndexY, zoomLevel);
final TileOffset topLeft = new TileOffset(topLeftKey, offsetTilePixelX, offsetTilePixelY);
// Set colors for grid and draw map.
tileIndexY = startTileIndexY % nrTiles;
int seqIndexY = 0;
int viewportPixelX = 0;
int viewportPixelY = 0;
int tileOffsetPixelY = topLeft.getOffsetY();
while (viewportPixelY < heightPixels) {
tileIndexX = startTileIndexX % nrTiles;
int seqIndexX = 0;
int tileOffsetPixelX = topLeft.getOffsetX();
final int tilePixelHeight = Math.min(MapConst.PIXELS_PER_TILE, heightPixels - viewportPixelY) - tileOffsetPixelY;
while (viewportPixelX < widthPixels) {
final int tilePixelWidth = Math.min(MapConst.PIXELS_PER_TILE, widthPixels - viewportPixelX) - tileOffsetPixelX;
// Create tile key.
final TileKey tileKey = new TileKey(tileIndexX, tileIndexY, zoomLevel);
// Get tile from cache (or load it now).
final T img = getTile(tileKey);
// Call "process()" method for tile.
processor.process(seqIndexX, seqIndexY, tileKey, img, viewportPixelX, viewportPixelY,
tileOffsetPixelX, tileOffsetPixelY, tilePixelWidth, tilePixelHeight);
tileOffsetPixelX = 0;
viewportPixelX = viewportPixelX + tilePixelWidth;
++seqIndexX;
tileIndexX = (tileIndexX + 1) % nrTiles;
}
tileOffsetPixelY = 0;
viewportPixelX = 0;
viewportPixelY = viewportPixelY + tilePixelHeight;
++seqIndexY;
tileIndexY = (tileIndexY + 1) % nrTiles;
}
if (preCaching) {
final long bufferFromIndexX = Math.max(0, startTileIndexX - bufferColumns);
final long bufferToIndexX = Math.min(nrTiles, (tileIndexX + bufferColumns) - 1);
final long bufferFromIndexY = Math.max(0, startTileIndexY - bufferRows);
final long bufferToIndexY = Math.min(nrTiles, (tileIndexY + bufferRows) - 1);
// Top.
for (long y = bufferFromIndexY; y < startTileIndexY; ++y) {
for (long x = bufferFromIndexX; x <= bufferToIndexX; ++x) {
final TileKey key = new TileKey(x % nrTiles, y % nrTiles, zoomLevel);
cacheTile(key);
}
}
// Left/right.
for (long y = startTileIndexY; y < tileIndexY; ++y) {
for (long x = bufferFromIndexX; x < startTileIndexX; ++x) {
final TileKey key = new TileKey(x % nrTiles, y % nrTiles, zoomLevel);
cacheTile(key);
}
for (long x = tileIndexX; x <= bufferToIndexX; ++x) {
final TileKey key = new TileKey(x % nrTiles, y % nrTiles, zoomLevel);
cacheTile(key);
}
}
// Bottom.
for (long y = tileIndexY; y <= bufferToIndexY; ++y) {
for (long x = bufferFromIndexX; x <= bufferToIndexX; ++x) {
final TileKey key = new TileKey(x % nrTiles, y % nrTiles, zoomLevel);
cacheTile(key);
}
}
}
}
/**
* Get all tiles for a specific viewport.
*
* @param widthPixels Width of viewport in pixels.
* @param heightPixels Height of viewport in pixels.
* @param mapCenter Center of map, as lat/lon.
* @param zoomLevel Zoomlevel (from 0..LbsConst.MapConst.MAXIMUM_ZOOM).
* @return The returned value is a collection of viewport images. Every viewport image element is contains an image
* to be plotted, the position from the top-left of the viewport and the crop area to plot from the image. This
* makes it easy to crop the images at the edges correctly.
*/
@Nonnull
public Collection> collectViewportTiles(
final int widthPixels, final int heightPixels,
@Nonnull final GeoPoint mapCenter, final int zoomLevel) {
assert widthPixels >= 0;
assert heightPixels >= 0;
assert mapCenter != null;
assert MathUtils.isBetween(zoomLevel, 0, MapConst.MAXIMUM_ZOOM) : zoomLevel;
assert MapConst.PIXELS_PER_TILE > 0;
final Collection> tiles = new ArrayList<>();
processViewportTiles(widthPixels, heightPixels, mapCenter, zoomLevel, new ViewportTileProcessor() {
@Override
public void process(
final int seqX, final int seqY, @Nonnull final TileKey tileKey, @Nullable final T img,
final int viewportX, final int viewportY,
final int tileOffsetX, final int tileOffsetY, final int width, final int height) {
final ViewportTile viewportTile =
new ViewportTile<>(
seqX, seqY, tileKey, img,
viewportX, viewportY, tileOffsetX, tileOffsetY, width, height);
tiles.add(viewportTile);
}
});
final int sizeMin = (widthPixels / MapConst.PIXELS_PER_TILE) * ((heightPixels / MapConst.PIXELS_PER_TILE));
final int sizeMax = ((widthPixels / MapConst.PIXELS_PER_TILE) + 2) * ((heightPixels / MapConst.PIXELS_PER_TILE) + 2);
assert MathUtils.isBetween(tiles.size(), sizeMin, sizeMax) :
tiles.size() + " not in [" + sizeMin + ", " + sizeMax + ']';
return tiles;
}
/**
* Convert a lat/lon coordinate to a map tile with an offset within the tile.
*
* @param point Coordinate.
* @param zoomLevel Zoom level.
* @return Map tile with zoomlevel and offset.
*/
@SuppressWarnings("NumericCastThatLosesPrecision")
@Nonnull
public static TileOffset convertLatLonToTileOffset(@Nonnull final GeoPoint point, final int zoomLevel) {
// Normalize lat/lon to 0..1.
final MercatorPoint mercs = MercatorPoint.latLonToMercs(point);
assert mercs != null;
assert MathUtils.isBetween(mercs.getMercX(), 0.0, 1.0) : mercs.getMercX();
assert MathUtils.isBetween(mercs.getMercY(), 0.0, 1.0) : mercs.getMercY();
// Maximum number of tiles on this zoom level (same for X and Y).
final double nrTiles = (1L << zoomLevel);
// Determine tile X and Y.
final long tileX = Math.min((long) (nrTiles - 1), (long) Math.floor(mercs.getMercX() * nrTiles));
final long tileY = Math.min((long) (nrTiles - 1), (long) Math.floor(mercs.getMercY() * nrTiles));
final TileKey key = new TileKey(tileX, tileY, zoomLevel);
final double deltaMercX = mercs.getMercX() - ((double) tileX / nrTiles);
final double deltaMercY = mercs.getMercY() - ((double) tileY / nrTiles);
final long nrPixels = Math.round(nrTiles * MapConst.PIXELS_PER_TILE);
final int offsetX = (int) Math.min(Math.round(deltaMercX * nrPixels), MapConst.PIXELS_PER_TILE);
final int offsetY = (int) Math.min(Math.round(deltaMercY * nrPixels), MapConst.PIXELS_PER_TILE);
assert MathUtils.isBetween(offsetX, 0, MapConst.PIXELS_PER_TILE) : offsetX;
assert MathUtils.isBetween(offsetY, 0, MapConst.PIXELS_PER_TILE) : offsetY;
return new TileOffset(key, offsetX, offsetY);
}
/**
* Convert a map tile with zoomlevel and offset to a lat/lon coordinate.
*
* @param tileOffset Map tile with zoomlevel and offset within tile.
* @return Coordinate.
*/
@Nonnull
public static GeoPoint convertTileOffsetToLatLon(@Nonnull final TileOffset tileOffset) {
assert tileOffset != null;
final int offsetX = tileOffset.getOffsetX();
final int offsetY = tileOffset.getOffsetY();
final int zoomLevel = tileOffset.getKey().getZoomLevel();
final double nrTiles = (1L << zoomLevel) * MapConst.PIXELS_PER_TILE;
final long tileX = tileOffset.getKey().getTileX() * MapConst.PIXELS_PER_TILE;
final long tileY = tileOffset.getKey().getTileY() * MapConst.PIXELS_PER_TILE;
final double mercX = (tileX + offsetX) / nrTiles;
final double mercY = (tileY + offsetY) / nrTiles;
final GeoPoint point = MercatorPoint.mercsToLatLon(mercX, mercY);
return point;
}
/**
* Convert a (X, Y) position on a screen to a latitude and longitude, given the center of the map and the
* zoomLevel.
*
* @param posX Offset in pixels from left.
* @param posY Offset in pixels from top.
* @param width Width in pixels.
* @param height Height in pixels.
* @param zoomLevel Zoom level.
* @param mapCenter Latitude and longitude of center of map.
* @return Latitude and longitude of XY position.
*/
@Nonnull
public static GeoPoint convertViewportXYToLatLon(final int posX, final int posY,
final int width, final int height, final int zoomLevel,
@Nonnull final GeoPoint mapCenter) {
assert (0 <= posX) && (posX < width);
assert (0 <= posY) && (posY < height);
assert width > 0;
assert height > 0;
assert MathUtils.isBetween(zoomLevel, MapConst.MINIMUM_ZOOM, MapConst.MAXIMUM_ZOOM) : zoomLevel;
assert mapCenter != null;
final double deltaX = posX - ((double) width / 2.0);
final double deltaY = posY - ((double) height / 2.0);
final double totalSize = (1L << zoomLevel) * MapConst.PIXELS_PER_TILE;
final MercatorPoint mercs = MercatorPoint.latLonToMercs(mapCenter);
final double mercX = MathUtils.limitTo(mercs.getMercX() + (deltaX / totalSize), 0.0, 1.0);
final double mercY = MathUtils.limitTo(mercs.getMercY() + (deltaY / totalSize), 0.0, 1.0);
final GeoPoint point = MercatorPoint.mercsToLatLon(mercX, mercY);
return point;
}
/**
* Given a viewport calculate the (x, y) position of a lat/lon. If the lat or the lon is not displayed in the
* viewport, null is returned for that value.
*
* @param point Lat/lon.
* @param width Width of viewport.
* @param height Height of viewport.
* @param zoomLevel Zoomlevel of viewport.
* @param mapCenter Latitude and longitude of center of map.
* @return Returns (x, y) position in viewport. 0 <= x < width and 0 <= y < height, or null if either of the values
* is out of range, if the lat or lon is not positioned within the viewport.
*/
@SuppressWarnings("NumericCastThatLosesPrecision")
@Nullable
public static Tuple convertLatLonToViewportXY(
@Nonnull final GeoPoint point,
final int width, final int height, final int zoomLevel,
@Nonnull final GeoPoint mapCenter) {
assert point != null;
assert mapCenter != null;
assert width > 0;
assert height > 0;
assert MathUtils.isBetween(zoomLevel, MapConst.MINIMUM_ZOOM, MapConst.MAXIMUM_ZOOM) : zoomLevel;
final double totalSize = (1L << zoomLevel) * MapConst.PIXELS_PER_TILE;
final MercatorPoint mercsCenter = MercatorPoint.latLonToMercs(mapCenter);
final MercatorPoint mercsPoint = MercatorPoint.latLonToMercs(point);
final double deltaX = mercsCenter.getMercX() - mercsPoint.getMercX();
final double deltaY = mercsCenter.getMercY() - mercsPoint.getMercY();
final double centerX = width / 2.0;
final double centerY = height / 2.0;
final double newX = centerX - (deltaX * totalSize);
final double newY = centerY - (deltaY * totalSize);
// Calculate position within viewport.
final Integer x;
if (MathUtils.isBetween(newX, 0.0, width)) {
x = (int) Math.floor(newX);
} else {
x = null;
}
final Integer y;
if (MathUtils.isBetween(newY, 0.0, height)) {
y = (int) Math.floor(newY);
} else {
y = null;
}
if ((x != null) && (y != null)) {
return new Tuple<>(x, y);
} else {
return null;
}
}
/**
* Given a set of points, a viewport width/height and the border width, return a zoomlevel and map center that will
* contain the specific viewport.
*
* @param width Width of viewport.
* @param height Height of viewport.
* @param borderWidth Border width (in pixels) inside viewport to keep points clear from.
* @param points Points to fit in viewport (but not in border).
* @return Zoom level and map center of map to display.
*/
@Nonnull
public static Tuple findZoomLevelAndMapCenter(
final int width, final int height, final int borderWidth,
@Nonnull final Collection points) {
assert (width - (2 * borderWidth)) > 0;
assert (height - (2 * borderWidth)) > 0;
assert points != null;
assert !points.isEmpty();
final int actualWidth = width - (2 * borderWidth);
final int actualHeight = height - (2 * borderWidth);
// Determine full rectangle of all points.
final GeoPoint first = points.iterator().next();
assert first != null;
GeoRectangle rect = new GeoRectangle(first, first);
for (final GeoPoint point : points) {
assert point != null;
rect = rect.grow(point);
}
final GeoPoint northEast = rect.getNorthEast();
final GeoPoint southWest = rect.getSouthWest();
// Search for a zoom-level until both northEast and southWest fit.
boolean found = false;
final GeoPoint mapCenter = rect.getCenter();
int zoomLevel = MapConst.MAXIMUM_ZOOM + 1;
while ((zoomLevel > MapConst.MINIMUM_ZOOM) && !found) {
--zoomLevel;
final Tuple topLeft =
convertLatLonToViewportXY(northEast, actualWidth, actualHeight, zoomLevel, mapCenter);
final Tuple bottomRight =
convertLatLonToViewportXY(southWest, actualWidth, actualHeight, zoomLevel, mapCenter);
found = (topLeft != null) && (bottomRight != null);
}
return new Tuple<>(zoomLevel, mapCenter);
}
/**
* Calculate a new map center, given a delta in pixels.
*
* @param deltaX Delta in pixels to left.
* @param deltaY Delta in pixels to bottom.
* @param zoomLevel Zoom level.
* @param mapCenter Latitude and longitude of center of map.
* @return Latitude and longitude of XY position.
*/
@Nonnull
public static GeoPoint moveLatLonByViewportXY(
final int deltaX, final int deltaY, final int zoomLevel,
@Nonnull final GeoPoint mapCenter) {
assert mapCenter != null;
assert MathUtils.isBetween(zoomLevel, MapConst.MINIMUM_ZOOM, MapConst.MAXIMUM_ZOOM) : zoomLevel;
final double totalSize = (1L << zoomLevel) * MapConst.PIXELS_PER_TILE;
final MercatorPoint mercs = MercatorPoint.latLonToMercs(mapCenter);
final double mercX = MathUtils.limitTo(mercs.getMercX() + (deltaX / totalSize), 0.0, 1.0);
final double mercY = MathUtils.limitTo(mercs.getMercY() + (deltaY / totalSize), 0.0, 1.0);
final GeoPoint point = MercatorPoint.mercsToLatLon(mercX, mercY);
return point;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy