Back to examples
advanced

Dynamically style interactive points

Add interactive, dynamically-styled points, derived from a web API call.

      

import Mapbox

class ViewController: UIViewController, MGLMapViewDelegate {
    var mapView: MGLMapView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let mapView = MGLMapView(frame: view.bounds)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        mapView.setCenter(CLLocationCoordinate2D(latitude: 37.090240, longitude: -95.712891), zoomLevel: 2, animated: false)

        mapView.delegate = self

        view.addSubview(mapView)

        // Add our own gesture recognizer to handle taps on our custom map features.
        mapView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleMapTap(sender:))))
        
        self.mapView = mapView
    }

    func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
        fetchPoints() { [weak self] (features) in
            self?.addItemsToMap(features: features)
        }
    }

    func addItemsToMap(features: [MGLPointFeature]) {
        // MGLMapView.style is optional, so you must guard against it not being set.
        guard let style = mapView.style else { return }

        // You can add custom UIImages to the map style.
        // These can be referenced by an MGLSymbolStyleLayer’s iconImage property.
        style.setImage(UIImage(named: "lighthouse")!, forName: "lighthouse")

        // Add the features to the map as a shape source.
        let source = MGLShapeSource(identifier: "lighthouses", features: features, options: nil)
        style.addSource(source)

        let lighthouseColor = UIColor(red: 0.08, green: 0.44, blue: 0.96, alpha: 1.0)

        // Use MGLCircleStyleLayer to represent the points with simple circles.
        // In this case, we can use style functions to gradually change properties between zoom level 2 and 7: the circle opacity from 50% to 100% and the circle radius from 2pt to 3pt.
        let circles = MGLCircleStyleLayer(identifier: "lighthouse-circles", source: source)
        circles.circleColor = MGLStyleValue(rawValue: lighthouseColor)
        circles.circleOpacity = MGLStyleValue(interpolationMode: .exponential,
            cameraStops: [2: MGLStyleValue(rawValue: 0.5),
                          7: MGLStyleValue(rawValue: 1)],
            options: nil)
        circles.circleRadius = MGLStyleValue(interpolationMode: .exponential,
            cameraStops: [2: MGLStyleValue(rawValue: 2),
                          7: MGLStyleValue(rawValue: 3)],
            options: nil)

        // Use MGLSymbolStyleLayer for more complex styling of points including custom icons and text rendering.
        let symbols = MGLSymbolStyleLayer(identifier: "lighthouse-symbols", source: source)
        symbols.iconImageName = MGLStyleValue(rawValue: "lighthouse")
        symbols.iconColor = MGLStyleValue(rawValue: lighthouseColor)
        symbols.iconScale = MGLStyleValue(rawValue: 0.5)
        symbols.iconOpacity = MGLStyleValue(interpolationMode: .exponential,
            cameraStops: [5.9: MGLStyleValue(rawValue: 0),
                          6: MGLStyleValue(rawValue: 1)],
            options: nil)
        symbols.iconHaloColor = MGLStyleValue(rawValue: UIColor.white.withAlphaComponent(0.5))
        symbols.iconHaloWidth = MGLStyleValue(rawValue: 1)
        // {name} references the "name" key in an MGLPointFeature’s attributes dictionary.
        symbols.text = MGLStyleValue(rawValue: "{name}")
        symbols.textColor = symbols.iconColor
        symbols.textFontSize = MGLStyleValue(interpolationMode: .exponential,
            cameraStops: [10: MGLStyleValue(rawValue: 10),
                          16: MGLStyleValue(rawValue: 16)],
            options: nil)
        symbols.textTranslation = MGLStyleValue(rawValue: NSValue(cgVector: CGVector(dx: 10, dy: 0)))
        symbols.textOpacity = symbols.iconOpacity
        symbols.textHaloColor = symbols.iconHaloColor
        symbols.textHaloWidth = symbols.iconHaloWidth
        symbols.textJustification = MGLStyleValue(rawValue: NSValue(mglTextJustification: .left))
        symbols.textAnchor = MGLStyleValue(rawValue: NSValue(mglTextAnchor: .left))

        style.addLayer(circles)
        style.addLayer(symbols)
    }

    // MARK: - Feature interaction
    @objc func handleMapTap(sender: UITapGestureRecognizer) {
        if sender.state == .ended {
            // Limit feature selection to just the following layer identifiers.
            let layerIdentifiers: Set = ["lighthouse-symbols", "lighthouse-circles"]

            // Try matching the exact point first.
            let point = sender.location(in: sender.view!)
            for f in mapView.visibleFeatures(at: point, styleLayerIdentifiers:layerIdentifiers)
              where f is MGLPointFeature {
                showCallout(feature: f as! MGLPointFeature)
                return
            }

            let touchCoordinate = mapView.convert(point, toCoordinateFrom: sender.view!)
            let touchLocation = CLLocation(latitude: touchCoordinate.latitude, longitude: touchCoordinate.longitude)

            // Otherwise, get all features within a rect the size of a touch (44x44).
            let touchRect = CGRect(origin: point, size: .zero).insetBy(dx: -22.0, dy: -22.0)
            let possibleFeatures = mapView.visibleFeatures(in: touchRect, styleLayerIdentifiers: Set(layerIdentifiers)).filter { $0 is MGLPointFeature }

            // Select the closest feature to the touch center.
            let closestFeatures = possibleFeatures.sorted(by: {
                return CLLocation(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude).distance(from: touchLocation) < CLLocation(latitude: $1.coordinate.latitude, longitude: $1.coordinate.longitude).distance(from: touchLocation)
            })
            if let f = closestFeatures.first {
                showCallout(feature: f as! MGLPointFeature)
                return
            }
            
            // If no features were found, deselect the selected annotation, if any.
            mapView.deselectAnnotation(mapView.selectedAnnotations.first, animated: true)
        }
    }

    func showCallout(feature: MGLPointFeature) {
        let point = MGLPointFeature()
        point.title = feature.attributes["name"] as? String
        point.coordinate = feature.coordinate

        // Selecting an feature that doesn’t already exist on the map will add a new annotation view.
        // We’ll need to use the map’s delegate methods to add an empty annotation view and remove it when we’re done selecting it.
        mapView.selectAnnotation(point, animated: true)
    }

    // MARK: - MGLMapViewDelegate

    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return true
    }

    func mapView(_ mapView: MGLMapView, didDeselect annotation: MGLAnnotation) {
        mapView.removeAnnotations([annotation])
    }

    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        // Create an empty view annotation. Set a frame to offset the callout.
        return MGLAnnotationView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
    }

    // MARK: - Data fetching and parsing

    func fetchPoints(withCompletion completion: @escaping (([MGLPointFeature]) -> Void)) {
        // Wikidata query for all lighthouses in the United States: http://tinyurl.com/zrl2jc4
        let query = "SELECT DISTINCT ?item " +
            "?itemLabel ?coor ?image " +
            "WHERE " +
            "{ " +
            "?item wdt:P31 wd:Q39715 . " +
            "?item wdt:P17 wd:Q30 . " +
            "?item wdt:P625 ?coor . " +
            "OPTIONAL { ?item wdt:P18 ?image } . " +
            "SERVICE wikibase:label { bd:serviceParam wikibase:language \"en\" } " +
            "} " +
        "ORDER BY ?itemLabel"

        let characterSet = NSMutableCharacterSet()
        characterSet.formUnion(with: CharacterSet.urlQueryAllowed)
        characterSet.removeCharacters(in: "?")
        characterSet.removeCharacters(in: "&")
        characterSet.removeCharacters(in: ":")

        let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: characterSet as CharacterSet)!

        let request = URLRequest(url: URL(string: "https://query.wikidata.org/sparql?query=\(encodedQuery)&format=json")!)

        URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
            guard let data = data else { return }
            guard let json = try? JSONSerialization.jsonObject(with: data, options:[]) as? [String: AnyObject] else { return }
            guard let results = json?["results"] as? [String: AnyObject] else { return }
            guard let items = results["bindings"] as? [[String: AnyObject]] else { return }
            DispatchQueue.main.async {
            completion(self.parseJSONItems(items: items))
            }
        }).resume()
    }

    func parseJSONItems(items: [[String: AnyObject]]) -> [MGLPointFeature] {
        var features = [MGLPointFeature]()
        for item in items {
            guard let label = item["itemLabel"] as? [String: AnyObject],
            let title = label["value"] as? String else { continue }
            guard let coor = item["coor"] as? [String: AnyObject],
            let point = coor["value"] as? String else { continue }

            let parsedPoint = point.replacingOccurrences(of: "Point(", with: "").replacingOccurrences(of: ")", with: "")
            let pointComponents = parsedPoint.components(separatedBy: " ")
            let coordinate = CLLocationCoordinate2D(latitude: Double(pointComponents[1])!, longitude: Double(pointComponents[0])!)
            let feature = MGLPointFeature()
            feature.coordinate = coordinate
            feature.title = title
            // A feature’s attributes can used by runtime styling for things like text labels.
            feature.attributes = [
                "name": title
            ]
            features.append(feature)
        }
        return features
    }
}




      
      


#import "ViewController.h"
@import Mapbox;

@interface ViewController ()<MGLMapViewDelegate>
@property (nonatomic) MGLMapView *mapView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    MGLMapView *mapView = [[MGLMapView alloc] initWithFrame:self.view.bounds];

    mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

    [mapView setCenterCoordinate:CLLocationCoordinate2DMake(37.090240, -95.712891) zoomLevel:2 animated:NO];

    mapView.delegate = self;

    // Add our own gesture recognizer to handle taps on our custom map features.
    [mapView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapTap:)]];

    [self.view addSubview:mapView];

    self.mapView = mapView;
}

- (void)mapView:(MGLMapView *)mapView didFinishLoadingStyle:(MGLStyle *)style {
    [self fetchPoints:^(NSArray *features) {
        [self addItemsToMap:features];
    }];
}

- (void)addItemsToMap:(NSArray *)features {
    // You can add custom UIImages to the map style.
    // These can be referenced by an MGLSymbolStyleLayer’s iconImage property.
    [self.mapView.style setImage:[UIImage imageNamed:@"lighthouse"] forName:@"lighthouse"];

    // Add the features to the map as a MGLShapeSource.
    MGLShapeSource *source = [[MGLShapeSource alloc] initWithIdentifier:@"lighthouses" features:features options:nil];
    [self.mapView.style addSource:source];

    UIColor *lighthouseColor = [UIColor colorWithRed:0.08 green:0.44 blue:0.96 alpha:1.0];

    // Use MGLCircleStyleLayer to represent the points with simple circles.
    // In this case, we can use style functions to gradually change properties between zoom level 2 and 7: the circle opacity from 50% to 100% and the circle radius from 2pt to 3pt.
    MGLCircleStyleLayer *circles = [[MGLCircleStyleLayer alloc] initWithIdentifier:@"lighthouse-circles" source:source];
    circles.circleColor = [MGLStyleValue valueWithRawValue:lighthouseColor];
    circles.circleOpacity = [MGLStyleValue valueWithInterpolationMode:MGLInterpolationModeExponential
        cameraStops:@{
            @2: [MGLStyleValue valueWithRawValue:@0.5],
            @7: [MGLStyleValue valueWithRawValue:@1.0]
        }
        options:@{MGLStyleFunctionOptionDefaultValue:[MGLStyleValue valueWithRawValue:@0.75]}];
                             
    circles.circleRadius = [MGLStyleValue valueWithInterpolationMode:MGLInterpolationModeInterval
        cameraStops:@{
            @2: [MGLStyleValue valueWithRawValue:@2],
            @7: [MGLStyleValue valueWithRawValue:@3]
        }
        options:@{MGLStyleFunctionOptionDefaultValue:@1}];
    
    // Use MGLSymbolStyleLayer for more complex styling of points including custom icons and text rendering.
    MGLSymbolStyleLayer *symbols = [[MGLSymbolStyleLayer alloc] initWithIdentifier:@"lighthouse-symbols" source:source];
    symbols.iconImageName = [MGLStyleValue valueWithRawValue:@"lighthouse"];
    symbols.iconScale = [MGLStyleValue valueWithRawValue:@0.5];
    symbols.iconOpacity = [MGLStyleValue valueWithInterpolationMode:MGLInterpolationModeExponential
        cameraStops:@{
            @5.9: [MGLStyleValue valueWithRawValue:@0],
            @6: [MGLStyleValue valueWithRawValue:@1],
        }
        options:nil];
    symbols.iconHaloColor = [MGLStyleValue valueWithRawValue:[[UIColor whiteColor] colorWithAlphaComponent:0.5]];
    symbols.iconHaloWidth = [MGLStyleValue valueWithRawValue:@1];
    // {name} references the "name" key in an MGLPointFeature’s attributes dictionary.
    symbols.text = [MGLStyleValue valueWithRawValue:@"{name}"];
    symbols.textColor = symbols.iconColor;
    symbols.textFontSize = [MGLStyleValue valueWithInterpolationMode:MGLInterpolationModeExponential
        cameraStops:@{
            @10: [MGLStyleValue valueWithRawValue:@10],
            @16: [MGLStyleValue valueWithRawValue:@16],
        }
        options:nil];
    symbols.textTranslation = [MGLStyleValue valueWithRawValue:[NSValue valueWithCGVector:CGVectorMake(10, 0)]];
    symbols.textOpacity = symbols.iconOpacity;
    symbols.textHaloColor = symbols.iconHaloColor;
    symbols.textHaloWidth = symbols.iconHaloWidth;
    symbols.textJustification = [MGLStyleValue valueWithRawValue:[NSValue valueWithMGLTextJustification:MGLTextJustificationLeft]];
    symbols.textAnchor = [MGLStyleValue valueWithRawValue:[NSValue valueWithMGLTextAnchor:MGLTextAnchorLeft]];

    [self.mapView.style addLayer:circles];
    [self.mapView.style addLayer:symbols];
}

#pragma mark - Feature interaction

- (void)handleMapTap:(UITapGestureRecognizer *)sender {
    if (sender.state == UIGestureRecognizerStateEnded) {
        // Limit feature selection to just the following layer identifiers.
        NSArray *layerIdentifiers = @[@"lighthouse-symbols", @"lighthouse-circles"];

        CGPoint point = [sender locationInView:sender.view];

        // Try matching the exact point first
        for (id f in [self.mapView visibleFeaturesAtPoint:point inStyleLayersWithIdentifiers:[NSSet setWithArray:layerIdentifiers]]) {
            if ([f isKindOfClass:[MGLPointFeature class]]) {
                [self showCallout:f];
                return;
            }
        }

        // Otherwise, get first features within a rect the size of a touch (44x44).
        CGRect pointRect = {point, CGSizeZero};
        CGRect touchRect = CGRectInset(pointRect, -22.0, -22.0);
        for (id f in [self.mapView visibleFeaturesInRect:touchRect inStyleLayersWithIdentifiers:[NSSet setWithArray:layerIdentifiers]]) {
            if ([f isKindOfClass:[MGLPointFeature class]]) {
                [self showCallout:f];
                return;
            }
        }

        // If no features were found, deselect the selected annotation, if any.
        [self.mapView deselectAnnotation:[[self.mapView selectedAnnotations] firstObject] animated:YES];
    }
}

- (void)showCallout:(MGLPointFeature *)feature {
    MGLPointFeature *point = [[MGLPointFeature alloc] init];
    point.title = feature.attributes[@"name"];
    point.coordinate = feature.coordinate;

    // Selecting an feature that doesn’t already exist on the map will add a new annotation view.
    // We’ll need to use the map’s delegate methods to add an empty annotation view and remove it when we’re done selecting it.
    [self.mapView selectAnnotation:point animated:YES];
}

#pragma mark - MGLMapViewDelegate

- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation {
    return YES;
}

- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id <MGLAnnotation>)annotation {
    [mapView removeAnnotation:annotation];
}

- (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id <MGLAnnotation>)annotation {
    // Create an empty view annotation. Set a frame to offset the callout.
    return [[MGLAnnotationView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
}

#pragma mark - Data fetching and parsing

- (void)fetchPoints:(void (^)(NSArray *))completion {
    // Wikidata query for all lighthouses in the United States: http://tinyurl.com/zrl2jc4
    NSString *query = @"SELECT DISTINCT ?item "
	"?itemLabel ?coor ?image "
	"WHERE "
	    "{ "
	    "?item wdt:P31 wd:Q39715 . "
	    "?item wdt:P17 wd:Q30 . "
	    "?item wdt:P625 ?coor . "
	    "OPTIONAL { ?item wdt:P18 ?image } . "
	    "SERVICE wikibase:label { bd:serviceParam wikibase:language \"en\" } "
	"} "
	"ORDER BY ?itemLabel";

    NSMutableCharacterSet *characterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
    [characterSet removeCharactersInString:@"?"];
    [characterSet removeCharactersInString:@"&"];
    [characterSet removeCharactersInString:@":"];

    NSString *encodedQuery = [query stringByAddingPercentEncodingWithAllowedCharacters:characterSet];

    NSString *urlString = [NSString stringWithFormat:@"https://query.wikidata.org/sparql?query=%@&format=json", encodedQuery];

    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:urlString] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (!data) return;

        NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        NSArray *items = json[@"results"][@"bindings"];

        if (!items) return;

        dispatch_async(dispatch_get_main_queue(), ^{
            completion([self parseJSONItems:items]);
        });
    }] resume];
}

- (NSArray *)parseJSONItems:(NSArray *)items {
    NSMutableArray *features = [NSMutableArray array];
    for (NSDictionary *item in items) {
        NSString *title = item[@"itemLabel"][@"value"];
        NSString *point = item[@"coor"][@"value"];
        if (!item || !point) continue;

        NSString *parsedPoint = [[point stringByReplacingOccurrencesOfString:@"Point(" withString:@""] stringByReplacingOccurrencesOfString:@")" withString:@""];
        NSArray *pointComponents = [parsedPoint componentsSeparatedByString:@" "];

        MGLPointFeature *feature = [[MGLPointFeature alloc] init];
        feature.coordinate = CLLocationCoordinate2DMake([pointComponents[1] doubleValue], [pointComponents[0] doubleValue]);
        feature.title = title;
        // A feature’s attributes can used by runtime styling for things like text labels.
        feature.attributes = @{
            @"name": title,
        };
        [features addObject:feature];
    }
    return features;
}

@end