summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAsheem Mamoowala <asheem.mamoowala@mapbox.com>2018-01-05 06:35:31 -0800
committerAsheem Mamoowala <asheem.mamoowala@mapbox.com>2018-02-08 12:39:30 -0800
commit64374711a09f27c41c93eb6b72d0f6a560234083 (patch)
treec96aece37da9d5a17dc6029bb89d1e6047a2eac6
parentaeba621573d77e1ce4f72b1e9b39bde92bf174a2 (diff)
downloadqtlocation-mapboxgl-64374711a09f27c41c93eb6b72d0f6a560234083.tar.gz
Support TileJSON bounds property (#10701)
* [core] Parse TileJSON bounds property * [core] Add TileRange and LatLngBounds::contains(CanonicalTileID) Move LatLngBounds::contains impl to cpp file * [core] Skip tile creation outside of tileset bounds * [core] Fix TileRange for wrapped bounds and use for CustomTileLoader instead of LatLngBounds comparisons for tiles.
-rw-r--r--cmake/core-files.cmake1
-rw-r--r--cmake/test-files.cmake2
-rw-r--r--include/mbgl/style/conversion/tileset.hpp31
-rw-r--r--include/mbgl/util/geo.hpp18
-rw-r--r--include/mbgl/util/projection.hpp4
-rw-r--r--include/mbgl/util/tileset.hpp14
-rw-r--r--src/mbgl/algorithm/update_renderables.hpp6
-rw-r--r--src/mbgl/annotation/render_annotation_source.cpp1
-rw-r--r--src/mbgl/renderer/sources/render_geojson_source.cpp1
-rw-r--r--src/mbgl/renderer/sources/render_raster_source.cpp1
-rw-r--r--src/mbgl/renderer/sources/render_vector_source.cpp1
-rw-r--r--src/mbgl/renderer/tile_pyramid.cpp11
-rw-r--r--src/mbgl/renderer/tile_pyramid.hpp1
-rw-r--r--src/mbgl/style/conversion/tileset.cpp104
-rw-r--r--src/mbgl/util/geo.cpp82
-rw-r--r--src/mbgl/util/tile_range.hpp47
-rw-r--r--test/style/conversion/tileset.test.cpp72
-rw-r--r--test/util/tile_range.test.cpp56
18 files changed, 436 insertions, 17 deletions
diff --git a/cmake/core-files.cmake b/cmake/core-files.cmake
index 33b3072f3a..0c109d080d 100644
--- a/cmake/core-files.cmake
+++ b/cmake/core-files.cmake
@@ -614,6 +614,7 @@ set(MBGL_CORE_FILES
src/mbgl/util/tile_coordinate.hpp
src/mbgl/util/tile_cover.cpp
src/mbgl/util/tile_cover.hpp
+ src/mbgl/util/tile_range.hpp
src/mbgl/util/tiny_sdf.cpp
src/mbgl/util/tiny_sdf.hpp
src/mbgl/util/token.hpp
diff --git a/cmake/test-files.cmake b/cmake/test-files.cmake
index 027c34f31e..dfd646e624 100644
--- a/cmake/test-files.cmake
+++ b/cmake/test-files.cmake
@@ -87,6 +87,7 @@ set(MBGL_TEST_FILES
test/style/conversion/layer.test.cpp
test/style/conversion/light.test.cpp
test/style/conversion/stringify.test.cpp
+ test/style/conversion/tileset.test.cpp
# style
test/style/filter.test.cpp
@@ -138,6 +139,7 @@ set(MBGL_TEST_FILES
test/util/thread.test.cpp
test/util/thread_local.test.cpp
test/util/tile_cover.test.cpp
+ test/util/tile_range.test.cpp
test/util/timer.test.cpp
test/util/token.test.cpp
test/util/url.test.cpp
diff --git a/include/mbgl/style/conversion/tileset.hpp b/include/mbgl/style/conversion/tileset.hpp
index 377170aa6a..6577e39576 100644
--- a/include/mbgl/style/conversion/tileset.hpp
+++ b/include/mbgl/style/conversion/tileset.hpp
@@ -10,6 +10,11 @@ namespace conversion {
template <>
struct Converter<Tileset> {
public:
+
+ bool validateLatitude(const double lat) const {
+ return lat < 90 && lat > -90;
+ }
+
template <class V>
optional<Tileset> operator()(const V& value, Error& error) const {
Tileset result;
@@ -72,6 +77,32 @@ public:
result.attribution = std::move(*attribution);
}
+ auto boundsValue = objectMember(value, "bounds");
+ if (boundsValue) {
+ if (!isArray(*boundsValue) || arrayLength(*boundsValue) != 4) {
+ error = { "bounds must be an array with left, bottom, top, and right values" };
+ return {};
+ }
+ optional<double> left = toDouble(arrayMember(*boundsValue, 0));
+ optional<double> bottom = toDouble(arrayMember(*boundsValue, 1));
+ optional<double> right = toDouble(arrayMember(*boundsValue, 2));
+ optional<double> top = toDouble(arrayMember(*boundsValue, 3));
+
+ if (!left || !right || !bottom || !top) {
+ error = { "bounds array must contain numeric longitude and latitude values" };
+ return {};
+ }
+ if (!validateLatitude(*bottom) || !validateLatitude(*top) || top <= bottom){
+ error = { "bounds latitude values must be between -90 and 90 with bottom less than top" };
+ return {};
+ }
+ if(*left >= *right) {
+ error = { "bounds left longitude should be less than right longitude" };
+ return {};
+ }
+ result.bounds = LatLngBounds::hull({ *bottom, *left }, { *top, *right });
+ }
+
return result;
}
};
diff --git a/include/mbgl/util/geo.hpp b/include/mbgl/util/geo.hpp
index 6d725b102b..dacdb968f3 100644
--- a/include/mbgl/util/geo.hpp
+++ b/include/mbgl/util/geo.hpp
@@ -154,19 +154,15 @@ public:
sw.longitude() > ne.longitude();
}
- bool contains(const LatLng& point) const {
- return (point.latitude() >= sw.latitude() &&
- point.latitude() <= ne.latitude() &&
- point.longitude() >= sw.longitude() &&
- point.longitude() <= ne.longitude());
+ bool crossesAntimeridian() const {
+ return (sw.wrapped().longitude() > ne.wrapped().longitude());
}
- bool intersects(const LatLngBounds area) const {
- return (area.ne.latitude() > sw.latitude() &&
- area.sw.latitude() < ne.latitude() &&
- area.ne.longitude() > sw.longitude() &&
- area.sw.longitude() < ne.longitude());
- }
+ bool contains(const CanonicalTileID& tileID) const;
+ bool contains(const LatLng& point, LatLng::WrapMode wrap = LatLng::Unwrapped) const;
+ bool contains(const LatLngBounds& area, LatLng::WrapMode wrap = LatLng::Unwrapped) const;
+
+ bool intersects(const LatLngBounds area, LatLng::WrapMode wrap = LatLng::Unwrapped) const;
private:
LatLng sw;
diff --git a/include/mbgl/util/projection.hpp b/include/mbgl/util/projection.hpp
index f64502c5bc..a9c12e595b 100644
--- a/include/mbgl/util/projection.hpp
+++ b/include/mbgl/util/projection.hpp
@@ -78,6 +78,10 @@ public:
return project_(latLng, worldSize(scale));
}
+ static Point<double> project(const LatLng& latLng, uint8_t zoom) {
+ return project_(latLng, std::pow(2.0, zoom));
+ }
+
static LatLng unproject(const Point<double>& p, double scale, LatLng::WrapMode wrapMode = LatLng::Unwrapped) {
auto p2 = p * util::DEGREES_MAX / worldSize(scale);
return LatLng {
diff --git a/include/mbgl/util/tileset.hpp b/include/mbgl/util/tileset.hpp
index 61aa47d4ea..7bef0e89ed 100644
--- a/include/mbgl/util/tileset.hpp
+++ b/include/mbgl/util/tileset.hpp
@@ -2,7 +2,9 @@
#include <mbgl/util/range.hpp>
#include <mbgl/util/constants.hpp>
-
+#include <mbgl/util/optional.hpp>
+#include <mbgl/util/geo.hpp>
+#include <tuple>
#include <vector>
#include <string>
#include <cstdint>
@@ -17,6 +19,7 @@ public:
Range<uint8_t> zoomRange;
std::string attribution;
Scheme scheme;
+ optional<LatLngBounds> bounds;
Tileset(std::vector<std::string> tiles_ = std::vector<std::string>(),
Range<uint8_t> zoomRange_ = { 0, util::DEFAULT_MAX_ZOOM },
@@ -25,13 +28,14 @@ public:
: tiles(std::move(tiles_)),
zoomRange(std::move(zoomRange_)),
attribution(std::move(attribution_)),
- scheme(scheme_) {}
+ scheme(scheme_),
+ bounds() {}
- // TileJSON also includes center, zoom, and bounds, but they are not used by mbgl.
+ // TileJSON also includes center and zoom but they are not used by mbgl.
friend bool operator==(const Tileset& lhs, const Tileset& rhs) {
- return std::tie(lhs.tiles, lhs.zoomRange, lhs.attribution, lhs.scheme)
- == std::tie(rhs.tiles, rhs.zoomRange, rhs.attribution, rhs.scheme);
+ return std::tie(lhs.tiles, lhs.zoomRange, lhs.attribution, lhs.scheme, lhs.bounds)
+ == std::tie(rhs.tiles, rhs.zoomRange, rhs.attribution, rhs.scheme, rhs.bounds);
}
};
diff --git a/src/mbgl/algorithm/update_renderables.hpp b/src/mbgl/algorithm/update_renderables.hpp
index c583b6b2b6..5fbe0d943f 100644
--- a/src/mbgl/algorithm/update_renderables.hpp
+++ b/src/mbgl/algorithm/update_renderables.hpp
@@ -35,7 +35,11 @@ void updateRenderables(GetTileFn getTile,
auto tile = getTile(idealDataTileID);
if (!tile) {
tile = createTile(idealDataTileID);
- assert(tile);
+ // For source types where TileJSON.bounds is set, tiles outside the
+ // bounds are not created
+ if(tile == nullptr) {
+ continue;
+ }
}
// if (source has the tile and bucket is loaded) {
diff --git a/src/mbgl/annotation/render_annotation_source.cpp b/src/mbgl/annotation/render_annotation_source.cpp
index ba80be0da0..0aff64583d 100644
--- a/src/mbgl/annotation/render_annotation_source.cpp
+++ b/src/mbgl/annotation/render_annotation_source.cpp
@@ -41,6 +41,7 @@ void RenderAnnotationSource::update(Immutable<style::Source::Impl> baseImpl_,
// Zoom level 16 is typically sufficient for annotations.
// See https://github.com/mapbox/mapbox-gl-native/issues/10197
{ 0, 16 },
+ optional<LatLngBounds> {},
[&] (const OverscaledTileID& tileID) {
return std::make_unique<AnnotationTile>(tileID, parameters);
});
diff --git a/src/mbgl/renderer/sources/render_geojson_source.cpp b/src/mbgl/renderer/sources/render_geojson_source.cpp
index 504db78ea3..c13cd49f46 100644
--- a/src/mbgl/renderer/sources/render_geojson_source.cpp
+++ b/src/mbgl/renderer/sources/render_geojson_source.cpp
@@ -62,6 +62,7 @@ void RenderGeoJSONSource::update(Immutable<style::Source::Impl> baseImpl_,
SourceType::GeoJSON,
util::tileSize,
impl().getZoomRange(),
+ optional<LatLngBounds>{},
[&] (const OverscaledTileID& tileID) {
return std::make_unique<GeoJSONTile>(tileID, impl().id, parameters, data->getTile(tileID.canonical));
});
diff --git a/src/mbgl/renderer/sources/render_raster_source.cpp b/src/mbgl/renderer/sources/render_raster_source.cpp
index bcd719365d..5e63e929e2 100644
--- a/src/mbgl/renderer/sources/render_raster_source.cpp
+++ b/src/mbgl/renderer/sources/render_raster_source.cpp
@@ -52,6 +52,7 @@ void RenderRasterSource::update(Immutable<style::Source::Impl> baseImpl_,
SourceType::Raster,
impl().getTileSize(),
tileset->zoomRange,
+ tileset->bounds,
[&] (const OverscaledTileID& tileID) {
return std::make_unique<RasterTile>(tileID, parameters, *tileset);
});
diff --git a/src/mbgl/renderer/sources/render_vector_source.cpp b/src/mbgl/renderer/sources/render_vector_source.cpp
index ca3071c6b0..a3da3e7cbd 100644
--- a/src/mbgl/renderer/sources/render_vector_source.cpp
+++ b/src/mbgl/renderer/sources/render_vector_source.cpp
@@ -55,6 +55,7 @@ void RenderVectorSource::update(Immutable<style::Source::Impl> baseImpl_,
SourceType::Vector,
util::tileSize,
tileset->zoomRange,
+ tileset->bounds,
[&] (const OverscaledTileID& tileID) {
return std::make_unique<VectorTile>(tileID, impl().id, parameters, *tileset);
});
diff --git a/src/mbgl/renderer/tile_pyramid.cpp b/src/mbgl/renderer/tile_pyramid.cpp
index 3e2311089d..c9e3b0630a 100644
--- a/src/mbgl/renderer/tile_pyramid.cpp
+++ b/src/mbgl/renderer/tile_pyramid.cpp
@@ -8,6 +8,7 @@
#include <mbgl/text/placement_config.hpp>
#include <mbgl/math/clamp.hpp>
#include <mbgl/util/tile_cover.hpp>
+#include <mbgl/util/tile_range.hpp>
#include <mbgl/util/enum.hpp>
#include <mbgl/util/logging.hpp>
@@ -15,6 +16,7 @@
#include <mapbox/geometry/envelope.hpp>
+#include <cmath>
#include <algorithm>
namespace mbgl {
@@ -62,6 +64,7 @@ void TilePyramid::update(const std::vector<Immutable<style::Layer::Impl>>& layer
const SourceType type,
const uint16_t tileSize,
const Range<uint8_t> zoomRange,
+ optional<LatLngBounds> bounds,
std::function<std::unique_ptr<Tile> (const OverscaledTileID&)> createTile) {
// If we need a relayout, abandon any cached tiles; they're now stale.
if (needsRelayout) {
@@ -134,7 +137,15 @@ void TilePyramid::update(const std::vector<Immutable<style::Layer::Impl>>& layer
auto it = tiles.find(tileID);
return it == tiles.end() ? nullptr : it->second.get();
};
+
+ optional<util::TileRange> tileRange = {};
+ if (bounds) {
+ tileRange = util::TileRange::fromLatLngBounds(*bounds, std::min(tileZoom, (int32_t)zoomRange.max));
+ }
auto createTileFn = [&](const OverscaledTileID& tileID) -> Tile* {
+ if (tileRange && !tileRange->contains(tileID.canonical)) {
+ return nullptr;
+ }
std::unique_ptr<Tile> tile = cache.get(tileID);
if (!tile) {
tile = createTile(tileID);
diff --git a/src/mbgl/renderer/tile_pyramid.hpp b/src/mbgl/renderer/tile_pyramid.hpp
index 73a8d34c1c..e34b050273 100644
--- a/src/mbgl/renderer/tile_pyramid.hpp
+++ b/src/mbgl/renderer/tile_pyramid.hpp
@@ -40,6 +40,7 @@ public:
SourceType type,
uint16_t tileSize,
Range<uint8_t> zoomRange,
+ optional<LatLngBounds> bounds,
std::function<std::unique_ptr<Tile> (const OverscaledTileID&)> createTile);
void startRender(PaintParameters&);
diff --git a/src/mbgl/style/conversion/tileset.cpp b/src/mbgl/style/conversion/tileset.cpp
new file mode 100644
index 0000000000..6e559c0cac
--- /dev/null
+++ b/src/mbgl/style/conversion/tileset.cpp
@@ -0,0 +1,104 @@
+#include <mbgl/style/conversion/tileset.hpp>
+#include <mbgl/util/geo.hpp>
+
+namespace mbgl {
+namespace style {
+namespace conversion {
+
+bool validateLatitude(const double lat) {
+ return lat < 90 && lat > -90;
+}
+
+optional<Tileset> Converter<Tileset>::operator()(const Convertible& value, Error& error) const {
+ Tileset result;
+
+ auto tiles = objectMember(value, "tiles");
+ if (!tiles) {
+ error = { "source must have tiles" };
+ return {};
+ }
+
+ if (!isArray(*tiles)) {
+ error = { "source tiles must be an array" };
+ return {};
+ }
+
+ for (std::size_t i = 0; i < arrayLength(*tiles); i++) {
+ optional<std::string> urlTemplate = toString(arrayMember(*tiles, i));
+ if (!urlTemplate) {
+ error = { "source tiles member must be a string" };
+ return {};
+ }
+ result.tiles.push_back(std::move(*urlTemplate));
+ }
+
+ auto schemeValue = objectMember(value, "scheme");
+ if (schemeValue) {
+ optional<std::string> scheme = toString(*schemeValue);
+ if (scheme && *scheme == "tms") {
+ result.scheme = Tileset::Scheme::TMS;
+ }
+ }
+
+ auto minzoomValue = objectMember(value, "minzoom");
+ if (minzoomValue) {
+ optional<float> minzoom = toNumber(*minzoomValue);
+ if (!minzoom || *minzoom < 0 || *minzoom > std::numeric_limits<uint8_t>::max()) {
+ error = { "invalid minzoom" };
+ return {};
+ }
+ result.zoomRange.min = *minzoom;
+ }
+
+ auto maxzoomValue = objectMember(value, "maxzoom");
+ if (maxzoomValue) {
+ optional<float> maxzoom = toNumber(*maxzoomValue);
+ if (!maxzoom || *maxzoom < 0 || *maxzoom > std::numeric_limits<uint8_t>::max()) {
+ error = { "invalid maxzoom" };
+ return {};
+ }
+ result.zoomRange.max = *maxzoom;
+ }
+
+ auto attributionValue = objectMember(value, "attribution");
+ if (attributionValue) {
+ optional<std::string> attribution = toString(*attributionValue);
+ if (!attribution) {
+ error = { "source attribution must be a string" };
+ return {};
+ }
+ result.attribution = std::move(*attribution);
+ }
+
+ auto boundsValue = objectMember(value, "bounds");
+ if (boundsValue) {
+ if (!isArray(*boundsValue) || arrayLength(*boundsValue) != 4) {
+ error = { "bounds must be an array with left, bottom, top, and right values" };
+ return {};
+ }
+ optional<double> left = toDouble(arrayMember(*boundsValue, 0));
+ optional<double> bottom = toDouble(arrayMember(*boundsValue, 1));
+ optional<double> right = toDouble(arrayMember(*boundsValue, 2));
+ optional<double> top = toDouble(arrayMember(*boundsValue, 3));
+
+ if (!left || !right || !bottom || !top) {
+ error = { "bounds array must contain numeric longitude and latitude values" };
+ return {};
+ }
+ if (!validateLatitude(*bottom) || !validateLatitude(*top) || top <= bottom){
+ error = { "bounds latitude values must be between -90 and 90 with bottom less than top" };
+ return {};
+ }
+ if(*left >= *right) {
+ error = { "bounds left longitude should be less than right longitude" };
+ return {};
+ }
+ result.bounds = LatLngBounds::hull({ *bottom, *left }, { *top, *right });
+ }
+
+ return result;
+}
+
+} // namespace conversion
+} // namespace style
+} // namespace mbgl
diff --git a/src/mbgl/util/geo.cpp b/src/mbgl/util/geo.cpp
index f38aba20c4..a04e2c5355 100644
--- a/src/mbgl/util/geo.cpp
+++ b/src/mbgl/util/geo.cpp
@@ -1,6 +1,8 @@
#include <mbgl/util/geo.hpp>
#include <mbgl/util/constants.hpp>
#include <mbgl/tile/tile_id.hpp>
+#include <mbgl/math/clamp.hpp>
+#include <mbgl/util/tile_range.hpp>
#include <cmath>
@@ -32,6 +34,86 @@ LatLngBounds::LatLngBounds(const CanonicalTileID& id)
ne({ lat_(id.z, id.y), lon_(id.z, id.x + 1) }) {
}
+bool LatLngBounds::contains(const CanonicalTileID& tileID) const {
+ return util::TileRange::fromLatLngBounds(*this, tileID.z).contains(tileID);
+}
+
+bool LatLngBounds::contains(const LatLng& point, LatLng::WrapMode wrap /*= LatLng::Unwrapped*/) const {
+ bool containsLatitude = point.latitude() >= sw.latitude() &&
+ point.latitude() <= ne.latitude();
+ if (!containsLatitude) {
+ return false;
+ }
+
+ bool containsUnwrappedLongitude = point.longitude() >= sw.longitude() &&
+ point.longitude() <= ne.longitude();
+ if (containsUnwrappedLongitude) {
+ return true;
+ } else if (wrap == LatLng::Wrapped) {
+ LatLngBounds wrapped(sw.wrapped(), ne.wrapped());
+ auto ptLon = point.wrapped().longitude();
+ if (crossesAntimeridian()) {
+ return (ptLon >= wrapped.sw.longitude() &&
+ ptLon <= util::LONGITUDE_MAX) ||
+ (ptLon <= wrapped.ne.longitude() &&
+ ptLon >= -util::LONGITUDE_MAX);
+ } else {
+ return (ptLon >= wrapped.sw.longitude() &&
+ ptLon <= wrapped.ne.longitude());
+ }
+ }
+ return false;
+}
+
+bool LatLngBounds::contains(const LatLngBounds& area, LatLng::WrapMode wrap /*= LatLng::Unwrapped*/) const {
+ bool containsLatitude = area.north() <= north() && area.south() >= south();
+ if (!containsLatitude) {
+ return false;
+ }
+
+ bool containsUnwrapped = area.east() <= east() && area.west() >= west();
+ if(containsUnwrapped) {
+ return true;
+ } else if (wrap == LatLng::Wrapped) {
+ LatLngBounds wrapped(sw.wrapped(), ne.wrapped());
+ LatLngBounds other(area.sw.wrapped(), area.ne.wrapped());
+ if (crossesAntimeridian() & !area.crossesAntimeridian()) {
+ return (other.east() <= util::LONGITUDE_MAX && other.west() >= wrapped.west()) ||
+ (other.east() <= wrapped.east() && other.west() >= -util::LONGITUDE_MAX);
+ } else {
+ return other.east() <= wrapped.east() && other.west() >= wrapped.west();
+ }
+ }
+ return false;
+}
+
+bool LatLngBounds::intersects(const LatLngBounds area, LatLng::WrapMode wrap /*= LatLng::Unwrapped*/) const {
+ bool latitudeIntersects = area.north() > south() && area.south() < north();
+ if (!latitudeIntersects) {
+ return false;
+ }
+
+ bool longitudeIntersects = area.east() > west() && area.west() < east();
+ if (longitudeIntersects) {
+ return true;
+ } else if (wrap == LatLng::Wrapped) {
+ LatLngBounds wrapped(sw.wrapped(), ne.wrapped());
+ LatLngBounds other(area.sw.wrapped(), area.ne.wrapped());
+ if (crossesAntimeridian()) {
+ return area.crossesAntimeridian() ||
+ other.east() > wrapped.west() ||
+ other.west() < wrapped.east();
+ } else if (other.crossesAntimeridian()){
+ return other.east() > wrapped.west() ||
+ other.west() < wrapped.east();
+ } else {
+ return other.east() > wrapped.west() &&
+ other.west() < wrapped.east();
+ }
+ }
+ return false;
+}
+
ScreenCoordinate EdgeInsets::getCenter(uint16_t width, uint16_t height) const {
return {
(width - left() - right()) / 2.0 + left(),
diff --git a/src/mbgl/util/tile_range.hpp b/src/mbgl/util/tile_range.hpp
new file mode 100644
index 0000000000..f630a49078
--- /dev/null
+++ b/src/mbgl/util/tile_range.hpp
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <mbgl/tile/tile_id.hpp>
+#include <mbgl/util/range.hpp>
+#include <mbgl/util/geo.hpp>
+#include <mbgl/util/projection.hpp>
+
+namespace mbgl {
+
+namespace util {
+
+class TileRange {
+public:
+ Range<Point<double>> range;
+ uint8_t z;
+
+ // Compute the range of tiles covered by the bounds.
+ static TileRange fromLatLngBounds(const LatLngBounds& bounds, uint8_t z) {
+ auto swProj = Projection::project(bounds.southwest().wrapped(), z);
+ auto ne = bounds.northeast();
+ auto neProj = Projection::project(ne.longitude() > util::LONGITUDE_MAX ? ne.wrapped() : ne , z);
+ const auto minX = std::floor(swProj.x);
+ const auto maxX = std::ceil(neProj.x);
+ const auto minY = std::floor(neProj.y);
+ const auto maxY = std::ceil(swProj.y);
+ return TileRange({ {minX, minY}, {maxX, maxY} }, z);
+ }
+
+ bool contains(const CanonicalTileID& tileID) {
+ return z == tileID.z &&
+ (range.min.x >= range.max.x ? //For wrapped bounds
+ tileID.x >= range.min.x || tileID.x < range.max.x :
+ tileID.x < range.max.x && tileID.x >= range.min.x) &&
+ tileID.y < range.max.y &&
+ tileID.y >= range.min.y;
+ }
+
+private:
+ TileRange(Range<Point<double>> range_, uint8_t z_)
+ : range(range_),
+ z(z_) {
+ }
+
+};
+
+} // namespace util
+} // namespace mbgl
diff --git a/test/style/conversion/tileset.test.cpp b/test/style/conversion/tileset.test.cpp
new file mode 100644
index 0000000000..8002cd038f
--- /dev/null
+++ b/test/style/conversion/tileset.test.cpp
@@ -0,0 +1,72 @@
+#include <mbgl/test/util.hpp>
+
+#include <mbgl/style/conversion/json.hpp>
+#include <mbgl/style/conversion/tileset.hpp>
+
+#include <mbgl/util/logging.hpp>
+
+using namespace mbgl;
+using namespace mbgl::style::conversion;
+
+TEST(Tileset, Empty) {
+ Error error;
+ mbgl::optional<Tileset> converted = convertJSON<Tileset>("{}", error);
+ EXPECT_FALSE((bool) converted);
+}
+
+TEST(Tileset, ErrorHandling) {
+ Error error;
+ mbgl::optional<Tileset> converted = convertJSON<Tileset>(R"JSON({
+ "tiles": "should not be a string"
+ })JSON", error);
+ EXPECT_FALSE((bool) converted);
+}
+
+TEST(Tileset, InvalidBounds) {
+ {
+ Error error;
+ mbgl::optional<Tileset> converted = convertJSON<Tileset>(R"JSON({
+ "tiles": ["http://mytiles"],
+ "bounds": [73, -180, -73, -120]
+ })JSON", error);
+
+ EXPECT_FALSE((bool) converted);
+ }
+ {
+ Error error;
+ mbgl::optional<Tileset> converted = convertJSON<Tileset>(R"JSON({
+ "tiles": ["http://mytiles"],
+ "bounds": [-120]
+ })JSON", error);
+
+ EXPECT_FALSE((bool) converted);
+ }
+ {
+ Error error;
+ mbgl::optional<Tileset> converted = convertJSON<Tileset>(R"JSON({
+ "tiles": ["http://mytiles"],
+ "bounds": "should not be a string"
+ })JSON", error);
+
+ EXPECT_FALSE((bool) converted);
+ }
+}
+
+TEST(Tileset, FullConversion) {
+ Error error;
+ Tileset converted = *convertJSON<Tileset>(R"JSON({
+ "tiles": ["http://mytiles"],
+ "scheme": "xyz",
+ "minzoom": 1,
+ "maxzoom": 2,
+ "attribution": "mapbox",
+ "bounds": [-180, -73, -120, 73]
+ })JSON", error);
+
+ EXPECT_EQ(converted.tiles[0], "http://mytiles");
+ EXPECT_EQ(converted.scheme, Tileset::Scheme::XYZ);
+ EXPECT_EQ(converted.zoomRange.min, 1);
+ EXPECT_EQ(converted.zoomRange.max, 2);
+ EXPECT_EQ(converted.attribution, "mapbox");
+ EXPECT_EQ(converted.bounds, LatLngBounds::hull({73, -180}, {-73, -120}));
+}
diff --git a/test/util/tile_range.test.cpp b/test/util/tile_range.test.cpp
new file mode 100644
index 0000000000..dc8ae28705
--- /dev/null
+++ b/test/util/tile_range.test.cpp
@@ -0,0 +1,56 @@
+
+#include <mbgl/util/tile_range.hpp>
+#include <mbgl/util/geo.hpp>
+#include <mbgl/map/transform.hpp>
+
+#include <gtest/gtest.h>
+
+using namespace mbgl;
+
+TEST(TileRange, ContainsWorld) {
+ auto range = util::TileRange::fromLatLngBounds(LatLngBounds::world(), 0);
+ EXPECT_TRUE(range.contains(CanonicalTileID(0, 0, 0)));
+ EXPECT_FALSE(range.contains(CanonicalTileID(10, 0, 0)));
+}
+
+TEST(TileRange, ContainsBoundsFromTile) {
+ {
+ const LatLngBounds bounds{ CanonicalTileID(3, 7, 0) };
+ auto range = util::TileRange::fromLatLngBounds(bounds, 3);
+ EXPECT_TRUE(range.contains(CanonicalTileID(3, 7, 0)));
+ }
+ {
+ const LatLngBounds bounds{ CanonicalTileID(10, 162, 395) };
+ auto range = util::TileRange::fromLatLngBounds(bounds, 10);
+ EXPECT_TRUE(range.contains(CanonicalTileID(10, 162, 395)));
+ }
+}
+TEST(TileRange, ContainsIntersectingTiles) {
+ auto bounds = LatLngBounds::hull({ 37.6609, -122.5744 }, { 37.8271, -122.3204 });
+ auto range = util::TileRange::fromLatLngBounds(bounds, 13);
+ EXPECT_FALSE(range.contains(CanonicalTileID(13, 1316, 3100)));
+ EXPECT_TRUE(range.contains(CanonicalTileID(13, 1310, 3166)));
+}
+
+TEST(TileRange, ContainsWrappedBounds) {
+ auto wrappedBounds = LatLngBounds::hull({ 37.6609, 237.6796 }, { 37.8271, 237.4256 });
+ auto range = util::TileRange::fromLatLngBounds(wrappedBounds, 13);
+ EXPECT_FALSE(range.contains(CanonicalTileID(13, 1316, 3100)));
+ EXPECT_TRUE(range.contains(CanonicalTileID(13, 1310, 3166)));
+}
+
+TEST(TileRange, ContainsBoundsCrossingAntimeridian) {
+ {
+ auto cossingBounds = LatLngBounds::hull({-20.9615, -214.309}, {19.477, -155.830});
+ auto range = util::TileRange::fromLatLngBounds(cossingBounds, 1);
+ EXPECT_TRUE(range.contains(CanonicalTileID(1, 1, 1)));
+ EXPECT_TRUE(range.contains(CanonicalTileID(1, 0, 0)));
+ }
+ {
+ auto cossingBounds = LatLngBounds::hull({-20.9615, -214.309}, {19.477, -155.830});
+ auto range = util::TileRange::fromLatLngBounds(cossingBounds, 6);
+ EXPECT_FALSE(range.contains(CanonicalTileID(6, 55, 34)));
+ EXPECT_FALSE(range.contains(CanonicalTileID(6, 5, 28)));
+ EXPECT_TRUE(range.contains(CanonicalTileID(6, 63, 28)));
+ }
+}