Mapbox:仅当注释在屏幕上可见时才添加注释

时间:2019-11-15 23:41:44

标签: ios swift mapbox

我的注释数据存储在Firebase的数据库中。我发现,只要注释没有自定义视图,我就可以下载10,000个注释的数据并将这些注释添加到地图中,而不会出现很大的延迟。

但是对于我的应用程序,我将需要使用自定义视图,每个注释视图都是由多个图像组成的图像。如果我使用自定义视图(即使自定义视图只是一个UIImage),该应用程序也会冻结,并最终收到错误消息“来自调试器的消息:由于内存问题而终止”。我的应用程序的最小缩放级别为15,因此用户几乎只能看到周围的物体。

我的目标是为用户大概10公里之内的所有注释下载注释数据(我将使用geohashing进行此操作,尽管这不是此问题的重点)。手机上的地图只能查看大约一公里左右的土地。

然后我要么只想

<div class="disc">

 a) add annotations that are visible on the phone 

我希望这些注释在屏幕边界之内立即可见,这样,如果用户在地图上滚动时,他们会立即看到这些注释。


我在视图控制器中拥有此委托函数,该函数确定每个注释的视图,当我对其进行注释时,添加注释会稍有延迟,但不是很多。

b) only load the views for the annotations that are visible.

示例

如果您观看此youtube视频,您会发现注解并不总是可见的,只有在您缩放或移动注解时注解才可见。 https://youtu.be/JWUFD48Od4M


MapViewController

func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
    if annotation is MGLUserLocation && mapView.userLocation != nil {
        let view = CurrentUserAnnoView(reuseIdentifier: currentUser.uid!)
        self.currentUserAnno = view
        return view
    }
    else if annotation is UserAnnotation{
        let anno = annotation as! UserAnnotation
        let auid = anno.reuseIdentifier //The anno uid
        if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: auid) {
            return annotationView
        } else {
            let annotationView = UserAnnotationView(reuseIdentifier: auid, size: CGSize(width: 45, height: 45), annotation: annotation)
            annotationView.isUserInteractionEnabled = true
            anno.view = annotationView
            return annotationView
        }
    }
    return MGLAnnotationView(annotation: annotation, reuseIdentifier: "ShouldntBeAssigned")  //Should never happen
}

用户注释

class MapViewController: UIViewController {

    @IBOutlet weak var newPostView: NewPostView!
    @IBOutlet var mapView: MGLMapView!
    var data: MapData?
    var currentUserAnno: CurrentUserAnnoView?
    var testCounter = 0

    let geoFire = GeoFire(firebaseRef: Database.database().reference().child("/users/core"))

    @IBAction func tap(_ sender: UITapGestureRecognizer) {
        self.view.endEditing(true)

    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        geoFire.setLocation(CLLocation(latitude: 37.7853889, longitude: -122.4056973), forKey: "7")
        self.startup()
    }

    func startup(){
        if CLLocationManager.isOff(){
            let popup = UIAlertController(title: "Location Services are Disabled", message: "Please enable location services in your 'Settings -> Privacy' if you want to use this app", preferredStyle: UIAlertController.Style.alert)
            popup.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: {(alert: UIAlertAction) in
                self.startup()
            }))
            popup.view.layoutIfNeeded()
            self.present(popup, animated: true, completion: nil)
        }else{
            self.mapView.userTrackingMode = .follow
            self.data = MapData(delegate: self)
        }
    }

    @IBAction func newHidea(_ sender: Any) {
        newPostView.isHidden = false
    }


}

extension MapViewController: MGLMapViewDelegate{

    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        print(testCounter)
        testCounter = testCounter + 1
        if annotation is MGLUserLocation && mapView.userLocation != nil {
            let view = CurrentUserAnnoView(reuseIdentifier: currentUser.uid!)
            self.currentUserAnno = view
            return view
        }
        else if annotation is UserAnnotation{
            let anno = annotation as! UserAnnotation
//            let auid = anno.reuseIdentifier //The anno uid
            if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "UserAnnotationView") {
                return annotationView
            } else {
                let annotationView = UserAnnotationView(reuseIdentifier: "UserAnnotationView", size: CGSize(width: 45, height: 45), annotation: annotation)
                annotationView.isUserInteractionEnabled = true
                //anno.view = annotationView
                return annotationView
            }
        }
        return MGLAnnotationView(annotation: annotation, reuseIdentifier: "ShouldntBeAssigned")  //Should never happen
    }


    func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
    /*The regular anno status box is replaced by one with buttons*/
        let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)
        let viewFrame = CGRect(origin: CGPoint(x: 0, y: -10), size: CGSize(width: 180, height: 400))
        var cView: AnnoCalloutView
        if (annotation as! UserAnnotation).status != nil{
            cView =  StatusCallout(representedObject: annotation, frame: viewFrame, annotationPoint: annotationPoint)
        }else{
            cView = ProfileCallout(representedObject: annotation, frame: viewFrame, annotationPoint: annotationPoint)
        }
        return cView
    }

    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        if (annotation is UserAnnotation) {
            return true
        }else{
            return false
        }

    }

    func mapView(_ mapView: MGLMapView, tapOnCalloutFor annotation: MGLAnnotation) {
        mapView.deselectAnnotation(annotation, animated: true)  // Hide the callout.
    }



}

//TODO: Check if there's a better method than a delegate to do this, since it's Model -> Controller
extension MapViewController: MapDataDelegate{
    func addAnnotation(_ anno: UserAnnotation) {
        self.mapView?.addAnnotation(anno)
    }
}

UserAnnotationView

class UserAnnotation: NSObject, MGLAnnotation {

    //////////Ignore these, required for MGLAnnotation//////
    var title: String?
    var subtitle: String?
    ////////////////////////////////////////////////////////

    var coordinate: CLLocationCoordinate2D
    var status: Status?{
        didSet{
            //TODO: update annotation
        }
    }
    var reuseIdentifier: String
    var avatar: Avatar
    var uid: String

    //MARK: You could assign these when the profile is viewed once, so if they view it again you have it saved.
    var uName: String?
    var bio: String?

    init(coordinate: CLLocationCoordinate2D, avatar: Avatar, reuseIdentifier: String?, uid: String) {
//    init(coordinate: CLLocationCoordinate2D, reuseIdentifier uid: String?) {
        self.coordinate = coordinate
        self.title = "None"
        self.subtitle = "None"
        self.reuseIdentifier = reuseIdentifier!
        self.uid = uid
        self.avatar = avatar
        super.init()
//        self.setAvatar(avatar: avatar)
    }

    init(coordinate: CLLocationCoordinate2D, title: String?, subtitle: String?){
        print("This shouldn't be printing")
        self.coordinate = coordinate
        self.uName = "ShouldntBeSet"
        self.title = "ShouldntBeSet"
        self.subtitle = "ShouldntBeSet"
        self.reuseIdentifier = "ShouldntBeAssigned"
        self.uid = "ShouldntBeAssigned"
        self.avatar = Avatar(withValues: [0])
    }
}

MapData

class UserAnnotationView: MGLAnnotationView {

    var anno: UserAnnotation?
    var statusView: UITextView?
    var imageView: UIImageView?
    var avatarImage: UIImage{
        let ai = AvatarImage()
        ai.update(with: (anno?.avatar.values)!)
        return ai.image!
    }


    init(reuseIdentifier: String, size: CGSize, annotation: MGLAnnotation) {
        super.init(reuseIdentifier: reuseIdentifier)
        // Prevents view from changing size when view tilted
        scalesWithViewingDistance = false
        frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        self.anno = annotation as? UserAnnotation
        self.setUpImageView(frame: frame, size: size, annotation: annotation)
        if anno?.status != nil{
            self.createStatus(status: (anno?.status?.status)!)
        }
    }

    func reuseWithDifferentAnno(annotation: UserAnnotation){
        self.anno = annotation
        self.imageView!.image = UIImage(named: "Will")
        //        let av = AvatarImage.newAvatar(values: (anno?.avatar.values)!)
//        self.imageView!.image = av.image
//        if anno?.status != nil{
//            self.createStatus(status: (anno?.status?.status)!)
//        }else{
//            if statusView != nil{
//                deleteStatus()
//            }
//        }
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    private func setUpImageView(frame: CGRect, size: CGSize, annotation: MGLAnnotation){
        self.imageView = UIImageView(frame: frame)
        self.imageView!.translatesAutoresizingMaskIntoConstraints = false
        if annotation is UserAnnotation {
//            let av = AvatarImage.newAvatar(values: (anno?.avatar.values)!)
//            self.imageView!.image = av.image
            self.imageView!.image = UIImage(named: "Will")

        }else{
            let image = UIImage()
            self.imageView!.image = image
        }
        addSubview(self.imageView!)
        imageViewConstraints(imageView: self.imageView!, size: size)
    }

    func setImage(to image: UIImage){
        self.imageView!.image = image
    }

    func createStatus(status: String){
        if (status == self.statusView?.text) && (self.subviews.contains(self.statusView!)){
            return
        }else if self.statusView != nil && self.subviews.contains(self.statusView!){
            deleteStatus()
        }
        self.statusView = UITextView()
        self.statusView!.text = status
        self.statusView!.isHidden = false
        self.adjustUITextViewHeight()
        self.statusView!.translatesAutoresizingMaskIntoConstraints = false
        self.statusView!.layer.cornerRadius = 5
        self.statusView!.textAlignment = .center
        addSubview(self.statusView!)
        textViewConstraints(textView: self.statusView!, isAbove: self.imageView!)
    }

    func deleteStatus(){
        self.statusView?.removeFromSuperview()
        self.statusView = nil
    }

    private func adjustUITextViewHeight(){

        self.statusView!.translatesAutoresizingMaskIntoConstraints = true
        self.statusView!.sizeToFit()
        self.statusView!.isScrollEnabled = false
    }

    private func imageViewConstraints(imageView: UIImageView, size: CGSize){
        let widCon = NSLayoutConstraint(item: imageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: size.width)
        let heightCon = NSLayoutConstraint(item: imageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: size.height)
        let cenCon = NSLayoutConstraint(item: imageView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0)
        NSLayoutConstraint.activate([cenCon, widCon, heightCon])
    }

    private func textViewConstraints(textView status: UITextView, isAbove imageView: UIImageView){
        let cenCon = NSLayoutConstraint(item: status, attribute: .centerX, relatedBy: .equal, toItem: imageView, attribute: .centerX, multiplier: 1, constant: 0)
        let botCon = NSLayoutConstraint(item: status, attribute: .bottom, relatedBy: .equal, toItem: imageView, attribute: .top, multiplier: 1, constant: -10)
        let widCon = NSLayoutConstraint(item: status, attribute: .width, relatedBy: .lessThanOrEqual, toItem: nil, attribute: .width, multiplier: 1, constant: 200)
        NSLayoutConstraint.activate([cenCon, botCon, widCon])
    }

}

4 个答案:

答案 0 :(得分:2)

从我的代码中可以看出,似乎您没有正确使用reuseIdentifier。

resueIdentifier和使视图出队的目的是永远不要创建更多实际可见的视图(或至少将其最小化)

您可以使用它来获取与您已经创建的类型相同的视图,但是不再可见或不再需要。因此,如果您的自定义视图具有UIImageView和标签以及某些布局,则不会再次创建它,而是重用已经创建的视图。

一旦获得可用的视图,就可以分配从注释更改为注释的属性,而无需创建另一个视图。

这意味着您下载了10,000或100,000个注释都没有关系,为地图创建的视图数永远不会大于屏幕上可见的视图数。

这样,您的代码应如下所示:

Future getCurrentUser() async {
FirebaseUser _user = await FirebaseAuth.instance.currentUser();
print("User: ${_user.displayName ?? "None"}");
return _user;}

答案 1 :(得分:0)

Mapbox具有用于更改的委托方法,并将更改区域(请选择)。

func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool)

当区域更改时,您需要将地图上的注释设置为该区域内的注释。最简单的方法似乎是将坐标转换为mapView的空间,然后检查它们是否在mapView的边界内。



let newAnnotations = allAnnotations.filter { annotation in
  let point = mapView.convert(annotation.coordinate, toPointTo: mapView)
  return mapView.bounds.contains(point)
}
if let existingAnnotations = mapView.annotations {
  mapView.removeAnnotations(existingAnnotations)
}
mapView.addAnnotations(newAnnotations)

答案 2 :(得分:0)

您所描述的问题是处理大量问题时的常见问题,而且恐怕您的方法无法为您提供帮助。如果/当用户大量使用地图(从最小缩放到最大缩放)时,尤其如此。所有要点将被下载,您将陷入同一问题。注意:如果您选择在用户缩小时删除注释,则称为聚类,这是以下解决方案提供的即用型(也就是不要重新发明轮子)

有关该主题的信息,请参见Mapbox的post,它适用于GL JS,但在您的情况下也适用相同的推理。对于iOS,mapbox发布了clustering api,但我还没有尝试过,但它似乎可以胜任。还有一个广泛的代码示例,您可以从中获得启发。出于明显的原因,我不会在这里复制它,只是最终结果的图片,因此您可以弄清楚这是否是您所需要的。

clustering datapoints

在github上也有很多代码可以做到这一点,请参见here

在地图框代码中

func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
let url = URL(fileURLWithPath: Bundle.main.path(forResource: "ports", ofType: "geojson")!)
let source = MGLShapeSource(identifier: "clusteredPorts",
url: url,
options: [.clustered: true, .clusterRadius: icon.size.width])
style.addSource(source)
[...]
}

替换行:

let url = URL(fileURLWithPath: Bundle.main.path(forResource: "ports", ofType: "geojson")!)
let source = MGLShapeSource(identifier: "clusteredPorts",
url: url, options: [.clustered: true, .clusterRadius: icon.size.width])

作者:

let source = MGLShapeSource(identifier: "my points",
shapes: shapes, options: [.clustered: true, .clusterRadius: icon.size.width])

其中shapes是[MGLShape],是从您的航点创建的。 MGLShape是使用MGLShape : NSObject <MGLAnnotation, NSSecureCoding>从“注释”派生的,请参阅here。另请参阅here了解MGLShapeSource原型。

您将必须创建一种方法,以从航点或简而言之实例化这些形状:

let source = MGLShapeSource(identifier: "my points",
shapes: self.createShapes(from:annotations), options: [.clustered: true, .clusterRadius: icon.size.width])

答案 3 :(得分:0)

如果我遵守这项权利,

我们可以忽略数据集大小的问题,因为仅显示它们附近的注释会减少数据集的大小。

对于您要显示的内容,当它们只是注释时,它们可以很好地工作,但是当您为它们提供自定义视图时,它们的显示和消失速度很慢。

通过观看视频,这似乎只是一个动画问题。视图注释正在淡入和淡出。如果您不想那种效果,请关闭该动画。

我无法从代码片段中看出可以指定该动画的位置,但是应该很容易找到它。例如,几个MLG API都具有(动画:布尔值)属性,您可以在其中指定false。