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

[ios, macos] fixes #6160: allow updating multipoint coordinates #6565

Merged
merged 23 commits into from
Oct 5, 2016

Conversation

incanus
Copy link
Contributor

@incanus incanus commented Oct 3, 2016

This adds -[MGLMultiPoint updateCoordinates:count:] and pipes to core, which immediately updates display of MGLPolyline or MGLPolygon annotations.

Several design considerations:

  • Went with updateCoordinates:... naming since merely making the coordinates property readwrite (effectively) as an analog to -[MGLPointAnnotation coordinate] is not doable with C arrays of CLLocationCoordinate2D structs.
  • Was a little duplicative with internal comments and deselection in MGLMapView for the sake of clarity and safety.
  • Tried to keep free and malloc together in scope for future maintenance safety.

/cc @1ec5 @friedbunny @boundsj

@incanus incanus added feature iOS Mapbox Maps SDK for iOS performance Speed, stability, CPU usage, memory usage, or power usage macOS Mapbox Maps SDK for macOS annotations Annotations on iOS and macOS or markers on Android beta blocker Blocks the next beta release labels Oct 3, 2016
@incanus incanus added this to the ios-v3.4.0 milestone Oct 3, 2016
@incanus incanus self-assigned this Oct 3, 2016
@mention-bot
Copy link

@incanus, thanks for your PR! By analyzing the history of the files in this pull request, we identified @jfirebaugh, @1ec5 and @boundsj to be potential reviewers.

@incanus
Copy link
Contributor Author

incanus commented Oct 3, 2016

You can test this with a small patch such as the following:

https://gist.github.com/incanus/fb2ffe1b235c9a132cd9d51dcfb369dd

@1ec5
Copy link
Contributor

1ec5 commented Oct 3, 2016

For consistency with -getCoordinates:inRange:, the new method should be called -setCoordinates:inRange:. This allows the developer to update the polyline or polygon piecemeal.

@incanus
Copy link
Contributor Author

incanus commented Oct 3, 2016

Going to implement @1ec5's suggestion; this is better API.

@incanus
Copy link
Contributor Author

incanus commented Oct 3, 2016

What about also having just -setCoordinates:count:for simplicity's sake? A count is easier to form than a range.

if ([self isMemberOfClass:[MGLMultiPoint class]])
{
[[NSException exceptionWithName:@"MGLAbstractClassException"
reason:@"MGLMultiPoint is an abstract class"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MGLMultiPoint is no longer technically an abstract class, since we're overloading it to represent the GeoJSON MultiPoint concept as well. The exception here is a good idea, but we should move the declaration of this method to MGLPolyline and MGLPolygon individually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@1ec5 Do you have more context on this? I'm not sure I understand the GeoJSON concept you are referring to.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the purposes of feature querying (but not annotations yet), we use this class to represent GeoJSON MultiPoints, which are just collections of points with no vertices connecting them. #5201 tracks cleaning up the class hierarchy so that MGLPolyline and MGLPolygon no longer inherit from MGLMultiPoint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@1ec5 1ec5 Oct 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, no, once #5201 lands, this exception and the others like it won’t even be necessary anymore. But in the meantime, all I’m suggesting is that we say:

[NSException raise:@"Unsupported method" format:@"The coordinates of a member of MGLMultiPoint are read-only."];

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I finally follow @1ec5; adjusted in 8d92589.

userInfo:nil] raise];
}

assert(_count > 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For internal errors in Objective-C code, prefer NSAssert over a C++ assert. In this case, we're catching a developer error, so it should be a full-blown NSException.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was following the pattern in -getCoordinates:range::

assert(range.location + range.length <= _count);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I guess we missed that too when we overloaded this class to represent multipoints.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@1ec5
Copy link
Contributor

1ec5 commented Oct 3, 2016

What about also having just -setCoordinates:count:for simplicity's sake? A count is easier to form than a range.

With a range, the developer could potentially append to the polyline without having to rebuild the whole polyline from scratch.

Having both methods complicates the code for little gain, I think. It would optimize for the case where the developer needs to modify the beginning n coordinates frequently, which is probably a minor use case compared to modifying something in the middle or appending to the polyline. It's easy enough to call NSMakeRange with a first parameter of 0, if that's what you want.

@incanus
Copy link
Contributor Author

incanus commented Oct 3, 2016

Sorry, I mean in addition:

- (void)setCoordinates:(CLLocationCoordinate2D *)coords range:(NSRange)range;
- (void)setCoordinates:(CLLocationCoordinate2D *)coords count:(NSUInteger)count;

@friedbunny
Copy link
Contributor

@incanus Please also add a changelog entry. I imagine we should create a port-to-macOS ticket as well, for API additions there.

@@ -2840,6 +2863,8 @@ - (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations
MGLAnnotationContext context;
context.annotation = annotation;
_annotationContextsByAnnotationTag[annotationTag] = context;

[(NSObject *)annotation addObserver:self forKeyPath:@"coordinates" options:0 context:(void *)(NSUInteger)annotationTag];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For MGLPolygon, also observe changes to interiorPolygons.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this could be tail work, since it would require making the interiorPolygons property mutable or at least settable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👉 #6576

@incanus
Copy link
Contributor Author

incanus commented Oct 4, 2016

Ok, range-based updating works. Here's constantly adding the map center to a polyline as the map moves:

weird

@incanus
Copy link
Contributor Author

incanus commented Oct 4, 2016

@incanus
Copy link
Contributor Author

incanus commented Oct 4, 2016

macOS is set as of 0349363. Test patch here:

https://gist.github.com/incanus/2f6dac071f2a744ae8bf55fa35ab454a

@incanus
Copy link
Contributor Author

incanus commented Oct 4, 2016

Ok, lots of work today and ready for review again. Let's 🚢this 🦃.

@@ -93,6 +84,47 @@ - (void)getCoordinates:(CLLocationCoordinate2D *)coords range:(NSRange)range
}
}

- (void)setCoordinates:(CLLocationCoordinate2D *)coords range:(NSRange)range
{
if ([self isMemberOfClass:[MGLMultiPoint class]])
Copy link
Contributor

@1ec5 1ec5 Oct 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof, I totally forgot this method is defined on MGLMultiPoint rather than MGLPolyline and MGLPolygon individually. In that case, this check should go away too, because there’s no reason you couldn’t get and set the coordinates of an MGLMultiPoint instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


if (range.length == 0)
{
[[NSException exceptionWithName:NSRangeException
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be more concise with a call to +[NSException raise:format:].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// replacing existing coordinate(s)
memcpy(_coordinates + range.location, coords, range.length * sizeof(CLLocationCoordinate2D));
}
else if (NSMaxRange(range) > _count)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this can just be else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userInfo:nil] raise];
}

[self willChangeValueForKey:@"coordinates"];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pointCount can also change. Might be worth implementing +keyPathsForValuesAffectingPointCount:.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// appending new coordinate(s)
NSUInteger newCount = NSMaxRange(range);
CLLocationCoordinate2D *newCoordinates = (CLLocationCoordinate2D *)malloc(newCount * sizeof(CLLocationCoordinate2D));
memcpy(newCoordinates, _coordinates, _count * sizeof(CLLocationCoordinate2D));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the developer is overwriting the tail end of the array while also extending it, we should avoid unnecessary work: take the minimum of _count and range.location.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// appending new coordinate(s)
NSUInteger newCount = NSMaxRange(range);
CLLocationCoordinate2D *newCoordinates = (CLLocationCoordinate2D *)malloc(newCount * sizeof(CLLocationCoordinate2D));
memcpy(newCoordinates, _coordinates, _count * sizeof(CLLocationCoordinate2D));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the address that the coordinates property points to can change, we need a warning on that property’s documentation along the lines of -[NSString UTF8String] (another example of an NS_RETURNS_INNER_POINTER method).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (NSMaxRange(range) <= _count)
{
// replacing existing coordinate(s)
memcpy(_coordinates + range.location, coords, range.length * sizeof(CLLocationCoordinate2D));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memcpy() documentation states that the destination and source of a copy mustn’t overlap, or Bad Things™ can happen. So we should avoid calling memcpy() in the case that either coords or coords + coords.length falls between _coordinates + range.location and _coordinates + NSMaxRange(range). Since this is a bit of an edge case – you’d have to pass in the coordinates property as coords – it’s fine to punt on that by raising an exception, but we should ticket it out as tail work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll consider this handled by the #6580 refactor and the fact that fcd5ada says yet as a cue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will catch the case in which the developer tries to close a polyline:

[polyline setCoordinates:polyline.coordinates range:NSMakeRange(polyline.count, 1)];

but not if they try to have it return to the midpoint:

[polyline setCoordinates:polyline.coordinates + polyline.pointCount / 2 range:NSMakeRange(polyline.count, 1)];

I think the check should be expanded to something like:

(coords >= _coordinates + range.location && coords < pointCount + NSMaxRange(range))
|| (coords + range.length >= pointCount + range.location && coords + range.length < pointCount + NSMaxRange(range))

By the way, in that case, it would be safe to call memmove() instead of memcpy().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. I'm going to avoid memmove() right now since it'll go away with the refactor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I got this ok in 7f6049b?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check may be off by 1 since _count is not 0 indexed and, when added to the array, it points at memory somewhere off the end of the array.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the comparison is < so it is probably fine. Sorry!

and/or a `length` which extends past the end of the existing array. The array
in _`coords`_ must be equal in number to the length of the range.
*/
- (void)setCoordinates:(CLLocationCoordinate2D *)coords range:(NSRange)range;
Copy link
Contributor

@1ec5 1ec5 Oct 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something didn’t feel right to me, and now I know why: we’re only allowing the developer to overwrite or extend the multipoint but not shrink it. They can splice in but not splice out. We’re assuming that coords’ range is equal to range.length, whereas the length would have to be passed in explicitly.

I don’t think we should take the time right now to implement all the possible splicing operations, but we should try to be consistent with naming convention for KVO indexed accessors, which NSMutableArray and others adhere to. Ideally, we’d call this method -replaceCoordinatesInRange:withCoordinates:count: and have separate -setCoordinates:count, -insertCoordinates:count:atIndex:, and -removeCoordinatesInRange: methods. But for now, let’s call this method -replaceCoordinatesInRange:withCoordinates:. When we eventually implement -replaceCoordinatesInRange:withCoordinates:count:, we can deprecate this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#6583

@1ec5
Copy link
Contributor

1ec5 commented Oct 4, 2016

#6580 would eliminate the manual C memory management and make it a lot easier (think one-liners) to implement the rest of the operations laid out in #6565 (comment).

@incanus
Copy link
Contributor Author

incanus commented Oct 4, 2016

Again all good to go. 😅

@incanus incanus merged commit 105673e into master Oct 5, 2016
@incanus incanus deleted the 6160-update-darwin-multipoints branch October 5, 2016 01:42
@1ec5
Copy link
Contributor

1ec5 commented Oct 5, 2016

Thanks for the great work here @incanus! I’ve filed #6580 for the other half of this feature.

@nitrag
Copy link
Contributor

nitrag commented Nov 7, 2016

Need guidance...I'm trying to add/update a Polyline on the fly.

var recordLine: MGLPolyline? = nil
func updateRecordingLine(_ coords: [CLLocationCoordinate2D]){
    var recordLineCoords = coords
    if(self.recordLine == nil){
        //add first time
        self.recordLine = MGLPolyline(coordinates: &recordLineCoords, count: UInt(recordLineCoords.count))
        self.mapView.addAnnotation(self.recordLine!)
    }else{
        //update
        self.recordLine?.replaceCoordinates(in: NSMakeRange(0, recordLineCoords.count), withCoordinates: &recordLineCoords)
    }
}

@JesseCrocker
Copy link
Contributor

Not being able to update a line with fewer coordinates than it previously had seems like an odd limitation.

@incanus
Copy link
Contributor Author

incanus commented Nov 11, 2016

@JesseCrocker 👉 #6583

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
annotations Annotations on iOS and macOS or markers on Android beta blocker Blocks the next beta release feature iOS Mapbox Maps SDK for iOS macOS Mapbox Maps SDK for macOS performance Speed, stability, CPU usage, memory usage, or power usage
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants