Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Arbitrary offline region shapes #11447

Merged
merged 4 commits into from
Aug 20, 2018
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
90 changes: 76 additions & 14 deletions bin/offline.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <mbgl/util/default_styles.hpp>
#include <mbgl/util/run_loop.hpp>
#include <mbgl/util/string.hpp>
#include <mbgl/util/geojson.hpp>

#include <mbgl/storage/default_file_source.hpp>

Expand All @@ -11,9 +12,47 @@
#include <csignal>
#include <atomic>
#include <sstream>
#include <fstream>

using namespace std::literals::chrono_literals;

std::string readFile(const std::string& fileName) {
std::ifstream stream(fileName.c_str());
if (!stream.good()) {
throw std::runtime_error("Cannot read file: " + fileName);
}

std::stringstream buffer;
buffer << stream.rdbuf();
stream.close();

return buffer.str();
}

mapbox::geometry::geometry<double> parseGeometry(const std::string& json) {
using namespace mapbox::geojson;
auto geojson = parse(json);
return geojson.match(
[](const geometry& geom) {
return geom;
},
[](const feature& feature) {
return feature.geometry;
},
[](const feature_collection& featureCollection) {
if (featureCollection.size() < 1) {
throw std::runtime_error("No features in feature collection");
}
geometry_collection geometries;

for (auto feature : featureCollection) {
geometries.push_back(feature.geometry);
}

return geometries;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

geometry_collection isn't supported by mbgl::util::TileCover. How would this be used in defining an offline region?
(MultiPoint, MultiLineString, and MultiPolygon are supported)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a recursive definition then. Is there any reason not to support it?

Mainly the types the interface accepts should be implemented right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The streaming Tile cover algorithm we are using works on homogeneous geometry types. A mixed type like geometry_collection would have to be run on each of the contained types separately and would return duplicates. It might also not return tiles in a consistent order.

});
}

int main(int argc, char *argv[]) {
args::ArgumentParser argumentParser("Mapbox GL offline tool");
args::HelpFlag helpFlag(argumentParser, "help", "Display this help menu", {'h', "help"});
Expand All @@ -22,11 +61,19 @@ int main(int argc, char *argv[]) {
args::ValueFlag<std::string> styleValue(argumentParser, "URL", "Map stylesheet", {'s', "style"});
args::ValueFlag<std::string> outputValue(argumentParser, "file", "Output database file name", {'o', "output"});
args::ValueFlag<std::string> apiBaseValue(argumentParser, "URL", "API Base URL", {'a', "apiBaseURL"});

args::ValueFlag<double> northValue(argumentParser, "degrees", "North latitude", {"north"});
args::ValueFlag<double> westValue(argumentParser, "degrees", "West longitude", {"west"});
args::ValueFlag<double> southValue(argumentParser, "degrees", "South latitude", {"south"});
args::ValueFlag<double> eastValue(argumentParser, "degrees", "East longitude", {"east"});

// LatLngBounds
args::Group latLngBoundsGroup(argumentParser, "LatLng bounds:", args::Group::Validators::AllOrNone);
args::ValueFlag<double> northValue(latLngBoundsGroup, "degrees", "North latitude", {"north"});
args::ValueFlag<double> westValue(latLngBoundsGroup, "degrees", "West longitude", {"west"});
args::ValueFlag<double> southValue(latLngBoundsGroup, "degrees", "South latitude", {"south"});
args::ValueFlag<double> eastValue(latLngBoundsGroup, "degrees", "East longitude", {"east"});

// Geometry
args::Group geoJSONGroup(argumentParser, "GeoJson geometry:", args::Group::Validators::AllOrNone);
args::ValueFlag<std::string> geometryValue(geoJSONGroup, "file", "GeoJSON file containing the region geometry", {"geojson"});


args::ValueFlag<double> minZoomValue(argumentParser, "number", "Min zoom level", {"minZoom"});
args::ValueFlag<double> maxZoomValue(argumentParser, "number", "Max zoom level", {"maxZoom"});
args::ValueFlag<double> pixelRatioValue(argumentParser, "number", "Pixel ratio", {"pixelRatio"});
Expand All @@ -48,23 +95,39 @@ int main(int argc, char *argv[]) {

std::string style = styleValue ? args::get(styleValue) : mbgl::util::default_styles::streets.url;

// Bay area
const double north = northValue ? args::get(northValue) : 37.2;
const double west = westValue ? args::get(westValue) : -122.8;
const double south = southValue ? args::get(southValue) : 38.1;
const double east = eastValue ? args::get(eastValue) : -121.7;

const double minZoom = minZoomValue ? args::get(minZoomValue) : 0.0;
const double maxZoom = maxZoomValue ? args::get(maxZoomValue) : 15.0;
const double pixelRatio = pixelRatioValue ? args::get(pixelRatioValue) : 1.0;
const std::string output = outputValue ? args::get(outputValue) : "offline.db";

using namespace mbgl;

OfflineRegionDefinition definition = [&]() {
if (geometryValue) {
try {
std::string json = readFile(geometryValue.Get());
auto geometry = parseGeometry(json);
return OfflineRegionDefinition{ OfflineGeometryRegionDefinition(style, geometry, minZoom, maxZoom, pixelRatio) };
} catch(std::runtime_error e) {
std::cerr << "Could not parse geojson file " << geometryValue.Get() << ": " << e.what() << std::endl;
exit(1);
}
} else {
// Bay area
const double north = northValue ? args::get(northValue) : 37.2;
const double west = westValue ? args::get(westValue) : -122.8;
const double south = southValue ? args::get(southValue) : 38.1;
const double east = eastValue ? args::get(eastValue) : -121.7;
LatLngBounds boundingBox = LatLngBounds::hull(LatLng(north, west), LatLng(south, east));
return OfflineRegionDefinition{ OfflineTilePyramidRegionDefinition(style, boundingBox, minZoom, maxZoom, pixelRatio) };
}
}();

const char* tokenEnv = getenv("MAPBOX_ACCESS_TOKEN");
const std::string token = tokenValue ? args::get(tokenValue) : (tokenEnv ? tokenEnv : std::string());

const std::string apiBaseURL = apiBaseValue ? args::get(apiBaseValue) : mbgl::util::API_BASE_URL;

using namespace mbgl;

util::RunLoop loop;
DefaultFileSource fileSource(output, ".");
Expand All @@ -73,8 +136,7 @@ int main(int argc, char *argv[]) {
fileSource.setAccessToken(token);
fileSource.setAPIBaseURL(apiBaseURL);

LatLngBounds boundingBox = LatLngBounds::hull(LatLng(north, west), LatLng(south, east));
OfflineTilePyramidRegionDefinition definition(style, boundingBox, minZoom, maxZoom, pixelRatio);

OfflineRegionMetadata metadata;

class Observer : public OfflineRegionObserver {
Expand Down
34 changes: 27 additions & 7 deletions include/mbgl/storage/offline.hpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#pragma once

#include <mbgl/util/geo.hpp>
#include <mbgl/util/geometry.hpp>
#include <mbgl/util/range.hpp>
#include <mbgl/util/optional.hpp>
#include <mbgl/util/variant.hpp>
#include <mbgl/style/types.hpp>
#include <mbgl/storage/response.hpp>

Expand Down Expand Up @@ -30,22 +32,40 @@ class OfflineTilePyramidRegionDefinition {
OfflineTilePyramidRegionDefinition(std::string, LatLngBounds, double, double, float);

/* Private */
std::vector<CanonicalTileID> tileCover(style::SourceType, uint16_t tileSize, const Range<uint8_t>& zoomRange) const;
uint64_t tileCount(style::SourceType, uint16_t tileSize, const Range<uint8_t>& zoomRange) const;
const std::string styleURL;
const LatLngBounds bounds;
const double minZoom;
const double maxZoom;
const float pixelRatio;
private:
Range<uint8_t> coveringZoomRange(style::SourceType, uint16_t tileSize, const Range<uint8_t>& zoomRange) const;
};

/*
* For the present, a tile pyramid is the only type of offline region. In the future,
* other definition types will be available and this will be a variant type.
* An offline region defined by a style URL, geometry, zoom range, and
* device pixel ratio.
*
* Both minZoom and maxZoom must be ≥ 0, and maxZoom must be ≥ minZoom.
*
* maxZoom may be ∞, in which case for each tile source, the region will include
* tiles from minZoom up to the maximum zoom level provided by that source.
*
* pixelRatio must be ≥ 0 and should typically be 1.0 or 2.0.
*/
class OfflineGeometryRegionDefinition {
public:
OfflineGeometryRegionDefinition(std::string styleURL, Geometry<double>, double minZoom, double maxZoom, float pixelRatio);

/* Private */
const std::string styleURL;
const Geometry<double> geometry;
const double minZoom;
const double maxZoom;
const float pixelRatio;
};

/*
* The offline region definition types supported
*/
using OfflineRegionDefinition = OfflineTilePyramidRegionDefinition;
using OfflineRegionDefinition = variant<OfflineTilePyramidRegionDefinition, OfflineGeometryRegionDefinition>;

/*
* The encoded format is private.
Expand Down
1 change: 1 addition & 0 deletions platform/android/MapboxGLAndroidSDK/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
api (dependenciesList.mapboxAndroidGestures) {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation dependenciesList.mapboxJavaTurf
implementation dependenciesList.supportAnnotations
implementation dependenciesList.supportFragmentV4
implementation dependenciesList.timber
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.mapbox.mapboxsdk.offline;

import android.os.Parcel;
import android.os.Parcelable;

import com.mapbox.geojson.Feature;
import com.mapbox.geojson.Geometry;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.turf.TurfMeasurement;

/**
* An offline region defined by a style URL, geometry, zoom range, and
* device pixel ratio.
* <p>
* Both minZoom and maxZoom must be ≥ 0, and maxZoom must be ≥ minZoom.
* <p>
* maxZoom may be ∞, in which case for each tile source, the region will include
* tiles from minZoom up to the maximum zoom level provided by that source.
* <p>
* pixelRatio must be ≥ 0 and should typically be 1.0 or 2.0.
*/
public class OfflineGeometryRegionDefinition implements OfflineRegionDefinition, Parcelable {

private String styleURL;
private Geometry geometry;
private double minZoom;
private double maxZoom;
private float pixelRatio;

/**
* Constructor to create an OfflineGeometryRegionDefinition from parameters.
*
* @param styleURL the style
* @param geometry the geometry
* @param minZoom min zoom
* @param maxZoom max zoom
* @param pixelRatio pixel ratio of the device
*/
public OfflineGeometryRegionDefinition(
String styleURL, Geometry geometry, double minZoom, double maxZoom, float pixelRatio) {
// Note: Also used in JNI
this.styleURL = styleURL;
this.geometry = geometry;
this.minZoom = minZoom;
this.maxZoom = maxZoom;
this.pixelRatio = pixelRatio;
}

/**
* Constructor to create an OfflineGeometryRegionDefinition from a Parcel.
*
* @param parcel the parcel to create the OfflineGeometryRegionDefinition from
*/
public OfflineGeometryRegionDefinition(Parcel parcel) {
this.styleURL = parcel.readString();
this.geometry = Feature.fromJson(parcel.readString()).geometry();
this.minZoom = parcel.readDouble();
this.maxZoom = parcel.readDouble();
this.pixelRatio = parcel.readFloat();
}

/*
* Getters
*/

public String getStyleURL() {
return styleURL;
}

public Geometry getGeometry() {
return geometry;
}

/**
* Calculates the bounding box for the Geometry it contains
* to retain backwards compatibility
* @return the {@link LatLngBounds} or null
*/
@Override
public LatLngBounds getBounds() {
if (geometry == null) {
return null;
}

double[] bbox = TurfMeasurement.bbox(geometry);
return LatLngBounds.from(bbox[3], bbox[2], bbox[1], bbox[0]);
}

public double getMinZoom() {
return minZoom;
}

public double getMaxZoom() {
return maxZoom;
}

public float getPixelRatio() {
return pixelRatio;
}

/*
* Parceable
*/

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(styleURL);
dest.writeString(Feature.fromGeometry(geometry).toJson());
dest.writeDouble(minZoom);
dest.writeDouble(maxZoom);
dest.writeFloat(pixelRatio);
}

public static final Creator CREATOR = new Creator() {
public OfflineGeometryRegionDefinition createFromParcel(Parcel in) {
return new OfflineGeometryRegionDefinition(in);
}

public OfflineGeometryRegionDefinition[] newArray(int size) {
return new OfflineGeometryRegionDefinition[size];
}
};
}
1 change: 1 addition & 0 deletions platform/android/src/jni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ void registerNatives(JavaVM *vm) {
OfflineRegion::registerNative(env);
OfflineRegionDefinition::registerNative(env);
OfflineTilePyramidRegionDefinition::registerNative(env);
OfflineGeometryRegionDefinition::registerNative(env);
OfflineRegionError::registerNative(env);
OfflineRegionStatus::registerNative(env);

Expand Down
6 changes: 2 additions & 4 deletions platform/android/src/offline/offline_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ void OfflineManager::createOfflineRegion(jni::JNIEnv& env_,
jni::Array<jni::jbyte> metadata_,
jni::Object<CreateOfflineRegionCallback> callback_) {
// Convert

// XXX hardcoded cast for now as we only support OfflineTilePyramidRegionDefinition
auto definition = OfflineTilePyramidRegionDefinition::getDefinition(env_, jni::Object<OfflineTilePyramidRegionDefinition>(*definition_));
auto definition = OfflineRegionDefinition::getDefinition(env_, definition_);

mbgl::OfflineRegionMetadata metadata;
if (metadata_) {
Expand Down Expand Up @@ -152,7 +150,7 @@ void OfflineManager::CreateOfflineRegionCallback::onCreate(jni::JNIEnv& env,
jni::Object<FileSource> jFileSource,
jni::Object<OfflineManager::CreateOfflineRegionCallback> callback,
mbgl::optional<mbgl::OfflineRegion> region) {
//Convert the region to java peer object
// Convert the region to java peer object
auto jregion = OfflineRegion::New(env, jFileSource, std::move(*region));

// Trigger callback
Expand Down
9 changes: 8 additions & 1 deletion platform/android/src/offline/offline_region.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,14 @@ void OfflineRegion::updateOfflineRegionMetadata(jni::JNIEnv& env_, jni::Array<jn
jni::Object<OfflineRegion> OfflineRegion::New(jni::JNIEnv& env, jni::Object<FileSource> jFileSource, mbgl::OfflineRegion region) {

// Definition
auto definition = jni::Object<OfflineRegionDefinition>(*OfflineTilePyramidRegionDefinition::New(env, region.getDefinition()));
auto definition = region.getDefinition().match(
[&](const mbgl::OfflineTilePyramidRegionDefinition def) {
return jni::Object<OfflineRegionDefinition>(
*OfflineTilePyramidRegionDefinition::New(env, def));
}, [&](const mbgl::OfflineGeometryRegionDefinition def) {
return jni::Object<OfflineRegionDefinition>(
*OfflineGeometryRegionDefinition::New(env, def));
});

// Metadata
auto metadata = OfflineRegion::metadata(env, region.getMetadata());
Expand Down
Loading