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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
086297d
[ios, macos] fixes #6160: allow updating multipoint coordinates
incanus Oct 3, 2016
156071a
move to range-based point updating
incanus Oct 4, 2016
d85aec1
Merge remote-tracking branch 'origin/master' into 6160-update-darwin-…
incanus Oct 4, 2016
e7888e1
assert() -> NSAssert() for user errors
incanus Oct 4, 2016
0349363
macos support for polyline/polygon coordinates mutation
incanus Oct 4, 2016
07aeddf
update changelog for ios & macos
incanus Oct 4, 2016
6931ab4
code formatting consistency
incanus Oct 4, 2016
a48e1b7
remove bogus and unpossible array size check
incanus Oct 4, 2016
a24482a
clearer docs
incanus Oct 4, 2016
6e61b93
Merge remote-tracking branch 'origin/master' into 6160-update-darwin-…
incanus Oct 4, 2016
608115e
clarify changelog
incanus Oct 4, 2016
6e607ec
NSAssert() -> NSException for external uses
incanus Oct 4, 2016
2d46f20
simplify operations with memcpy() & avoid for loops
incanus Oct 4, 2016
8d92589
adjust exceptions for MGLMultiPoint modernity
incanus Oct 4, 2016
a2377ed
remove unneeded use check
incanus Oct 4, 2016
d240ba9
formatting & brevity
incanus Oct 4, 2016
18cd62a
remove redundant control flow
incanus Oct 4, 2016
ea0c900
pointCount KVO is dependent on coordinates
incanus Oct 4, 2016
b9c10e3
memory warning for inner pointer
incanus Oct 4, 2016
d4cb5a3
avoid unnecessary copying
incanus Oct 4, 2016
fcd5ada
disallow pointer reuse
incanus Oct 4, 2016
138ae55
rename method
incanus Oct 4, 2016
7f6049b
better check for coordinate memory reuse
incanus Oct 5, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion platform/darwin/src/MGLMultiPoint.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ NS_ASSUME_NONNULL_BEGIN
*/
@interface MGLMultiPoint : MGLShape

/** The array of coordinates associated with the shape. */
/**
The array of coordinates associated with the shape.

This C array is a pointer to a structure inside the multipoint object,
which may have a lifetime shorter than the multipoint object and will
certainly not have a longer lifetime. Therefore, you should copy the C
array if it needs to be stored outside of the memory context in which you
use this property.
*/
@property (nonatomic, readonly) CLLocationCoordinate2D *coordinates NS_RETURNS_INNER_POINTER;

/** The number of coordinates associated with the shape. (read-only) */
Expand All @@ -35,6 +43,22 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)getCoordinates:(CLLocationCoordinate2D *)coords range:(NSRange)range;

/**
Updates one or more coordinates for the shape, which will instantaneously
cause the shape to be redrawn if it is currently visible on the map.

@param range The range of points to update. The `location` field indicates the
first point you are replacing, with `0` being the first point, `1` being
the second point, and so on. The `length` field indicates the number of
points to update. You can append to an existing array of coordinates
by specifying a range with a `location` at the end of the existing array
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.
@param coords The array of coordinates defining the shape. The data in this
array is copied to the object.
*/
- (void)replaceCoordinatesInRange:(NSRange)range withCoordinates:(CLLocationCoordinate2D *)coords;

@end

NS_ASSUME_NONNULL_END
109 changes: 71 additions & 38 deletions platform/darwin/src/MGLMultiPoint.mm
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#import "MGLMultiPoint_Private.h"
#import "MGLGeometry_Private.h"
#import "MGLTypes.h"

#import <mbgl/util/geo.hpp>

mbgl::Color MGLColorObjectFromCGColorRef(CGColorRef cgColor) {
if (!cgColor) {
return { 0, 0, 0, 0 };
}
mbgl::Color MGLColorObjectFromCGColorRef(CGColorRef cgColor)
{
if (!cgColor) return { 0, 0, 0, 0 };
NSCAssert(CGColorGetNumberOfComponents(cgColor) >= 4, @"Color must have at least 4 components");
const CGFloat *components = CGColorGetComponents(cgColor);
return { (float)components[0], (float)components[1], (float)components[2], (float)components[3] };
Expand All @@ -18,28 +18,34 @@ @implementation MGLMultiPoint
MGLCoordinateBounds _bounds;
}

- (instancetype)initWithCoordinates:(CLLocationCoordinate2D *)coords
count:(NSUInteger)count
- (instancetype)initWithCoordinates:(CLLocationCoordinate2D *)coords count:(NSUInteger)count
{
self = [super init];

if (self)
{
_count = count;
_coordinates = (CLLocationCoordinate2D *)malloc(_count * sizeof(CLLocationCoordinate2D));
[self setupWithCoordinates:coords count:count];
}

mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty();
return self;
}

for (NSUInteger i = 0; i < _count; i++)
{
_coordinates[i] = coords[i];
bounds.extend(mbgl::LatLng(coords[i].latitude, coords[i].longitude));
}
- (void)setupWithCoordinates:(CLLocationCoordinate2D *)coords count:(NSUInteger)count
{
if (_coordinates) free(_coordinates);

_count = count;
_coordinates = (CLLocationCoordinate2D *)malloc(_count * sizeof(CLLocationCoordinate2D));

_bounds = MGLCoordinateBoundsFromLatLngBounds(bounds);
mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty();

for (NSUInteger i = 0; i < _count; i++)
{
_coordinates[i] = coords[i];
bounds.extend(mbgl::LatLng(coords[i].latitude, coords[i].longitude));
}

return self;
_bounds = MGLCoordinateBoundsFromLatLngBounds(bounds);
}

- (void)dealloc
Expand All @@ -49,41 +55,30 @@ - (void)dealloc

- (CLLocationCoordinate2D)coordinate
{
if ([self isMemberOfClass:[MGLMultiPoint class]])
{
[[NSException exceptionWithName:@"MGLAbstractClassException"
reason:@"MGLMultiPoint is an abstract class"
userInfo:nil] raise];
}

assert(_count > 0);

return CLLocationCoordinate2DMake(_coordinates[0].latitude, _coordinates[0].longitude);
}

- (NSUInteger)pointCount
{
if ([self isMemberOfClass:[MGLMultiPoint class]])
{
[[NSException exceptionWithName:@"MGLAbstractClassException"
reason:@"MGLMultiPoint is an abstract class"
userInfo:nil] raise];
}

return _count;
}

+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingPointCount
{
return [NSSet setWithObjects:@"coordinates", nil];
}

- (void)getCoordinates:(CLLocationCoordinate2D *)coords range:(NSRange)range
{
if ([self isMemberOfClass:[MGLMultiPoint class]])
if (range.location + range.length > _count)
{
[[NSException exceptionWithName:@"MGLAbstractClassException"
reason:@"MGLMultiPoint is an abstract class"
userInfo:nil] raise];
[NSException raise:NSRangeException
format:@"Invalid coordinate range %@ extends beyond current coordinate count of %zu",
NSStringFromRange(range), _count];
}

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

NSUInteger index = 0;

for (NSUInteger i = range.location; i < range.location + range.length; i++)
Expand All @@ -93,6 +88,43 @@ - (void)getCoordinates:(CLLocationCoordinate2D *)coords range:(NSRange)range
}
}

- (void)replaceCoordinatesInRange:(NSRange)range withCoordinates:(CLLocationCoordinate2D *)coords
{
if ((coords >= _coordinates && coords < _coordinates + _count) ||
(coords + range.length >= _coordinates && coords + range.length < _coordinates + _count))
{
[NSException raise:NSRangeException format:@"Reuse of existing coordinates array %p not supported", coords];
}
else if (range.length == 0)
{
[NSException raise:NSRangeException format:@"Empty coordinate range %@", NSStringFromRange(range)];
}
else if (range.location > _count)
{
[NSException raise:NSRangeException
format:@"Invalid range %@ for existing coordinate count %zu",
NSStringFromRange(range), _count];
}

[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.

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!

}
else
{
// appending new coordinate(s)
NSUInteger newCount = NSMaxRange(range);
CLLocationCoordinate2D *newCoordinates = (CLLocationCoordinate2D *)malloc(newCount * sizeof(CLLocationCoordinate2D));
memcpy(newCoordinates, _coordinates, fmin(_count, range.location) * sizeof(CLLocationCoordinate2D));
memcpy(newCoordinates + range.location, coords, range.length * sizeof(CLLocationCoordinate2D));
[self setupWithCoordinates:newCoordinates count:newCount];
free(newCoordinates);
}
[self didChangeValueForKey:@"coordinates"];
}

- (MGLCoordinateBounds)overlayBounds
{
return _bounds;
Expand All @@ -103,15 +135,16 @@ - (BOOL)intersectsOverlayBounds:(MGLCoordinateBounds)overlayBounds
return MGLLatLngBoundsFromCoordinateBounds(_bounds).intersects(MGLLatLngBoundsFromCoordinateBounds(overlayBounds));
}

- (mbgl::Annotation)annotationObjectWithDelegate:(__unused id <MGLMultiPointDelegate>)delegate {
- (mbgl::Annotation)annotationObjectWithDelegate:(__unused id <MGLMultiPointDelegate>)delegate
{
NSAssert(NO, @"Cannot add an annotation from an instance of %@", NSStringFromClass([self class]));
return mbgl::SymbolAnnotation({mbgl::Point<double>()});
}

- (NSString *)description
{
return [NSString stringWithFormat:@"<%@: %p; count = %lu; bounds = %@>",
NSStringFromClass([self class]), (void *)self, (unsigned long)_count, MGLStringFromCoordinateBounds(_bounds)];
NSStringFromClass([self class]), (void *)self, (unsigned long)_count, MGLStringFromCoordinateBounds(_bounds)];
}

@end
1 change: 1 addition & 0 deletions platform/ios/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT

### Annotations

* MGLPolyline annotations and the exterior coordinates of MGLPolygon annotations are now able to be mutated, part or all, and changes are displayed immediately. ([#6565](https://github.com/mapbox/mapbox-gl-native/pull/6565))
* Fixed an issue preventing MGLAnnotationView from animating when its coordinate changes. ([#6215](https://github.com/mapbox/mapbox-gl-native/pull/6215))
* Fixed an issue causing the wrong annotation view to be selected when tapping an annotation view with a center offset applied. ([#5931](https://github.com/mapbox/mapbox-gl-native/pull/5931))
* Fixed an issue that assigned annotation views to polyline and polygon annotations. ([#5770](https://github.com/mapbox/mapbox-gl-native/pull/5770))
Expand Down
29 changes: 29 additions & 0 deletions platform/ios/src/MGLMapView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1795,6 +1795,29 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
}
}
}
else if ([keyPath isEqualToString:@"coordinates"] && [object isKindOfClass:[MGLMultiPoint class]])
{
MGLMultiPoint *annotation = object;
MGLAnnotationTag annotationTag = (MGLAnnotationTag)(NSUInteger)context;
// We can get here because a subclass registered itself as an observer
// of the coordinates key path of a multipoint annotation but failed
// to handle the change. This check deters us from treating the
// subclass’s context as an annotation tag. If the context happens to
// match a valid annotation tag, the annotation will be unnecessarily
// but safely updated.
if (annotation == [self annotationWithTag:annotationTag])
{
// Update the annotation’s backing geometry to match the annotation model object.
_mbglMap->updateAnnotation(annotationTag, [annotation annotationObjectWithDelegate:self]);

// We don't current support shape multipoint annotation selection, but let's make sure
// deselection is handled just to avoid problems in the future.
if (annotationTag == _selectedAnnotationTag)
{
[self deselectAnnotation:annotation animated:YES];
}
}
}
}

+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingZoomEnabled
Expand Down Expand Up @@ -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

}
else if ( ! [annotation isKindOfClass:[MGLMultiPolyline class]]
|| ![annotation isKindOfClass:[MGLMultiPolygon class]]
Expand Down Expand Up @@ -3132,6 +3157,10 @@ - (void)removeAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations
{
[(NSObject *)annotation removeObserver:self forKeyPath:@"coordinate" context:(void *)(NSUInteger)annotationTag];
}
else if ([annotation isKindOfClass:[MGLMultiPoint class]])
{
[(NSObject *)annotation removeObserver:self forKeyPath:@"coordinates" context:(void *)(NSUInteger)annotationTag];
}

_mbglMap->removeAnnotation(annotationTag);
}
Expand Down
1 change: 1 addition & 0 deletions platform/macos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Fixed an issue with symbols not being properly stripped from the dynamic framework when built with `make xpackage SYMBOLS=NO`. ([#6531](https://github.com/mapbox/mapbox-gl-native/pull/6531))
* Added `showAnnotations:animated:` and `showAnnotations:edgePadding:animated:`, which moves the map viewport to show the specified annotations. ([#5749](https://github.com/mapbox/mapbox-gl-native/pull/5749))
* Fixed an issue where the map view’s center would always be calculated as if the view occupied the entire window. ([#6102](https://github.com/mapbox/mapbox-gl-native/pull/6102))
* MGLPolyline annotations and the exterior coordinates of MGLPolygon annotations are now able to be mutated, part or all, and changes are displayed immediately. ([#6565](https://github.com/mapbox/mapbox-gl-native/pull/6565))
* To make an MGLPolyline or MGLPolygon span the antimeridian, specify coordinates with longitudes greater than 180° or less than −180°. ([#6088](https://github.com/mapbox/mapbox-gl-native/pull/6088))
* MGLMapView’s `styleURL` property can now be set to an absolute file URL. ([#6026](https://github.com/mapbox/mapbox-gl-native/pull/6026))
* GeoJSON sources specified by the stylesheet at design time now support `cluster`, `clusterMaxZoom`, and `clusterRadius` attributes for clustering point features on the base map. ([#5724](https://github.com/mapbox/mapbox-gl-native/pull/5724))
Expand Down
23 changes: 23 additions & 0 deletions platform/macos/src/MGLMapView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,25 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(_
[self deselectAnnotation:annotation];
}
}
} else if ([keyPath isEqualToString:@"coordinates"] &&
[object isKindOfClass:[MGLMultiPoint class]]) {
MGLMultiPoint *annotation = object;
MGLAnnotationTag annotationTag = (MGLAnnotationTag)(NSUInteger)context;
// We can get here because a subclass registered itself as an observer
// of the coordinates key path of a multipoint annotation but failed
// to handle the change. This check deters us from treating the
// subclass’s context as an annotation tag. If the context happens to
// match a valid annotation tag, the annotation will be unnecessarily
// but safely updated.
if (annotation == [self annotationWithTag:annotationTag]) {
_mbglMap->updateAnnotation(annotationTag, [annotation annotationObjectWithDelegate:self]);
// We don't current support shape multipoint annotation selection, but let's make sure
// deselection is handled just to avoid problems in the future.
if (annotationTag == _selectedAnnotationTag)
{
[self deselectAnnotation:annotation];
}
}
}
}

Expand Down Expand Up @@ -1625,6 +1644,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];
} else if (![annotation isKindOfClass:[MGLMultiPolyline class]]
|| ![annotation isKindOfClass:[MGLMultiPolygon class]]
|| ![annotation isKindOfClass:[MGLShapeCollection class]]) {
Expand Down Expand Up @@ -1762,6 +1783,8 @@ - (void)removeAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations {
if ([annotation isKindOfClass:[NSObject class]] &&
![annotation isKindOfClass:[MGLMultiPoint class]]) {
[(NSObject *)annotation removeObserver:self forKeyPath:@"coordinate" context:(void *)(NSUInteger)annotationTag];
} else if ([annotation isKindOfClass:[MGLMultiPoint class]]) {
[(NSObject *)annotation removeObserver:self forKeyPath:@"coordinates" context:(void *)(NSUInteger)annotationTag];
}

_mbglMap->removeAnnotation(annotationTag);
Expand Down