I have created an application that constantly reads the current user coordinates and store them in a SQLite database.
I have a map that is displayed over the whole screen.
And now I want to draw a line over the map while the user moves.
I already created all this.
The problem is that I can't make it to be a 'live'. The Overlay is not updating.
This is the logic:
In ViewDidLoad I have
...
if (nil != self.routeLine) {
[self.mapView addOverlay:self.routeLine];
}
In a function that handle each new coordinates I have:
...
NSString* coordinate = [NSString stringWithFormat:#"%f,%f", thisLocation.longitude, thisLocation.latitude];
[self.paths addObject:coordinate];
MKMapPoint northEastPoint;
MKMapPoint southWestPoint;
// create a c array of points.
MKMapPoint* pointArr = malloc(sizeof(CLLocationCoordinate2D) * self.paths.count);
for(int idx = 0; idx < self.paths.count; idx++)
{
// break the string down even further to latitude and longitude fields.
NSString* currentPointString = [self.paths objectAtIndex:idx];
NSArray* latLonArr = [currentPointString componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:#","]];
CLLocationDegrees latitude = [[latLonArr objectAtIndex:1] doubleValue];
CLLocationDegrees longitude = [[latLonArr objectAtIndex:0] doubleValue];
// create our coordinate and add it to the correct spot in the array
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(latitude, longitude);
MKMapPoint point = MKMapPointForCoordinate(coordinate);
// adjust the bounding box
// if it is the first point, just use them, since we have nothing to compare to yet.
if (idx == 0) {
northEastPoint = point;
southWestPoint = point;
}
else
{
if (point.x > northEastPoint.x)
northEastPoint.x = point.x;
if(point.y > northEastPoint.y)
northEastPoint.y = point.y;
if (point.x < southWestPoint.x)
southWestPoint.x = point.x;
if (point.y < southWestPoint.y)
southWestPoint.y = point.y;
}
pointArr[idx] = point;
}
// create the polyline based on the array of points.
self.routeLine = [MKPolyline polylineWithPoints:pointArr count:self.paths.count];
_routeRect = MKMapRectMake(southWestPoint.x, southWestPoint.y, northEastPoint.x - southWestPoint.x, northEastPoint.y - southWestPoint.y);
// clear the memory allocated earlier for the points
free(pointArr);
This is viewForOverlay delegate function:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
MKOverlayView* overlayView = nil;
if(overlay == self.routeLine)
{
//if we have not yet created an overlay view for this overlay, create it now.
if(nil == self.routeLineView)
{
self.routeLineView = [[MKPolylineView alloc] initWithPolyline:self.routeLine];
self.routeLineView.fillColor = [UIColor blueColor];
self.routeLineView.strokeColor = [UIColor blueColor];
self.routeLineView.lineWidth = 5;
}
overlayView = self.routeLineView;
}
return overlayView;
}
In viewDidLoad, the code calls addOverlay with self.routeLine which I assume is initially set to the previously-saved coordinates.
The map view adds the MKPoyline that routeLine points to into its internal list of overlays and draws the overlay.
Then, in the "function that handles each new coordinate", self.routeLine is changed to point to a new MKPolyline.
The reason the overlay view is not updated by the map is because the map view is still using the original MKPolyline that was passed to it when addOverlay was called.
Since MKPolyline itself is not mutable (it does not allow one to change the list of points/coordinates after creation), there are two main options:
Remove the existing overlay and add a new one with the updated coordinates. This is the simplest option. After setting self.routeLine to a new MKPolyline, do the following (for example):
[self.mapView removeOverlays:mapView.overlays];
self.routeLineView = nil;
[self.mapView addOverlay:self.routeLine];
The main drawback to this simple approach is that the overlay will seem to flicker if the updates are done frequently or if the overlay is large or complex.
The alternative is to create a custom overlay and overlay view that are mutable and enabling you to dynamically refresh the overlay more quickly and smoothly.
Fortunately, Apple has provided a sample app called Breadcrumb which does exactly that.
Maybe you just forgot to call [self.view setNeedsDisplay]?
UIView docs
Related
I draw path on MKMapView based on coordinates stored in SQLite on iPhone.
But now I stored 14000 coordinates (just lat/lng) in database and now when I want to display overlay path I get application crash.
My question is is there any way to optimize this code to be faster?
This is in view did load:
// ar is NSMutableArray and it is populate from database for a few seconds but code bellow cost me app crash
for(Path* p in ar)
{
self.routeLine = nil;
self.routeLineView = nil;
// while we create the route points, we will also be calculating the bounding box of our route
// so we can easily zoom in on it.
MKMapPoint northEastPoint;
MKMapPoint southWestPoint;
// create a c array of points.
MKMapPoint* pointArr = malloc(sizeof(CLLocationCoordinate2D) * ar.count);
for(int idx = 0; idx < ar.count; idx++)
{
Path *m_p = [ar objectAtIndex:idx];
CLLocationDegrees latitude = m_p.Latitude;
CLLocationDegrees longitude = m_p.Longitude;
// create our coordinate and add it to the correct spot in the array
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(latitude, longitude);
MKMapPoint point = MKMapPointForCoordinate(coordinate);
// adjust the bounding box
// if it is the first point, just use them, since we have nothing to compare to yet.
if (idx == 0) {
northEastPoint = point;
southWestPoint = point;
}
else
{
if (point.x > northEastPoint.x)
northEastPoint.x = point.x;
if(point.y > northEastPoint.y)
northEastPoint.y = point.y;
if (point.x < southWestPoint.x)
southWestPoint.x = point.x;
if (point.y < southWestPoint.y)
southWestPoint.y = point.y;
}
pointArr[idx] = point;
}
// create the polyline based on the array of points.
self.routeLine = [MKPolyline polylineWithPoints:pointArr count:ar.count];
_routeRect = MKMapRectMake(southWestPoint.x, southWestPoint.y, northEastPoint.x - southWestPoint.x, northEastPoint.y - southWestPoint.y);
// clear the memory allocated earlier for the points
free(pointArr);
[self.mapView removeOverlays: self.mapView.overlays];
// add the overlay to the map
if (nil != self.routeLine) {
[self.mapView addOverlay:self.routeLine];
}
UPDATE
ViewDidLoad:
...
[self performSelectorInBackground:#selector(drawPathInBackground) withObject:nil];
...
-(void)drawPathInBackground{
for(int idx = 0; idx < ar.count; idx++)
{ ... }
[self.mapView performSelector:#selector(addOverlay:) onThread:[NSThread mainThread] withObject:self.routeLine waitUntilDone:YES];
}
I did like this and UI not freezes.
The only thing that left is how to draw MKPolyLine on every X points?
three approaches:
don't display every point but rather combine nearby points to just one. the saving depends on your data and the necessity to display all.
if possible load the data in a background thread and display in multiple batches on the main thread. the user will practically see how the data is loaded after time.
load and display data lazily. means: only display those points which are visible on screen
Do fetching from the database and processing in a background thread.
Then reduce the number of coordinates in the path using the Douglas–Peucker algorithm:
And cache the results.
If you have array of coordinates the use this code
here routes is array of coordinates.
NSLog(#"count %d",[routes count]);
MKMapPoint* pointArr = malloc(sizeof(CLLocationCoordinate2D) * [routes count]);
for(int idx = 0; idx < [routes count]; idx++)
{
CLLocation* location = [routes objectAtIndex:idx];
CLLocationCoordinate2D workingCoordinate;
workingCoordinate.latitude=location.coordinate.latitude;
workingCoordinate.longitude=location.coordinate.longitude;
NSLog(#"loop = %f,%f",workingCoordinate.latitude, workingCoordinate.longitude);
MKMapPoint point = MKMapPointForCoordinate(workingCoordinate);
pointArr[idx] = point;
}
// create the polyline based on the array of points.
self.routeLine = [MKPolyline polylineWithPoints:pointArr count:[routes count]];
[mapView addOverlay:self.routeLine];
free(pointArr);
Hope this helps.
google has a algorithm that can encode the locations to string.
for your situation , 14000 coordinates will be encoded to a String nearly 14000 length.
then put the String into sqlite.
it will accelerate the speed to get data from DB
I have downloaded this code. I want to check whether the users current location belongs to any of the region. For that i have called following method but its always returning FALSE. Below is my code:
-(void)checkPoint
{
CLLocationCoordinate2D mapCoordinate ;
mapCoordinate.longitude=34.54664654;
mapCoordinate.latitude=-117.05646;
NSMutableArray *overlays = (NSMutableArray *)[HHLViewController usStatesAndTerritoryOverlays];
MKMapPoint mapPoint = MKMapPointForCoordinate(mapCoordinate);
for (int i=0; i<[overlays count]; i++)
{
MKOverlayView *overlayview = (MKOverlayView *)[self.stateMapView viewForOverlay:[overlays objectAtIndex:i]];
MKPolygonView *polygonView =[[MKPolygonView alloc] initWithOverlay:overlayview.overlay];
CGPoint polygonViewPoint = [polygonView pointForMapPoint:mapPoint];
BOOL mapCoordinateIsInPolygon =
CGPathContainsPoint(polygonView.path, NULL, polygonViewPoint, NO);
NSLog(#"polygon is inside %d",mapCoordinateIsInPolygon);
}
}
My problem is i want always getting NO value in mapCoordinateIsInPolygon eventhough the point belongs to that region.
I dont want to display map so any other options are also welcomed.
I've created a MKMapView with MKPolygons based on coordinates. There are multiple polygons on the map (look here for an example of what I am re-creating as an app).
What I am trying to do is when the user touches the polygon, it opens a popover view with information about the location. This information is currently stored inside a plist file with the coordinates.
What I currently have so far is that I am able to get touch event and print to the log that the polygon was touched.
The question that I have is:
Can MKPolygonView be used like an MKAnnotationView where once the user taps the pin more information pops up about that current location?
I want to do the same for the polygon view. When touched, the user would see more information about the location that is stored in the plist. If it is possible what would be the best way to get it to work?
My current code is below.
#import "outagemapViewController.h"
#import "MyAnnotation.h"
#import "WildcardGestureRecognizer.h"
#define METERS_PER_MILE 46309.344
#interface outagemapViewController ()
#end
#implementation outagemapViewController
- (void)viewDidLoad {
outages = [[NSArray alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"outages"ofType:#"plist"]];
for (NSDictionary *coloredAreas in outages) {
coordinateData = coloredAreas[#"coords"];
test = coloredAreas[#"outages"];
NSLog(#"test %#", test);
coordsLen = [coordinateData count];
NSLog(#"coords %d", coordsLen);
CLLocationCoordinate2D coords[coordsLen];
for (i=0; i < coordsLen; i++) {
NSString *lat = coordinateData[i];
NSArray *latt = [lat componentsSeparatedByString:#","];
double latitude = [[latt objectAtIndex:0] doubleValue];
double longitude = [[latt objectAtIndex:1] doubleValue];
coords[i] = CLLocationCoordinate2DMake(latitude, longitude);
}
MKPolygon* poly2 = [MKPolygon polygonWithCoordinates:coords count:coordsLen];
poly2.title=#"test";
[self.mapView addOverlay:poly2];
}
}
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay {
{
if ([overlay isKindOfClass:[MKPolygon class]])
{
MKPolygonView* aView = [[MKPolygonView alloc] initWithPolygon:(MKPolygon*)overlay];
int numbers = [test intValue];
if(numbers >= 10){
aView.fillColor = [[UIColor greenColor] colorWithAlphaComponent:0.6];
aView.strokeColor = [[UIColor greenColor] colorWithAlphaComponent:1.0];
aView.lineWidth = 3;
}else if(numbers < 10){
aView.fillColor = [[UIColor yellowColor] colorWithAlphaComponent:0.6];
aView.strokeColor = [[UIColor yellowColor] colorWithAlphaComponent:1.0];
aView.lineWidth = 3;
}
return aView;
}
return nil;
}
}
-(void)viewWillAppear:(BOOL)animated{
CLLocationCoordinate2D zoomLocation;
zoomLocation.latitude = 35.20418;
zoomLocation.longitude = -89.86862;
MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(zoomLocation, 0.5*METERS_PER_MILE, 0.5*METERS_PER_MILE);
[_mapView setRegion:viewRegion animated:YES];
WildcardGestureRecognizer * tapInterceptor = [[WildcardGestureRecognizer alloc] init];
tapInterceptor.touchesBeganCallback = ^(NSSet * touches, UIEvent * event) {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.mapView];
CLLocationCoordinate2D coord = [self.mapView convertPoint:point toCoordinateFromView:self.mapView];
MKMapPoint mapPoint = MKMapPointForCoordinate(coord);
for (id overlay in self.mapView.overlays)
{
if ([overlay isKindOfClass:[MKPolygon class]])
{
MKPolygon *poly = (MKPolygon*) overlay;
id view = [self.mapView viewForOverlay:poly];
if ([view isKindOfClass:[MKPolygonView class]])
{
MKPolygonView *polyView = (MKPolygonView*) view;
CGPoint polygonViewPoint = [polyView pointForMapPoint:mapPoint];
BOOL mapCoordinateIsInPolygon = CGPathContainsPoint(polyView.path, NULL, polygonViewPoint, NO);
if (mapCoordinateIsInPolygon) {
// debug(#"hit!");
NSLog(#"hit");
} else {
NSLog(#"miss");
}
}
}
}
};
[self.mapView addGestureRecognizer:tapInterceptor];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
Unfortunately, for overlays, there's no built-in touch-detection and callout view like there is for annotations.
You'll have to do the touch-detection manually like you're already doing (and it looks like it should work).
(Even more unfortunate here is that adding a gesture recognizer directly to the overlay view doesn't work -- you have to add it to the whole map and then check whether the touch point is in any overlay.)
For an overlay callout view, once you've detected a touch on an overlay, you can create a custom UIView and do addSubview. I suggest adding it to the map instead of the overlay view and you might be able to use the CGPoint point you are already calculating to determine the frame of the custom callout view.
You might also want to keep a ivar/property reference to the overlay callout view so it can be easily removed and re-added if the user taps on another overlay while the callout for another overlay is already displayed.
Another option which is probably easier is to create a custom UIViewController and present or push it. The specifics of showing it depend on whether you're using a navigation controller and/or storyboard.
If your app is also built for iPad, you could also show the "callout" using a UIPopoverController.
See How do I display a UIPopoverView as a annotation to the map view? (iPad) for a code example (it's with an annotation but you should be able to adapt it for the overlay).
Once you've identified which overlay was tapped, you need to display its associated data which is in your original data source (the outages array). Right now, overlays are created and added but have no reference back to the original data object (outage dictionary in outages array).
(Subclassing MKPolygon to add a custom property has issues and workarounds and creating a completely custom MKOverlay class introduces a lot of other additional work.)
For your current data source structure, a simple, quick (and somewhat crude) option is to set the overlay's title property to the index in the outages array of the outage object associated with the overlay. Since the title property is an NSString and the array index is an integer, we'll convert it to a string:
NSUInteger outageIndex = [outages indexOfObject:coloredAreas];
poly2.title = [NSString stringWithFormat:#"%d", outageIndex];
[self.mapView addOverlay:poly2];
In viewForOverlay, it looks like you're using test (which comes from an outage object) to determine the polygon's color. The value of the externally declared/set test variable will not necessarily be in sync with the overlay the delegate method is currently being called for (the map could call viewForOverlay multiple times for the same overlay and not necessarily in the order you add them). You have to retrieve the outage object based on some property of the overlay parameter. Since we are setting the overlay's title property to the outage's index:
//int numbers = [test intValue]; <-- remove this line
int outageIndex = [overlay.title intValue];
NSDictionary *outageDict = [outages objectAtIndex:outageIndex];
id outageNumbersObject = outageDict[#"outages"];
//replace id above with actual type
//can't tell from code in question whether it's NSString or NSNumber
int numbers = [outageNumbersObject intValue];
//use "numbers" to set polygon color...
Finally, when an overlay is tapped, you use the same method as in viewForOverlay to get the outage object:
if (mapCoordinateIsInPolygon) {
int outageIndex = [overlay.title intValue];
NSDictionary *outageDict = [outages objectAtIndex:outageIndex];
NSLog(#"hit, outageDict = %#", outageDict);
//show view with info from outageDict...
}
I use a MapView and I set annotations on it (purple pins) and my user location (which is a blue circle).
Because the purple pin annotations will move, I have to remove and set them new to the map.
I set it with:
CLLocationCoordinate2D coordinate;
coordinate.latitude = 49.2802;
coordinate.longitude = -123.1182;
NSUInteger count = 1;
for(int i = 0; i < 10; i++) {
CGFloat latDelta = rand()*.035/RAND_MAX - .02;
CGFloat longDelta = rand()*.03/RAND_MAX - .015;
CLLocationCoordinate2D newCoord = {coordinate.latitude+latDelta, coordinate.longitude+longDelta};
MyMapAnnotation* annotation = [[MyMapAnnotation alloc] initWithCoordinate:newCoord andID:count++];
[mapView addAnnotation:annotation];
[annotation release];
}
Before that, I do a
[mapView removeAnnotations:mapView.annotations];
but this line also remove my location with the blue point!
How could I do this without removing my location.
Thanks a lot in advance & Best Regards.
A very easy way to do this is to include this line before the loop where you remove the annotations:
self.mapView.showsUserLocation = NO;
and then after the loop, put the user location back in
self.mapView.showsUserLocation = YES;
EDIT: I just saw that you didn't loop through the locations to remove them. I'm not sure if this will work with the way you do it.
I am trying to "uncenter" the map view and remove all annotations. the reason for this is that the mapView will be used by different view controllers to display different annotations and locations. What's happening now is that i am using the "remove annotations" method and i removes them, but the map stays centered on the spot and thus when the new annotations come in, it does not move. Is there a way to reset the region and if so, what value do i reset it to. I tried nil, zero and some calculated value but "core animation" says they are all invalid. how do i get that to zoom out and back in on my new annotations?
here is some code
-(void)recenterMap {
NSArray *coordinates = [_mapView valueForKeyPath:#"annotations.coordinate"];
CLLocationCoordinate2D maxCoord = {-90.0f, -180.0f};
CLLocationCoordinate2D minCoord = {90.0f, 180.0f};
//NSLog(#"%#", [coordinates description]);
for (NSValue *value in coordinates) {
CLLocationCoordinate2D coord = {0.0f, 0.0f};
[value getValue: &coord];
if(coord.longitude > maxCoord.longitude) {
maxCoord.longitude = coord.longitude;
}
if(coord.latitude > maxCoord.latitude){
maxCoord.latitude = coord.latitude;
}
if(coord.longitude < minCoord.longitude){
minCoord.longitude = coord.longitude;
}
if(coord.latitude < minCoord.latitude){
minCoord.latitude = coord.latitude;
}
}
MKCoordinateRegion region = {{0.0f, 0.0f}, {0.0f, 0.0f}};
region.center.longitude = (minCoord.longitude + maxCoord.longitude) / 2.0;
region.center.latitude = (minCoord.latitude + maxCoord.latitude) / 2.0;
region.span.longitudeDelta = maxCoord.longitude - minCoord.longitude;
region.span.latitudeDelta = maxCoord.latitude - minCoord.latitude;
[_mapView setRegion: region animated: YES];
}
Thats the recenter methos i'm using
- (MKAnnotationView *)mapView: (MKMapView *)mapView viewForAnnotation: (id <MKAnnotation>)annotation{
MKAnnotationView *view = nil;
if(annotation != mapView.userLocation){
Annotation *schoolAnn = (Annotation*)annotation;
view = [self.mapView dequeueReusableAnnotationViewWithIdentifier:#"schoolLoc"];
if(nil == view){
view = [[[MKPinAnnotationView alloc] initWithAnnotation:schoolAnn reuseIdentifier:#"schoolLoc"]autorelease];
}
[(MKPinAnnotationView *)view setAnimatesDrop:YES];
[view setCanShowCallout:YES];
}
else {
[self recenterMap];
}
return view;
}
This "else" piece waits until the blue marble drops in then recenters the map. The problem i have with that part is that when i go back to the previous View Controller i remove all annotations, so when i come back in the User location (for some reason) does not pop back in. How do i get this to pop back in?
thanks in advance guys
Got the first part, made it zoom out again by just calling a function i made that set the region up to be over the US.
-(void)unCenterMap{
MKCoordinateRegion region2 = {{0.0f, 0.0f}, {0.0f, 0.0f}};
region2.center.longitude = (-98.151855); //coordinate for approximately the center of the US
region2.center.latitude = 39.402244;
region2.span.longitudeDelta = 3.0;
region2.span.latitudeDelta = 3.0;
[_mapView setRegion: region2 animated: YES];
}
The second part still baffles me, how do I get that location to pop in again?