#include "MapProjection.h" #include #include #include namespace Pyxis { namespace MapProjection { const double WEB_MERCATOR_MAX_LATITUDE = 85.05112878; namespace { const double PI = 3.141592653589793238462643383279502884; const double MAX_RAW_TILE_COORDINATE = 1000000000000.0; bool finite(double value) { return std::isfinite(value) != 0; } double worldPixels(std::uint32_t zoom) { return static_cast(tileCount(zoom)) * static_cast(TILE_SIZE); } double wrap(double value, double period) { double result = std::fmod(value, period); if (result < 0.0) { result += period; } // Guard against a possible rounded period after arithmetic at the edge. return result >= period ? 0.0 : result; } double shortestWrappedDelta(double delta, double period) { return wrap(delta + (period * 0.5), period) - (period * 0.5); } std::uint32_t wrapTileX(std::int64_t raw, std::uint32_t tiles) { const std::int64_t period = static_cast(tiles); std::int64_t result = raw % period; if (result < 0) { result += period; } return static_cast(result); } std::uint32_t clampTileY(std::int64_t raw, std::uint32_t tiles) { if (raw < 0) { return 0U; } const std::int64_t last = static_cast(tiles) - 1; if (raw > last) { return tiles - 1U; } return static_cast(raw); } bool validViewport(const Viewport& viewport) { return finite(viewport.left) && finite(viewport.top) && (viewport.width != 0U) && (viewport.height != 0U); } } // namespace double clampLatitude(double latitude) { if (latitude > WEB_MERCATOR_MAX_LATITUDE) { return WEB_MERCATOR_MAX_LATITUDE; } if (latitude < -WEB_MERCATOR_MAX_LATITUDE) { return -WEB_MERCATOR_MAX_LATITUDE; } return latitude; } double normalizeLongitude(double longitude) { if (!finite(longitude)) { return longitude; } double result = std::fmod(longitude, 360.0); if (result >= 180.0) { result -= 360.0; } else if (result < -180.0) { result += 360.0; } return result; } bool isValidZoom(std::uint32_t zoom) { return zoom <= static_cast(MAX_ZOOM); } std::uint32_t tileCount(std::uint32_t zoom) { if (!isValidZoom(zoom)) { return 0U; } std::uint32_t count = 1U; for (std::uint32_t level = 0U; level < zoom; ++level) { count *= 2U; } return count; } Status latLonToGlobalPixel(const GeoPoint& point, std::uint32_t zoom, GlobalPixel& output) { if (!isValidZoom(zoom)) { return Status::INVALID_ZOOM; } if (!finite(point.latitude) || !finite(point.longitude)) { return Status::INVALID_ARGUMENT; } const double world = worldPixels(zoom); const double latitude = clampLatitude(point.latitude); const double longitude = normalizeLongitude(point.longitude); const double radians = latitude * PI / 180.0; const double mercator = std::log(std::tan((PI * 0.25) + (radians * 0.5))); GlobalPixel result; result.x = ((longitude + 180.0) / 360.0) * world; result.y = (0.5 - (mercator / (2.0 * PI))) * world; if (result.y < 0.0) { result.y = 0.0; } else if (result.y > world) { result.y = world; } output = result; return Status::OK; } Status globalPixelToLatLon(const GlobalPixel& pixel, std::uint32_t zoom, GeoPoint& output) { if (!isValidZoom(zoom)) { return Status::INVALID_ZOOM; } if (!finite(pixel.x) || !finite(pixel.y)) { return Status::INVALID_ARGUMENT; } const double world = worldPixels(zoom); double y = pixel.y; if (y < 0.0) { y = 0.0; } else if (y > world) { y = world; } const double normalized_y = 0.5 - (y / world); GeoPoint result; result.latitude = (180.0 / PI) * std::atan(std::sinh(2.0 * PI * normalized_y)); result.latitude = clampLatitude(result.latitude); result.longitude = normalizeLongitude((wrap(pixel.x, world) / world * 360.0) - 180.0); output = result; return Status::OK; } Status globalPixelToTile(const GlobalPixel& pixel, std::uint32_t zoom, TileIndex& output) { if (!isValidZoom(zoom)) { return Status::INVALID_ZOOM; } if (!finite(pixel.x) || !finite(pixel.y)) { return Status::INVALID_ARGUMENT; } const std::uint32_t tiles = tileCount(zoom); const double world = worldPixels(zoom); const double wrapped_x = wrap(pixel.x, world); double clamped_y = pixel.y; if (clamped_y < 0.0) { clamped_y = 0.0; } else if (clamped_y >= world) { clamped_y = std::nextafter(world, 0.0); } TileIndex result; result.x = static_cast(std::floor(wrapped_x / static_cast(TILE_SIZE))); result.y = static_cast(std::floor(clamped_y / static_cast(TILE_SIZE))); result.zoom = zoom; if (result.x >= tiles) { result.x = 0U; } if (result.y >= tiles) { result.y = tiles - 1U; } output = result; return Status::OK; } Status viewportTiles(const Viewport& viewport, std::uint32_t zoom, bool include_border, TilePlacement* output, std::size_t capacity, std::size_t& count) { count = 0U; if (!isValidZoom(zoom)) { return Status::INVALID_ZOOM; } if (!validViewport(viewport) || ((output == NULL) && (capacity != 0U))) { return Status::INVALID_ARGUMENT; } const double right = viewport.left + static_cast(viewport.width); const double bottom = viewport.top + static_cast(viewport.height); const double tile_size = static_cast(TILE_SIZE); if (!finite(right) || !finite(bottom)) { return Status::INVALID_ARGUMENT; } const double world = worldPixels(zoom); const double clipped_top = viewport.top < 0.0 ? 0.0 : viewport.top; const double clipped_bottom = bottom > world ? world : bottom; if (clipped_top >= clipped_bottom) { return Status::OK; } double first_x_value = std::floor(viewport.left / tile_size); double last_x_value = std::ceil(right / tile_size) - 1.0; double first_y_value = std::floor(clipped_top / tile_size); double last_y_value = std::ceil(clipped_bottom / tile_size) - 1.0; if (include_border) { first_x_value -= 1.0; last_x_value += 1.0; first_y_value -= 1.0; last_y_value += 1.0; } if ((std::fabs(first_x_value) > MAX_RAW_TILE_COORDINATE) || (std::fabs(last_x_value) > MAX_RAW_TILE_COORDINATE) || (std::fabs(first_y_value) > MAX_RAW_TILE_COORDINATE) || (std::fabs(last_y_value) > MAX_RAW_TILE_COORDINATE)) { return Status::VIEWPORT_TOO_LARGE; } const std::int64_t first_x = static_cast(first_x_value); const std::int64_t last_x = static_cast(last_x_value); const std::int64_t first_y = static_cast(first_y_value); const std::int64_t last_y = static_cast(last_y_value); const std::int64_t columns = (last_x - first_x) + 1; const std::int64_t rows = (last_y - first_y) + 1; if ((columns <= 0) || (rows <= 0) || (columns > static_cast(MAX_VIEWPORT_TILES)) || (rows > static_cast(MAX_VIEWPORT_TILES)) || (columns * rows > static_cast(MAX_VIEWPORT_TILES))) { return Status::VIEWPORT_TOO_LARGE; } TilePlacement candidates[MAX_VIEWPORT_TILES]; std::size_t unique_count = 0U; const std::uint32_t tiles = tileCount(zoom); const double viewport_center_x = static_cast(viewport.width) * 0.5; for (std::int64_t raw_y = first_y; raw_y <= last_y; ++raw_y) { const std::uint32_t y = clampTileY(raw_y, tiles); const double screen_y = (static_cast(y) * tile_size) - viewport.top; for (std::int64_t raw_x = first_x; raw_x <= last_x; ++raw_x) { const std::uint32_t x = wrapTileX(raw_x, tiles); const double screen_x = (static_cast(raw_x) * tile_size) - viewport.left; std::size_t duplicate = unique_count; for (std::size_t i = 0U; i < unique_count; ++i) { if ((candidates[i].tile.x == x) && (candidates[i].tile.y == y)) { duplicate = i; break; } } if (duplicate == unique_count) { candidates[unique_count].tile.x = x; candidates[unique_count].tile.y = y; candidates[unique_count].tile.zoom = zoom; candidates[unique_count].screen_x = screen_x; candidates[unique_count].screen_y = screen_y; ++unique_count; } else { const double old_distance = std::fabs((candidates[duplicate].screen_x + (tile_size * 0.5)) - viewport_center_x); const double new_distance = std::fabs((screen_x + (tile_size * 0.5)) - viewport_center_x); if (new_distance < old_distance) { candidates[duplicate].screen_x = screen_x; } } } } if (unique_count > capacity) { return Status::CAPACITY_EXCEEDED; } if ((unique_count != 0U) && (output == NULL)) { return Status::CAPACITY_EXCEEDED; } for (std::size_t i = 0U; i < unique_count; ++i) { output[i] = candidates[i]; } count = unique_count; return Status::OK; } Status projectMarker(const GeoPoint& point, const Viewport& viewport, std::uint32_t zoom, MarkerProjection& output) { if (!isValidZoom(zoom)) { return Status::INVALID_ZOOM; } if (!validViewport(viewport)) { return Status::INVALID_ARGUMENT; } GlobalPixel marker_pixel; const Status status = latLonToGlobalPixel(point, zoom, marker_pixel); if (status != Status::OK) { return status; } const double world = worldPixels(zoom); const double half_width = static_cast(viewport.width) * 0.5; const double center_x = viewport.left + half_width; MarkerProjection result; result.screen_x = half_width + shortestWrappedDelta(marker_pixel.x - center_x, world); result.screen_y = marker_pixel.y - viewport.top; result.visible = (result.screen_x >= 0.0) && (result.screen_x < static_cast(viewport.width)) && (result.screen_y >= 0.0) && (result.screen_y < static_cast(viewport.height)); output = result; return Status::OK; } Status panGlobalPixel(const GlobalPixel& input, double delta_x, double delta_y, std::uint32_t zoom, GlobalPixel& output) { if (!isValidZoom(zoom)) { return Status::INVALID_ZOOM; } if (!finite(input.x) || !finite(input.y) || !finite(delta_x) || !finite(delta_y)) { return Status::INVALID_ARGUMENT; } const double world = worldPixels(zoom); const double next_x = input.x + delta_x; const double next_y = input.y + delta_y; if (!finite(next_x) || !finite(next_y)) { return Status::INVALID_ARGUMENT; } GlobalPixel result; result.x = wrap(next_x, world); result.y = next_y; if (result.y < 0.0) { result.y = 0.0; } else if (result.y > world) { result.y = world; } output = result; return Status::OK; } } // namespace MapProjection } // namespace Pyxis