Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android's OfflineManager and iOS's MGLOffline #88

Closed
SalimHFX opened this issue Jul 1, 2019 · 11 comments
Closed

Android's OfflineManager and iOS's MGLOffline #88

SalimHFX opened this issue Jul 1, 2019 · 11 comments
Labels
enhancement New feature or request flutter stale

Comments

@SalimHFX
Copy link

SalimHFX commented Jul 1, 2019

Possible duplicate of #27 ,
I just wanted to know whether there were any updates on that

@tobrun
Copy link
Collaborator

tobrun commented Nov 10, 2019

Offline side loading only atm: https://medium.com/@jamesayvaz/offline-maps-for-your-flutter-app-3ea64111b73c
Currently not on the roadmap, happy to review any PRs around this.

@tobrun tobrun added the flutter label Nov 10, 2019
@tobrun tobrun changed the title Does this plugin provide support for using Android's OfflineManager and iOS's MGLOffline natively ? Android's OfflineManager and iOS's MGLOffline Nov 10, 2019
@tobrun tobrun added the enhancement New feature or request label Nov 10, 2019
@maximumbuster
Copy link
Collaborator

I'm gonna take a stab at this cause I'd really like it for a project I'm working on

@vincekruger
Copy link
Collaborator

vincekruger commented Nov 28, 2019

@max-outway I did quick and dirty implementation earlier this year. here is my code. Might help.

/// Starts an offline map download.
///
/// The returned [Future] completes once listeners have been notified.
Future<void> downloadOfflineMap(MapDownloadOptions options) async {
  assert(options != null);
  await _channel.invokeMethod('map#startOfflineDownload', <String, dynamic>{
    'mapName': options.mapName,
    'minZoom': options.minZoom,
    'maxZoom': options.maxZoom,
    'targetBounds': options.targetBounds._toList()
  });
  notifyListeners();
}

func onMethodCall

case "map#startOfflineDownload":
    guard let arguments = methodCall.arguments as? [String: Any] else { return }
    guard let mapName = arguments["mapName"] as? String else { return }
    guard let minZoom = arguments["minZoom"] as? Double else { return }
    guard let maxZoom = arguments["maxZoom"] as? Double else { return }
    guard let targetBounds = arguments["targetBounds"] as? [[Double]] else { return }
            
    startOfflinePackDownload(mapName: mapName, minZoom: minZoom, maxZoom: maxZoom, targetBounds: MGLCoordinateBounds.fromArray(targetBounds))
// MARK: - MGLOfflinePack Start

func startOfflinePackDownload(mapName: String, minZoom: Double, maxZoom: Double, targetBounds: MGLCoordinateBounds) {
    // Create a region that includes the current viewport and any tiles needed to view it when zoomed further in.
    // Because tile count grows exponentially with the maximum zoom level, you should be conservative with your `toZoomLevel` setting.
    let region = MGLTilePyramidOfflineRegion(styleURL: mapView.styleURL,
                                                bounds: targetBounds,
                                                fromZoomLevel: minZoom,
                                                toZoomLevel: maxZoom)
    
    // Store some data for identification purposes alongside the downloaded resources.
    let userInfo = ["name": mapName]
    let context = NSKeyedArchiver.archivedData(withRootObject: userInfo)
    
    // Create and register an offline pack with the shared offline storage object.
    MGLOfflineStorage.shared.addPack(for: region, withContext: context) { (pack, error) in
        guard error == nil else {
            // The pack couldn’t be created for some reason.
            print("Map Download Error: \(error?.localizedDescription ?? "unknown error")")
            return
        }

        // Start downloading.
        pack!.resume()
    }
}

// MARK: - MGLOfflinePack notification handlers

@objc func offlinePackProgressDidChange(notification: NSNotification) {
    // Get the offline pack this notification is regarding,
    // and the associated user info for the pack; in this case, `name = My Offline Pack`
    if let pack = notification.object as? MGLOfflinePack,
        let userInfo = NSKeyedUnarchiver.unarchiveObject(with: pack.context) as? [String: String] {
        let progress = pack.progress
        
        // or notification.userInfo![MGLOfflinePackProgressUserInfoKey]!.MGLOfflinePackProgressValue
        let completedResources = progress.countOfResourcesCompleted
        let expectedResources = progress.countOfResourcesExpected
        
        // Calculate current progress percentage.
        let progressPercentage = Float(completedResources) / Float(expectedResources)
        
        mapDownloadProgressDidChange?(progressPercentage)
        
        // todo return this percentage on a listener
        
        // If this pack has finished, print its size and resource count.
        if completedResources == expectedResources {
            
//                let byteCount = ByteCountFormatter.string(fromByteCount: Int64(pack.progress.countOfBytesCompleted), countStyle: ByteCountFormatter.CountStyle.memory)
            //print("Offline pack “\(userInfo["name"] ?? "unknown")” completed: \(byteCount), \(completedResources) resources")
        } else {
            // Otherwise, print download/verification progress.
            //print("Offline pack “\(userInfo["name"] ?? "unknown")” has \(completedResources) of \(expectedResources) resources — \(progressPercentage * 100)%.")
        }
    }
}

@objc func offlinePackDidReceiveError(notification: NSNotification) {
    if let pack = notification.object as? MGLOfflinePack,
        let userInfo = NSKeyedUnarchiver.unarchiveObject(with: pack.context) as? [String: String],
        let error = notification.userInfo?[MGLOfflinePackUserInfoKey.error] as? NSError {
        print("Offline pack “\(userInfo["name"] ?? "unknown")” received error: \(error.localizedFailureReason ?? "unknown error")")
    }
}

@objc func offlinePackDidReceiveMaximumAllowedMapboxTiles(notification: NSNotification) {
    if let pack = notification.object as? MGLOfflinePack,
        let userInfo = NSKeyedUnarchiver.unarchiveObject(with: pack.context) as? [String: String],
        let maximumCount = (notification.userInfo?[MGLOfflinePackUserInfoKey.maximumCount] as AnyObject).uint64Value {
        print("Offline pack “\(userInfo["name"] ?? "unknown")” reached limit of \(maximumCount) tiles.")
    }
}

@tobrun
Copy link
Collaborator

tobrun commented Nov 28, 2019

@max-outway sounds great! @vincekruger thanks for sharing code snippets!

@maximumbuster
Copy link
Collaborator

maximumbuster commented Jan 10, 2020

Sweet, this obviously still needs a bit of work but does it look on the right track? git diff

Main questions:

  • Are create region, list regions and delete region the only methods we need?
  • Is keeping a reference to the result object the best way to return for pack creation (on ios) since notifications are required to track the creation process?
  • Since packs are removed using the reference object which flutter doesn't know about, would simply comparing the regions for equality (zoom, bounds, etc.) be the best way to delete regions?

Thanks for any insights you guys have!

@philiplindberg
Copy link
Contributor

@maximumbuster great work so far! I really need offline support for an app I'm working on too, so would love to see this implemented.

@m0nac0
Copy link
Collaborator

m0nac0 commented May 14, 2020

@maximumbuster I just came across your offline implementation and I think this would be a great feature.
These are a few things that came to my mind when looking through your code (which in general looks really good already btw):

  • I think those 3 methods should be fine.
  • Regarding deletion: offline regions seem to support custom metadata (byte[] metadata on android and Data context on iOS) , so we could probably use that to generate a random id for every region, return it to flutter in the OfflineRegionOptions and use that id to refer to the region later on like we do with symbols.
  • For keeping the reference to the result object on iOS: I could see this becoming an issue if someone starts to download region A and then immediately also region B, before region A has finished downloading. If we generate random ids as mentioned above we could replace downloadResult with a downloadResults [id: FlutterResult] dictionary and lookup the corresponding result in the callback.
  • Also could we pass back the OfflineRegionOptions in the result when the region has been successfully created?

@maximumbuster
Copy link
Collaborator

@m0nac0 great suggestions! I'll make those changes and put up a PR

@m0nac0 m0nac0 mentioned this issue May 22, 2020
@hafiz703
Copy link

Ah wanted to submit a PR until i stumbled on this. Let me know if I should still, I've already done an offline manager for my own project - offline downloading, listing ,deleting downloaded maps and changing tile download limits and tested on both iOS and Android

@maximumbuster
Copy link
Collaborator

@hafiz703 if you already have this completed feel free to pr!

@stale
Copy link

stale bot commented Jan 14, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Jan 14, 2022
@stale stale bot closed this as completed Jan 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request flutter stale
Projects
None yet
Development

No branches or pull requests

7 participants