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

Add MGLCircle (with radius expressed in physical units) #2167

Open
incanus opened this issue Aug 24, 2015 · 53 comments
Open

Add MGLCircle (with radius expressed in physical units) #2167

incanus opened this issue Aug 24, 2015 · 53 comments
Labels
⚠️ DO NOT MERGE Work in progress, proof of concept, or on hold feature gl-ios Google Maps parity For feature parity with the Google Maps SDK for Android or iOS iOS Mapbox Maps SDK for iOS MapKit parity For feature parity with MapKit on iOS or macOS product

Comments

@incanus
Copy link
Contributor

incanus commented Aug 24, 2015

Now that v8 has landed in 0451aca we have a Circle layer type.

@1ec5
Copy link
Contributor

1ec5 commented Aug 24, 2015

ref #1740

@cowgp
Copy link

cowgp commented Oct 27, 2015

@incanus is there a roadmap plan for when MGLCircle will be implemented? Trying to update from the old "RMMapView" based SDK and the lack of replacement for an RMCircle is blocking me.

@incanus incanus self-assigned this Oct 28, 2015
@incanus
Copy link
Contributor Author

incanus commented Oct 28, 2015

Thanks for the ping on this @cowgp. It hasn't been on the roadmap, but thinking this was relatively straightforward, I took a look today in 2167-MGLCircle.

The Cocoa side was pretty easy, but as it turns out, even though we have support for circles in the renderer and styles as of 0451aca, we don't have support for circle annotations in that API.

It's an interesting bind, since circles seems like shapes (have alpha & color) but behave like points (have a single coordinate and a scalar icon-like property radius). So the going has been a little more than I expected there, but I think I'm close.

Stay tuned — will try to push this through soon.

/cc @friedbunny @jfirebaugh

@incanus
Copy link
Contributor Author

incanus commented Oct 28, 2015

@cowgp Sorry, I'm gonna have to say we're going to push this out a ways right now — too many higher priorities. If it was as easy as I had initially guessed, with pure Cocoa API, it would have been easy to rationalize, but increasing the surface area of our annotations API right now is not ideal given the other things we're integrating them with.

Stay tuned; I'd love to get this in, but it's not a priority roadmap right now.

@incanus incanus removed their assignment Oct 28, 2015
@cowgp
Copy link

cowgp commented Oct 28, 2015

@incanus - Thanks so much for looking into it, I appreciate the effort. I do hope it can remain a priority, seems like an important feature from my perspective. That said, totally understand that this update is all new underpinnings and thus there's a lot to build up.

@incanus
Copy link
Contributor Author

incanus commented Oct 28, 2015

@cowgp Depending on if your needs are truly at runtime, you could possibly use the existing support in the style spec for rendering circles in the meantime? mapbox/mapbox-gl-js#1446 shows an example of how this looks in the style. You could also consider mutating the style JSON and custom setting it at runtime as a workaround.

@cowgp
Copy link

cowgp commented Nov 9, 2015

@incanus - I looked into the style spec you linked, but no it doesn't seem to be a viable solve for our needs. We have ~45k active users each with one or more geofences that they can set/modify at will. This is accomplished in the existing deprecated SDK by adding an annotation at the center point of the geofence and then in the layerForAnnotation delegate method returning an RMCircle with the correct radius in meters for the fence. It seems to me a clear case where an MGLCircle is necessary to have in the new SDK.

@incanus
Copy link
Contributor Author

incanus commented Nov 9, 2015

What I meant about mutating the style, though, could be a workaround here. The idea would be that you'd have a parsed representation of the style JSON (as, say, an NSMutableDictionary) to which you could add/modify items in the layers array using the circle primitive. Then, you'd send this out to JSON data with NSJSONSerialization and use it to set the styleJSON in the library.

Admittedly more work than just having MGLCircle, but an idea.

@EasyAsABC123
Copy link

Is this still not available with the new release of mapbox studio?

@incanus
Copy link
Contributor Author

incanus commented Nov 19, 2015

Nope, no further movement here just yet. Workaround is to code circle primitives into your style and/or add them to the style JSON at runtime for now.

@corydolphin
Copy link

@incanus can you please provide an example of this workaround?

@incanus
Copy link
Contributor Author

incanus commented Nov 30, 2015

Ah, yes, this would require something like the GeoJSON source in #2161 in order to modify runtime data for the circle layer(s). In that case, I would recommend instead adding UIView objects atop the map for now and modifying this routine to update each's placement using the same call as here to find the new screen center point based on circle center coordinate and using -metersPerPixelAtLatitude to determine radius on screen. It's not ideal, but a stopgap for now.

@incanus
Copy link
Contributor Author

incanus commented Dec 1, 2015

I take that back — this could also be accomplished with the existing annotations API, with the slight downside that unlike a true MGLCircle, it must be layered atop all the other map layers (and below any point annotations). However, that aside, it's pretty trivial to do. Here's a sample project that pulls a GeoJSON with points, parses it, picks the first 50, and adds point annotations using imagery generated on-the-fly with Core Graphics having random radius and color (reusing sprites efficiently where available).

simulator screen shot nov 30 2015 10 29 56 pm

/cc @friedbunny @jfirebaugh @brunoabinader @tmcw

@incanus
Copy link
Contributor Author

incanus commented Dec 1, 2015

Of course the other main hack here is that circle radii are only valid for a single zoom since they don't scale as the map does. You could combat this by pre-generating circles of varying radii for given integer zoom levels and only allowing external control to zoom to each, or else disallowing map zoom once you've added a particular image. Either way you'd be using MGLMapView.metersPerPixelAtLatitude() to determine the draw radius for the current zoom.

@cowgp
Copy link

cowgp commented Dec 1, 2015

@incanus - Thanks for showing a functional work around - helpful for sure. The performance hit when zooming is significant - and disabling zoom is not an option for me. That loops me back to your original suggestion, I haven't tried it yet, but I was about to - mutating the style that is. You've made subsequent suggestions here and a POC workaround that did not head down that path. Is there anything wrong with still pursuing circles in the style JSON?

@incanus
Copy link
Contributor Author

incanus commented Dec 2, 2015

@cowgp It should work, the trick is updating your vector tile source data rapidly enough. You would put circle primitives in your style JSON directly and reapply the mapView.styleURL to update the appearance (which itself would be a little dramatic visually, but could work for a prep, load, then reveal scenario). The trick is you can only put those circles where there are data points in the tiles, and so you would need to do something like upload your GeoJSON or other source the Mapbox backend and then reload it to the client. Or else manually hack a solution with the underlying AnnotationTile live-tile structures used for point/shape annotations, but at that point you're in the territory that I was towards building MGLCircle anyway.

@cowgp
Copy link

cowgp commented Jan 6, 2016

@incanus - I've initially implemented my work around in a similar manner to the sample you provided, but I've hit a snag and am now banging my head. using a standard MGLPointAnnotation and providing the circle as a UIImage does a fine job of simply rendering a circle... our use case is a radial geofence, which unfortunately means that the circle needs to adjust it's size correctly when the zoom level on the map changes. the head banging comes in that I have tried implementing both:
- (void)mapViewRegionIsChanging:(MGLMapView *)mapView
- (void)mapView:(MGLMapView *)mapView regionDidChangeAnimated:(BOOL)animated
as trigger points to try and force calculate/draw a new circle based on the new metersPerPixel value of the geofence annotation, but the annotation intermittently is not displayed/rendered. To prove it is not my circle drawing code, I just used a standard PNG image and it similarly would only appear intermittently.

The trick is that to get a new image displayed for the annotation, you have to somehow get - mapView: imageForAnnotation: to be called for that annotation. I first tried removing the annotation and then adding it right back, then tried removing the annotation and alloc/init-ing a new one in it's place. The later will consistently trigger imageForAnnotation: and I provide an image, whether that is a static PNG or a custom drawn circle at runtime, but as I said above, it only appears on the screen some of the time even though the method always returns something.

Do you have a better trick for forcing imageForAnnotation: to be called so I can provide resized circles?

@mb12
Copy link

mb12 commented Jan 6, 2016

@cowgp Can you model this using Polygon annotation? A polygon with 50 or so edges will very closely match a circle.

@cowgp
Copy link

cowgp commented Jan 6, 2016

@mb12 - the polygon is a much much better solve than rendering circles as images and struggling with the zoom. I went with 45 sides (evenly divisible into 360) and it's reasonably performant and it correctly scales when the map is zoomed. Thanks a bunch for the suggestion. For anyone else following along, below is my method, and what seems to be the best work around for current lack of MGLCircle.

- (MGLPolygon*)polygonCircleForCoordinate:(CLLocationCoordinate2D)coordinate withMeterRadius:(double)meterRadius
{
    NSUInteger degreesBetweenPoints = 8; //45 sides
    NSUInteger numberOfPoints = floor(360 / degreesBetweenPoints);
    double distRadians = meterRadius / 6371000.0; // earth radius in meters
    double centerLatRadians = coordinate.latitude * M_PI / 180;
    double centerLonRadians = coordinate.longitude * M_PI / 180;
    CLLocationCoordinate2D coordinates[numberOfPoints]; //array to hold all the points
    for (NSUInteger index = 0; index < numberOfPoints; index++) {
        double degrees = index * degreesBetweenPoints;
        double degreeRadians = degrees * M_PI / 180;
        double pointLatRadians = asin( sin(centerLatRadians) * cos(distRadians) + cos(centerLatRadians) * sin(distRadians) * cos(degreeRadians));
        double pointLonRadians = centerLonRadians + atan2( sin(degreeRadians) * sin(distRadians) * cos(centerLatRadians),
                                              cos(distRadians) - sin(centerLatRadians) * sin(pointLatRadians) );
        double pointLat = pointLatRadians * 180 / M_PI;
        double pointLon = pointLonRadians * 180 / M_PI;
        CLLocationCoordinate2D point = CLLocationCoordinate2DMake(pointLat, pointLon);
        coordinates[index] = point;
    }
    MGLPolygon *polygon = [MGLPolygon polygonWithCoordinates:coordinates count:numberOfPoints];
    return polygon;
}

The delegate methods for stroke color and fill color are then called for you to set your colors:
- (UIColor *)mapView:(MGLMapView *)mapView strokeColorForShapeAnnotation:(MGLShape *)annotation
- (UIColor *)mapView:(MGLMapView *)mapView fillColorForPolygonAnnotation:(MGLPolygon *)annotation

It is unfortunate however that supplying a color with less than 1 alpha value for the fill does weird things with the GL blend mode. your only option is to return the alpha in the delegate method:
- (CGFloat)mapView:(MGLMapView *)mapView alphaForShapeAnnotation:(MGLShape *)annotation
which applies the alpha to the stroke as well as the fill. In an ideal scenario, I'd be able to have a semi-transparent fill with an opaque stroke. Regardless, the above polygon hack gets me way closer to functional that I was before.

circle

@samuelleach
Copy link

Hello - Nadia from Mapbox support pointed us towards #2167 (comment) as a workaround for our need for this feature.

I'm definitely putting in a feature request for this.

Here are a couple of iOS apps that do this: It is implemented as a feature in Citymapper (approximate walking isochrone) using Google Maps and in Tile app which using Apple Maps.

@luizmello
Copy link

Thanks to @Ruben2112, here is a Kotlin conversion from your code:

private fun polygonCircleForCoordinate(location: LatLng, radius: Double): ArrayList<LatLng> {
    val degreesBetweenPoints = 8 //45 sides
    val numberOfPoints = Math.floor((360 / degreesBetweenPoints).toDouble()).toInt()
    val distRadians = radius / 6371000.0 // earth radius in meters
    val centerLatRadians = location.latitude * Math.PI / 180
    val centerLonRadians = location.longitude * Math.PI / 180
    val polygons = arrayListOf<LatLng>() //array to hold all the points
    for (index in 0 until numberOfPoints) {
        val degrees = (index * degreesBetweenPoints).toDouble()
        val degreeRadians = degrees * Math.PI / 180
        val pointLatRadians = Math.asin(Math.sin(centerLatRadians) * Math.cos(distRadians) + Math.cos(centerLatRadians) * Math.sin(distRadians) * Math.cos(degreeRadians))
        val pointLonRadians = centerLonRadians + Math.atan2(Math.sin(degreeRadians) * Math.sin(distRadians) * Math.cos(centerLatRadians),
                Math.cos(distRadians) - Math.sin(centerLatRadians) * Math.sin(pointLatRadians))
        val pointLat = pointLatRadians * 180 / Math.PI
        val pointLon = pointLonRadians * 180 / Math.PI
        val point = LatLng(pointLat, pointLon)
        polygons.add(point)
    }
    return polygons
}

Add to mapboxMap:

       mapboxMap!!.addPolygon(PolygonOptions()
                .addAll(polygonCircleForCoordinate(alertPoint.latLng, alertPoint.radius!!))
                .fillColor(colorAlertPoint)
                .strokeColor(colorAlertStroke))

@1ec5
Copy link
Contributor

1ec5 commented Oct 6, 2017

This issue is specifically about supporting georeferenced circles in the iOS SDK (and macOS SDK). #4312 tracks the same thing for the Android SDK.

@KaneCheshire
Copy link

KaneCheshire commented Jun 6, 2018

Am I correct in thinking there's still no parity with MKCircle in MapBox? Seems like a glaring omission to me.

Edit: one of the proposed solutions (#2167 (comment)) doesn't even compile anymore, and crashes when changed to use NSExpression.

@yehe01
Copy link

yehe01 commented Jul 9, 2018

#2167 (comment) Actually works. I just changed MGLStyleValue to NSExpression(forConstantValue: xxx)

@stale stale bot added the archived Archived because of inactivity label Jan 5, 2019
@stale
Copy link

stale bot commented Jan 5, 2019

This issue has been automatically detected as stale because it has not had recent activity and will be archived. Thank you for your contributions.

@stale stale bot closed this as completed Jan 5, 2019
@julianrex julianrex reopened this Jan 7, 2019
@stale
Copy link

stale bot commented Jan 7, 2019

This issue has been automatically detected as stale because it has not had recent activity and will be archived. Thank you for your contributions.

@stale stale bot closed this as completed Jan 7, 2019
@friedbunny friedbunny reopened this Jan 7, 2019
@stale stale bot removed the archived Archived because of inactivity label Jan 7, 2019
@jlubeck
Copy link

jlubeck commented Feb 7, 2019

Any news on this front? (trying to get the bot not to close it again)

@KaneCheshire
Copy link

Given it’s 3 years old, would be nice to have some sort of update on the issue for sure.

@julianrex
Copy link
Contributor

@jlubeck @KaneCheshire just wanted to assure you that this is on our list - and you should start seeing some movement on this issue soon.

@KaneCheshire
Copy link

Thanks for the update @julianrex

@jlubeck
Copy link

jlubeck commented Mar 6, 2019

Great to hear that @julianrex !

@captainbarbosa
Copy link
Contributor

captainbarbosa commented Apr 24, 2019

@jlubeck @KaneCheshire - After some internal discussions we are currently planning a major refactor to how shapes are generated to be added to a map. The current plan is to wait until that work is done before proceeding to address this issue. I know this is not ideal, but we believe the upcoming refactoring we’re working on will make adding all sorts of shapes to a map much easier for everyone in the long run. We’ll be sure to post back here as soon as we have more updates.

If you're looking for a workaround right now, one possibility would be to implement this with turf-swift. There are limitations to this approach though - this is not a true circle, but rather a an n-sided polygon (I just used the default 64 from the Turf.js documentation. Here is an example of my implementation. In addition, you can also update the shape of this circle on-the-fly using an MGLMapViewDelegate method that responds to map position changes.

@jlubeck
Copy link

jlubeck commented Apr 24, 2019

Thanks for the heads up @captainbarbosa

@zugaldia zugaldia added the ⚠️ DO NOT MERGE Work in progress, proof of concept, or on hold label Jun 11, 2019
@KaneCheshire
Copy link

How's the refactoring going? This issue will be 4 years old in just over a month :/

@chloekraw
Copy link
Contributor

All, thanks for following this ticket. Our refactor is going well, and we are nearing a go/no-go decision on merging #14534 sometime this year after we resolve the remaining considerations.

Please upvote this comment if #14534 would work for your use case and solve your needs.

Per its description, the PR implements a new MGLCircle class, analogous to MapKit’s MKCircle or the Google Map SDK’s GMSCircle, which allows the developer to add spherical caps to the map view as overlays or to a shape source as polygons.

@jmkiley jmkiley added the gl-ios label Nov 21, 2019
@jlubeck
Copy link

jlubeck commented Feb 10, 2020

Any news on this issue? Looks like it was on good track back in August... And Github is returning a 500 error on the pull request so I can't see if there is progress there... Weird thing is that is the only PR that is returning an error, the others work just fine

Thanks!

@hdmdhr
Copy link

hdmdhr commented Mar 8, 2021

Inspired by @incanus's answer in 2017, I came up with another solution, which doesn't requires usage of MGLCircleStyleLayer, but uses mapView(_:viewFor annotation:) -> MGLAnnotationView? delegate method.

Code below is written in Swift 5, using Mapbox-iOS-SDK 6.1.0, The performance seems ok on a simulator.

// Code below only covers 1 annotation use case for demo purpose, make changes if you need to support multiple annotations
class MyMapBoxViewController: UIViewController, MGLMapViewDelegate {
    
    /// Use this stored property to track my circle
    private var circleAnnotationView: MGLAnnotationView?
    private let radiusMeters: Double = 200
    private let divisor: Double = 2
    ...
    
    /// Return a clear image instead of default pin image, so the annotation will be invisible.
    /// if you do not want to hide annotation, comment this method out
    func mapView(_ mapView: MGLMapView, imageFor annotation: MGLAnnotation) -> MGLAnnotationImage? {
        let clearImage = mapView.dequeueReusableAnnotationImage(withIdentifier: "clear") ??
            .init(image: UIImage(color: .clear)!, reuseIdentifier: "clear")
        return clearImage
    }
    
    /// Return a circular `MGLAnnotationView` for the annotation, use ViewController's stored property `circleAnnotationView` to track it
    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        // use lng, lat to create a unique reuse identifier
        let reuseIdentifier = "\(annotation.coordinate.longitude)-\(annotation.coordinate.latitude)"
        
        // reuse or create a circle view around the annotation
        circleAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) ??
            CustomAnnotationView(reuseIdentifier: reuseIdentifier)
        var radiusPoints = radiusMeters / mapView.metersPerPoint(atLatitude: annotation.coordinate.latitude)
        radiusPoints /= divisor  // divide by (2) here so circle doesn't fill the entire screen, please choose your own divisor
        circleAnnotationView?.bounds = .init(origin: .zero, size: .init(width: radiusPoints, height: radiusPoints))
        circleAnnotationView?.backgroundColor = UIColor.blue.withAlphaComponent(0.5)
        
        return circleAnnotationView
    }
    
    /// When map zooms, rescale `circleAnnotationView` based on zoom level
    func mapViewRegionIsChanging(_ mapView: MGLMapView) {
        guard circleAnnotationView != nil else { return }
        var radiusPoints = radiusMeters / mapView.metersPerPoint(atLatitude: mapView.centerCoordinate.latitude)
        radiusPoints /= divisor  // divide by (2) here so circle doesn't fill the entire screen, please choose your own divisor
        circleAnnotationView?.bounds = .init(origin: .zero, size: .init(width: radiusPoints, height: radiusPoints))
    }
    
}

Some subclass and extension used in code above

class CustomAnnotationView: MGLAnnotationView {
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // Use CALayer’s corner radius to turn this view into a circle.
        layer.cornerRadius = bounds.width / 2
        layer.borderWidth = 2
        layer.borderColor = UIColor.textBlue.cgColor
    }
}

extension UIImage {

    /// Use color to init an `UIImage`
    convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
        let rect = CGRect(origin: .zero, size: size)
        UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
        color.setFill()
        UIRectFill(rect)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        guard let cgImage = image?.cgImage else { return nil }
        self.init(cgImage: cgImage)
    }
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
⚠️ DO NOT MERGE Work in progress, proof of concept, or on hold feature gl-ios Google Maps parity For feature parity with the Google Maps SDK for Android or iOS iOS Mapbox Maps SDK for iOS MapKit parity For feature parity with MapKit on iOS or macOS product
Projects
None yet
Development

Successfully merging a pull request may close this issue.