diff --git a/README.md b/README.md index c32a64b65..34594ab54 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,28 @@ Add these lines to your Info.plist | Line | :white_check_mark: | | | Fill | | | +## Offline Sideloading + +Support for offline maps is available by *"side loading"* the required map tiles and including them in your `assets` folder. + +* Create your tiles package by following the guide available [here](https://docs.mapbox.com/ios/maps/overview/offline/). + +* Place the tiles.db file generated in step one in your assets directory and add a reference to it in your `pubspec.yml` file. + +``` + assets: + - assets/cache.db +``` + +* Call `installOfflineMapTiles` when your application starts to copy your tiles into the location where Mapbox can access them. **NOTE:** This method should be called **before** the Map widget is loaded to prevent collisions when copying the files into place. + +``` + try { + await installOfflineMapTiles(join("assets", "cache.db")); + } catch (err) { + print(err); + } +``` ## Documentation diff --git a/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java b/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java new file mode 100644 index 000000000..027dd67ea --- /dev/null +++ b/android/src/main/java/com/mapbox/mapboxgl/GlobalMethodHandler.java @@ -0,0 +1,80 @@ +package com.mapbox.mapboxgl; + +import android.content.Context; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; + +class GlobalMethodHandler implements MethodChannel.MethodCallHandler { + private static final String TAG = GlobalMethodHandler.class.getSimpleName(); + private static final String DATABASE_NAME = "mbgl-offline.db"; + private static final int BUFFER_SIZE = 1024 * 2; + private final PluginRegistry.Registrar registrar; + + GlobalMethodHandler(PluginRegistry.Registrar registrar) { + this.registrar = registrar; + } + + @Override + public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { + switch (methodCall.method) { + case "installOfflineMapTiles": + String tilesDb = methodCall.argument("tilesdb"); + String assetKey = registrar.lookupKeyForAsset(tilesDb); + installOfflineMapTiles(assetKey); + result.success(null); + break; + default: + result.notImplemented(); + break; + } + } + + private void installOfflineMapTiles(String assetKey) { + final Context context = registrar.activeContext(); + try { + File dest = new File(context.getFilesDir(), DATABASE_NAME); + copy(context.getAssets().open(assetKey), + new FileOutputStream(dest)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static int copy(InputStream input, OutputStream output) throws IOException { + final byte[] buffer = new byte[BUFFER_SIZE]; + final BufferedInputStream in = new BufferedInputStream(input, BUFFER_SIZE); + final BufferedOutputStream out = new BufferedOutputStream(output, BUFFER_SIZE); + int count = 0; + int n = 0; + try { + while ((n = in.read(buffer, 0, BUFFER_SIZE)) != -1) { + out.write(buffer, 0, n); + count += n; + } + out.flush(); + } finally { + try { + out.close(); + } catch (IOException e) { + Log.e(TAG, e.getMessage(), e); + } + try { + in.close(); + } catch (IOException e) { + Log.e(TAG, e.getMessage(), e); + } + } + return count; + } +} \ No newline at end of file diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java index db9b09db7..b9e64a4f4 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapsPlugin.java @@ -8,10 +8,11 @@ import android.app.Application; import android.os.Bundle; -import io.flutter.plugin.common.PluginRegistry.Registrar; - import java.util.concurrent.atomic.AtomicInteger; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry.Registrar; + /** * Plugin for controlling a set of MapboxMap views to be shown as overlays on top of the Flutter * view. The overlay should be hidden during transformations or while Flutter is rendering on top of @@ -35,6 +36,10 @@ public static void registerWith(Registrar registrar) { .platformViewRegistry() .registerViewFactory( "plugins.flutter.io/mapbox_gl", new MapboxMapFactory(plugin.state, registrar)); + + MethodChannel methodChannel = + new MethodChannel(registrar.messenger(), "plugins.flutter.io/mapbox_gl"); + methodChannel.setMethodCallHandler(new GlobalMethodHandler(registrar)); } @Override diff --git a/ios/Classes/SwiftMapboxGlFlutterPlugin.swift b/ios/Classes/SwiftMapboxGlFlutterPlugin.swift index fc4e93f10..9434a4238 100644 --- a/ios/Classes/SwiftMapboxGlFlutterPlugin.swift +++ b/ios/Classes/SwiftMapboxGlFlutterPlugin.swift @@ -5,5 +5,51 @@ public class SwiftMapboxGlFlutterPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let instance = MapboxMapFactory(withMessenger: registrar.messenger()) registrar.register(instance, withId: "plugins.flutter.io/mapbox_gl") + + let channel = FlutterMethodChannel(name: "plugins.flutter.io/mapbox_gl", binaryMessenger: registrar.messenger()) + + channel.setMethodCallHandler { (methodCall, result) in + switch(methodCall.method) { + case "installOfflineMapTiles": + guard let arguments = methodCall.arguments as? [String: String] else { return } + let tilesdb = arguments["tilesdb"] + let assetkey = registrar.lookupKey(forAsset: tilesdb!) + installOfflineMapTiles(key: assetkey) + result(nil) + default: + result(FlutterMethodNotImplemented) + } + } + } + + private static func getTilesUrl() -> URL { + guard var cachesUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first, + let bundleId = Bundle.main.object(forInfoDictionaryKey: kCFBundleIdentifierKey as String) as? String else { + fatalError("Could not get map tiles directory") + } + cachesUrl.appendPathComponent(bundleId) + cachesUrl.appendPathComponent(".mapbox") + cachesUrl.appendPathComponent("cache.db") + return cachesUrl + } + + // Copies the "offline" tiles to where Mapbox expects them + private static func installOfflineMapTiles(key: String) { + var tilesUrl = getTilesUrl() + let bundlePath = Bundle.main.path(forResource: key, ofType: nil) + NSLog("Cached tiles not found, copying from bundle... \(String(describing: bundlePath)) ==> \(tilesUrl)") + do { + let parentDir = tilesUrl.deletingLastPathComponent() + try FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true, attributes: nil) + if FileManager.default.fileExists(atPath: tilesUrl.path) { + try FileManager.default.removeItem(atPath: tilesUrl.path) + } + try FileManager.default.copyItem(atPath: bundlePath!, toPath: tilesUrl.path) + var resourceValues = URLResourceValues() + resourceValues.isExcludedFromBackup = true + try tilesUrl.setResourceValues(resourceValues) + } catch let error { + NSLog("Error copying bundled tiles: \(error)") + } } } diff --git a/lib/mapbox_gl.dart b/lib/mapbox_gl.dart index dcc93a342..83c0b15e5 100644 --- a/lib/mapbox_gl.dart +++ b/lib/mapbox_gl.dart @@ -23,3 +23,4 @@ part 'src/symbol.dart'; part 'src/line.dart'; part 'src/circle.dart'; part 'src/ui.dart'; +part 'src/global.dart'; \ No newline at end of file diff --git a/lib/src/global.dart b/lib/src/global.dart new file mode 100644 index 000000000..5b129d9df --- /dev/null +++ b/lib/src/global.dart @@ -0,0 +1,19 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of mapbox_gl; + +final MethodChannel _globalChannel = + MethodChannel('plugins.flutter.io/mapbox_gl'); + +/// Copy tiles db file passed in to the tiles cache directory (sideloaded) to +/// make tiles available offline. +Future installOfflineMapTiles(String tilesDb) async { + await _globalChannel.invokeMethod( + 'installOfflineMapTiles', + { + 'tilesdb': tilesDb, + }, + ); +}