-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[ios, macos] fixes #6160: allow updating multipoint coordinates #6565
Conversation
@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. |
You can test this with a small patch such as the following: https://gist.github.com/incanus/fb2ffe1b235c9a132cd9d51dcfb369dd |
For consistency with -getCoordinates:inRange:, the new method should be called -setCoordinates:inRange:. This allows the developer to update the polyline or polygon piecemeal. |
Going to implement @1ec5's suggestion; this is better API. |
What about also having just |
if ([self isMemberOfClass:[MGLMultiPoint class]]) | ||
{ | ||
[[NSException exceptionWithName:@"MGLAbstractClassException" | ||
reason:@"MGLMultiPoint is an abstract class" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 MultiPoint
s, 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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."];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
userInfo:nil] raise]; | ||
} | ||
|
||
assert(_count > 0); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
Sorry, I mean in addition: - (void)setCoordinates:(CLLocationCoordinate2D *)coords range:(NSRange)range;
- (void)setCoordinates:(CLLocationCoordinate2D *)coords count:(NSUInteger)count; |
@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]; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👉 #6576
Updated testing patch: https://gist.github.com/incanus/d84f8079660458ff4df7865d4ba64326 |
macOS is set as of 0349363. Test patch here: https://gist.github.com/incanus/2f6dac071f2a744ae8bf55fa35ab454a |
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]]) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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:]
.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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"]; |
There was a problem hiding this comment.
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:
.
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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()
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
→ #6583
#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). |
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)
}
} |
Not being able to update a line with fewer coordinates than it previously had seems like an odd limitation. |
This adds
-[MGLMultiPoint updateCoordinates:count:]
and pipes to core, which immediately updates display ofMGLPolyline
orMGLPolygon
annotations.Several design considerations:
updateCoordinates:...
naming since merely making thecoordinates
propertyreadwrite
(effectively) as an analog to-[MGLPointAnnotation coordinate]
is not doable with C arrays ofCLLocationCoordinate2D
structs.MGLMapView
for the sake of clarity and safety.free
andmalloc
together in scope for future maintenance safety./cc @1ec5 @friedbunny @boundsj