Skip to content

Commit

Permalink
Added MBTiles support for iOS and Android (#2208)
Browse files Browse the repository at this point in the history
* Added MBTiles support for iOS and Android

* Added changes regarding the comments of @h3ll0w0rld123 from here: #2208 (comment)

* Added whitespaces

* Hotfix: Imported exceptions. Changed database.close()

* Hotfix: Removed the finally statemend. Resulted in always returning null

* Removed repetition of returns. Moved everything into finally statement instead

* Throwing exceptions

* Added MapView.MbTile to Readme.

Inclduded the component in the Readme

* Added example file

* Included more information in Readme

* Edited example file accodring to linter errors in Pull Request

* Edited index.d.ts according to merge conflicts in Pull Request. Had to change a few lines

* Edited example file according to linter errors in merge request

* Edited example file according to linter errors in merge request. I am starting not to like Travis...

* Edited example file according to linter errors in merge request. I am starting not to like Travis...
  • Loading branch information
Christoph authored and rborn committed Jul 19, 2018
1 parent a125488 commit 2d760e1
Show file tree
Hide file tree
Showing 30 changed files with 6,224 additions and 270 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,39 @@ For Android: LocalTile is still just overlay over original map tiles. It means t

See [OSM Wiki](https://wiki.openstreetmap.org/wiki/Category:Tile_downloading) for how to download tiles for offline usage.


#### Tile Overlay using MbTile

Tiles can be stored locally in a MBTiles database on the device using xyz tiling scheme. The locally stored tiles can be displayed as a tile overlay. This can be used for displaying maps offline. Manging many tiles in a database is especially useful if larger areas are covered by an offline map. Keeping all the files locally and "raw" on the device most likely results in bad performance as well as troublesome datahandling. Please make sure that your database follows the MBTiles [specifications](https://github.com/mapbox/mbtiles-spec). This only works with tiles stored in the [xyz scheme](https://gist.github.com/tmcw/4954720) as used by Google, OSM, MapBox, ... Make sure to include the ending .mbtiles when you pass your pathTemplate.

```jsx
import MapView from 'react-native-maps';

<MapView
region={this.state.region}
onRegionChange={this.onRegionChange}
>
<MapView.MbTile
/**
* The path template of the locally stored MBTiles database.
/storage/emulated/0/path/to/mBTilesDatabase.mbtiles
*/
pathTemplate={this.state.pathTemplate}
/**
* The size of provided local tiles (usually 256 or 512).
*/
tileSize={256}
/>
</MapView>
```

For Android: LocalTile is still just overlay over original map tiles. It means that if device is online, underlying tiles will be still downloaded. If original tiles download/display is not desirable set mapType to 'none'. For example:
```
<MapView
mapType={Platform.OS == "android" ? "none" : "standard"}
>
```

### Customizing the map style

Create the json object, or download a generated one from the [google style generator](https://mapstyle.withgoogle.com/).
Expand Down
91 changes: 91 additions & 0 deletions example/examples/MbTileOverlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { Component } from 'react';
import {
StyleSheet,
View,
Dimensions,
TouchableOpacity,
Text,
Platform,
} from 'react-native';
import MapView from 'react-native-maps';

const { width, height } = Dimensions.get('window');

const ASPECT_RATIO = width / height;
const LATITUDE = 23.736906;
const LONGITUDE = 90.397768;
const LATITUDE_DELTA = 0.022;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;


type Props = {};
export default class App extends Component<Props> {
constructor(props) {
super(props);

this.state = {
offlineMap: false,
};
}

_toggleOfflineMap = () => {
this.setState({
offlineMap: !this.state.offlineMap,
});
}

render() {
return (
<View
style={styles.container}
>
<MapView
style={styles.map}
initialRegion={{
latitude: LATITUDE,
longitude: LONGITUDE,
latitudeDelta: LATITUDE_DELTA,
longitudeDelta: LONGITUDE_DELTA,
}}
loadingEnabled
loadingIndicatorColor="#666666"
loadingBackgroundColor="#eeeeee"
mapType={Platform.OS === 'android' && this.state.offlineMap ? 'none' : 'standard'}
>
{this.state.offlineMap ?
<MapView.MbTile
pathTemplate={'Path/to/mBTilesDatabase.mbtiles'}
tileSize={256}
/> : null}
</MapView>
<TouchableOpacity
style={styles.button}
onPress={() => this._toggleOfflineMap()}
>
<Text> {this.state.offlineMap ? 'Switch to Online Map' : 'Switch to Offline Map'} </Text>
</TouchableOpacity>
</View>
);
}
}

const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
},
button: {
position: 'absolute',
bottom: 20,
backgroundColor: 'lightblue',
zIndex: 999999,
height: 50,
width: width / 2,
borderRadius: width / 2,
justifyContent: 'center',
alignItems: 'center',
},
map: {
...StyleSheet.absoluteFillObject,
},
});
11 changes: 10 additions & 1 deletion index.d.ts
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ declare module "react-native-maps" {
}

// =======================================================================
// UrlTile & LocalTile
// UrlTile, LocalTile & MbTile
// =======================================================================

export interface MapUrlTileProps extends ViewProperties {
Expand All @@ -387,6 +387,15 @@ declare module "react-native-maps" {
export class LocalTile extends React.Component<MapLocalTileProps, any> {
}

export interface MapMbTileProps extends ViewProperties {
pathTemplate: string;
tileSize: number;
zIndex?: number;
}

export class MbTile extends React.Component<MapMbTileProps, any> {
}

// =======================================================================
// Overlay
// =======================================================================
Expand Down
16 changes: 5 additions & 11 deletions index.js
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import MapView, { Animated, MAP_TYPES, ProviderPropType } from './lib/components/MapView';
import Marker from './lib/components/MapMarker.js';
import Overlay from './lib/components/MapOverlay.js';
import MapView from './lib/components/MapView';

export { default as Marker } from './lib/components/MapMarker.js';
export { default as Polyline } from './lib/components/MapPolyline.js';
export { default as Polygon } from './lib/components/MapPolygon.js';
export { default as Circle } from './lib/components/MapCircle.js';
export { default as UrlTile } from './lib/components/MapUrlTile.js';
export { default as LocalTile } from './lib/components/MapLocalTile.js';
export { default as MbTile } from './lib/components/MapMbTile.js';
export { default as Overlay } from './lib/components/MapOverlay.js';
export { default as Callout } from './lib/components/MapCallout.js';
export { default as AnimatedRegion } from './lib/components/AnimatedRegion.js';

export { Marker, Overlay };
export { Animated, MAP_TYPES, ProviderPropType };

export { Animated, ProviderPropType, MAP_TYPES } from './lib/components/MapView.js';
export const PROVIDER_GOOGLE = MapView.PROVIDER_GOOGLE;
export const PROVIDER_DEFAULT = MapView.PROVIDER_DEFAULT;

export const MarkerAnimated = Marker.Animated;
export const OverlayAnimated = Overlay.Animated;

export default MapView;

Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.airbnb.android.react.maps;

import android.content.Context;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileOverlay;
import com.google.android.gms.maps.model.TileOverlayOptions;
import com.google.android.gms.maps.model.TileProvider;

import java.io.File;

import android.os.Environment;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteDatabaseLockedException;
import android.database.Cursor;

/**
* Created by Christoph Lambio on 30/03/2018.
* Based on AirMapLocalTileManager.java
* Copyright (c) zavadpe
*/

public class AirMapMbTile extends AirMapFeature {

class AIRMapMbTileProvider implements TileProvider {
private static final int BUFFER_SIZE = 16 * 1024;
private int tileSize;
private String pathTemplate;


public AIRMapMbTileProvider(int tileSizet, String pathTemplate) {
this.tileSize = tileSizet;
this.pathTemplate = pathTemplate;
}

@Override
public Tile getTile(int x, int y, int zoom) {
byte[] image = readTileImage(x, y, zoom);
return image == null ? TileProvider.NO_TILE : new Tile(this.tileSize, this.tileSize, image);
}

public void setPathTemplate(String pathTemplate) {
this.pathTemplate = pathTemplate;
}

public void setTileSize(int tileSize) {
this.tileSize = tileSize;
}

private byte[] readTileImage(int x, int y, int zoom) {
String rawQuery = "SELECT * FROM map INNER JOIN images ON map.tile_id = images.tile_id WHERE map.zoom_level = {z} AND map.tile_column = {x} AND map.tile_row = {y}";
byte[] tile = null;
try {
SQLiteDatabase offlineDataDatabase = SQLiteDatabase.openDatabase(this.pathTemplate, null, SQLiteDatabase.OPEN_READONLY);
String query = rawQuery.replace("{x}", Integer.toString(x))
.replace("{y}", Integer.toString(y))
.replace("{z}", Integer.toString(zoom));
Cursor cursor = offlineDataDatabase.rawQuery(query, null);
if (cursor.moveToFirst()) {
tile = cursor.getBlob(5);
}
cursor.close();
offlineDataDatabase.close();
} catch (SQLiteCantOpenDatabaseException e) {
e.printStackTrace();
throw e;
} catch (SQLiteDatabaseCorruptException e) {
e.printStackTrace();
throw e;
} catch (SQLiteDatabaseLockedException e) {
e.printStackTrace();
throw e;
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
return tile;
}
}
}

private TileOverlayOptions tileOverlayOptions;
private TileOverlay tileOverlay;
private AirMapMbTile.AIRMapMbTileProvider tileProvider;

private String pathTemplate;
private float tileSize;
private float zIndex;

public AirMapMbTile(Context context) {
super(context);
}

public void setPathTemplate(String pathTemplate) {
this.pathTemplate = pathTemplate;
if (tileProvider != null) {
tileProvider.setPathTemplate(pathTemplate);
}
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}

public void setZIndex(float zIndex) {
this.zIndex = zIndex;
if (tileOverlay != null) {
tileOverlay.setZIndex(zIndex);
}
}

public void setTileSize(float tileSize) {
this.tileSize = tileSize;
if (tileProvider != null) {
tileProvider.setTileSize((int)tileSize);
}
}

public TileOverlayOptions getTileOverlayOptions() {
if (tileOverlayOptions == null) {
tileOverlayOptions = createTileOverlayOptions();
}
return tileOverlayOptions;
}

private TileOverlayOptions createTileOverlayOptions() {
TileOverlayOptions options = new TileOverlayOptions();
options.zIndex(zIndex);
this.tileProvider = new AirMapMbTile.AIRMapMbTileProvider((int)this.tileSize, this.pathTemplate);
options.tileProvider(this.tileProvider);
return options;
}

@Override
public Object getFeature() {
return tileOverlay;
}

@Override
public void addToMap(GoogleMap map) {
this.tileOverlay = map.addTileOverlay(getTileOverlayOptions());
}

@Override
public void removeFromMap(GoogleMap map) {
tileOverlay.remove();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.airbnb.android.react.maps;

import android.content.Context;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.WindowManager;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;

/**
* Created by Christoph Lambio on 30/03/2018.
* Based on AirMapLocalTileManager.java
* Copyright (c) zavadpe
*/
public class AirMapMbTileManager extends ViewGroupManager<AirMapMbTile> {
private DisplayMetrics metrics;

public AirMapMbTileManager(ReactApplicationContext reactContext) {
super();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
metrics = new DisplayMetrics();
((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getRealMetrics(metrics);
} else {
metrics = reactContext.getResources().getDisplayMetrics();
}
}

@Override
public String getName() {
return "AIRMapMbTile";
}

@Override
public AirMapMbTile createViewInstance(ThemedReactContext context) {
return new AirMapMbTile(context);
}

@ReactProp(name = "pathTemplate")
public void setPathTemplate(AirMapMbTile view, String pathTemplate) { view.setPathTemplate(pathTemplate); }

@ReactProp(name = "tileSize", defaultFloat = 256f)
public void setTileSize(AirMapMbTile view, float tileSize) {
view.setTileSize(tileSize);
}

@ReactProp(name = "zIndex", defaultFloat = -1.0f)
public void setZIndex(AirMapMbTile view, float zIndex) {
view.setZIndex(zIndex);
}

}

0 comments on commit 2d760e1

Please sign in to comment.