diff options
-rw-r--r-- | include/mbgl/map/camera.hpp | 4 | ||||
-rw-r--r-- | platform/ios/app/MBXViewController.m | 77 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 19 | ||||
-rw-r--r-- | src/mbgl/layout/symbol_projection.cpp | 2 | ||||
-rw-r--r-- | src/mbgl/map/transform.cpp | 76 | ||||
-rw-r--r-- | src/mbgl/map/transform.hpp | 5 | ||||
-rw-r--r-- | src/mbgl/map/transform_state.cpp | 42 | ||||
-rw-r--r-- | src/mbgl/map/transform_state.hpp | 6 | ||||
-rw-r--r-- | src/mbgl/programs/symbol_program.cpp | 2 | ||||
-rw-r--r-- | src/mbgl/text/collision_index.cpp | 17 | ||||
-rw-r--r-- | test/api/annotations.test.cpp | 67 | ||||
-rw-r--r-- | test/fixtures/local_glyphs/no_local_with_content_insets/expected.png | bin | 0 -> 8428 bytes | |||
-rw-r--r-- | test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png | bin | 0 -> 7263 bytes | |||
-rw-r--r-- | test/map/transform.test.cpp | 45 | ||||
-rw-r--r-- | test/text/local_glyph_rasterizer.test.cpp | 50 |
15 files changed, 311 insertions, 101 deletions
diff --git a/include/mbgl/map/camera.hpp b/include/mbgl/map/camera.hpp index e3d6677c0a..c8b665fc16 100644 --- a/include/mbgl/map/camera.hpp +++ b/include/mbgl/map/camera.hpp @@ -16,7 +16,7 @@ namespace mbgl { */ struct CameraOptions { CameraOptions& withCenter(const optional<LatLng>& o) { center = o; return *this; } - CameraOptions& withPadding(const EdgeInsets& p) { padding = p; return *this; } + CameraOptions& withPadding(const optional<EdgeInsets>& p) { padding = p; return *this; } CameraOptions& withAnchor(const optional<ScreenCoordinate>& o) { anchor = o; return *this; } CameraOptions& withZoom(const optional<double>& o) { zoom = o; return *this; } CameraOptions& withBearing(const optional<double>& o) { bearing = o; return *this; } @@ -27,7 +27,7 @@ struct CameraOptions { /** Padding around the interior of the view that affects the frame of reference for `center`. */ - EdgeInsets padding; + optional<EdgeInsets> padding; /** Point of reference for `zoom` and `angle`, assuming an origin at the top-left corner of the view. */ diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index 3335606f98..567ca151f7 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -105,6 +105,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { MBXSettingsMiscellaneousShowSnapshots, MBXSettingsMiscellaneousMissingIcon, MBXSettingsMiscellaneousShouldLimitCameraChanges, + MBXSettingsMiscellaneousSetContentInsets, MBXSettingsMiscellaneousShowCustomLocationManager, MBXSettingsMiscellaneousOrnamentsPlacement, MBXSettingsMiscellaneousPrintLogFile, @@ -209,6 +210,7 @@ CLLocationCoordinate2D randomWorldCoordinate() { @property (nonatomic) BOOL shouldLimitCameraChanges; @property (nonatomic) BOOL randomWalk; @property (nonatomic) NSMutableArray<UIWindow *> *helperWindows; +@property (nonatomic) NSMutableArray<UIView *> *contentInsetsOverlays; @end @@ -219,6 +221,8 @@ CLLocationCoordinate2D randomWorldCoordinate() { @implementation MBXViewController { BOOL _isTouringWorld; + BOOL _contentInsetsEnabled; + UIEdgeInsets _originalContentInsets; } #pragma mark - Setup & Teardown @@ -475,6 +479,7 @@ CLLocationCoordinate2D randomWorldCoordinate() { @"Show Snapshots", @"Missing Icon", [NSString stringWithFormat:@"%@ Camera Changes", (_shouldLimitCameraChanges ? @"Unlimit" : @"Limit")], + [NSString stringWithFormat:@"Turn %@ Content Insets", (_contentInsetsEnabled ? @"Off" : @"On")], @"View Route Simulation", @"Ornaments Placement", ]]; @@ -739,6 +744,61 @@ CLLocationCoordinate2D randomWorldCoordinate() { } break; } + case MBXSettingsMiscellaneousSetContentInsets: + { + if (!_contentInsetsEnabled) { + _originalContentInsets = [self.mapView contentInset]; + } + _contentInsetsEnabled = !_contentInsetsEnabled; + self.automaticallyAdjustsScrollViewInsets = !_contentInsetsEnabled; + UIEdgeInsets contentInsets = self.mapView.bounds.size.width > self.mapView.bounds.size.height + ? UIEdgeInsetsMake(_originalContentInsets.top, 0.5 * self.mapView.bounds.size.width, _originalContentInsets.bottom, 0.0) + : UIEdgeInsetsMake(0.25 * self.mapView.bounds.size.height, 0.0, _originalContentInsets.bottom, 0.25 * self.mapView.bounds.size.width); + if (_contentInsetsEnabled) { + if (!self.contentInsetsOverlays) + self.contentInsetsOverlays = [NSMutableArray array]; + if (![self.contentInsetsOverlays count]) { + UIView *view = [[UIView alloc]initWithFrame:CGRectMake(0, 0, self.mapView.bounds.size.width, contentInsets.top)]; + view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5]; + [self.contentInsetsOverlays addObject:view]; + [self.view addSubview:view]; + view = [[UIView alloc]initWithFrame:CGRectMake(0, 0, contentInsets.left, self.mapView.bounds.size.height)]; + view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5]; + [self.contentInsetsOverlays addObject:view]; + [self.view addSubview:view]; + view = [[UIView alloc]initWithFrame:CGRectMake(self.mapView.bounds.size.width - contentInsets.right, 0, contentInsets.right, self.mapView.bounds.size.height)]; + view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5]; + [self.contentInsetsOverlays addObject:view]; + [self.view addSubview:view]; + view = [[UIView alloc]initWithFrame:CGRectMake(0, self.mapView.bounds.size.height - contentInsets.bottom, self.mapView.bounds.size.width, self.mapView.bounds.size.height)]; + view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5]; + [self.contentInsetsOverlays addObject:view]; + [self.view addSubview:view]; + } + [self.view bringSubviewToFront:self.contentInsetsOverlays[0]]; + [self.view bringSubviewToFront:self.contentInsetsOverlays[1]]; + [self.view bringSubviewToFront:self.contentInsetsOverlays[2]]; + [self.view bringSubviewToFront:self.contentInsetsOverlays[3]]; + + // Denver streets parallel to cardinal directions help illustrate + // viewport center offset when edge insets are set. + MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:CLLocationCoordinate2DMake(39.72707, -104.9986) + acrossDistance:100 + pitch:60 + heading:0]; + __weak typeof(self) weakSelf = self; + [self.mapView setCamera:camera withDuration:0.3 animationTimingFunction:nil completionHandler:^{ + [weakSelf.mapView setContentInset:contentInsets animated:TRUE]; + }]; + } else { + [self.view sendSubviewToBack:self.contentInsetsOverlays[0]]; + [self.view sendSubviewToBack:self.contentInsetsOverlays[1]]; + [self.view sendSubviewToBack:self.contentInsetsOverlays[2]]; + [self.view sendSubviewToBack:self.contentInsetsOverlays[3]]; + [self.mapView setContentInset:_originalContentInsets animated:TRUE]; + } + break; + } case MBXSettingsMiscellaneousOrnamentsPlacement: { MBXOrnamentsViewController *ornamentsViewController = [[MBXOrnamentsViewController alloc] init]; @@ -2005,6 +2065,23 @@ CLLocationCoordinate2D randomWorldCoordinate() { [sender setAccessibilityValue:nextAccessibilityValue]; } +#pragma mark - UIViewDelegate + +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator +{ + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + if (_contentInsetsEnabled) + { + _contentInsetsEnabled = NO; + self.automaticallyAdjustsScrollViewInsets = YES; + [self.mapView setContentInset:UIEdgeInsetsZero]; + } + while (self.contentInsetsOverlays && [self.contentInsetsOverlays count]) { + [[self.contentInsetsOverlays lastObject] removeFromSuperview]; + [self.contentInsetsOverlays removeLastObject]; + } +} + #pragma mark - MGLMapViewDelegate - (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id<MGLAnnotation>)annotation diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index fa49a2f280..bc4900ace9 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -1076,20 +1076,15 @@ public: return; } - // After adjusting the content inset, move the center coordinate from the - // old frame of reference to the new one represented by the newly set - // content inset. - CLLocationCoordinate2D oldCenter = self.centerCoordinate; - - _contentInset = contentInset; - if (self.userTrackingMode == MGLUserTrackingModeNone) { // Don’t call -setCenterCoordinate:, which resets the user tracking mode. - [self _setCenterCoordinate:oldCenter animated:animated]; + [self _setCenterCoordinate:self.centerCoordinate edgePadding:contentInset zoomLevel:self.zoomLevel direction:self.direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:NULL]; + _contentInset = contentInset; } else { + _contentInset = contentInset; [self didUpdateLocationWithUserTrackingAnimated:animated]; } @@ -3313,10 +3308,6 @@ public: [self _setCenterCoordinate:centerCoordinate edgePadding:self.contentInset zoomLevel:zoomLevel direction:direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:completion]; } -- (void)_setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate animated:(BOOL)animated { - [self _setCenterCoordinate:centerCoordinate edgePadding:self.contentInset zoomLevel:self.zoomLevel direction:self.direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:NULL]; -} - - (void)_setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate edgePadding:(UIEdgeInsets)insets zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion { if (!_mbglMap) @@ -3356,7 +3347,7 @@ public: } MGLMapCamera *camera = [self cameraForCameraOptions:cameraOptions]; - if ([self.camera isEqualToMapCamera:camera]) + if ([self.camera isEqualToMapCamera:camera] && UIEdgeInsetsEqualToEdgeInsets(_contentInset, insets)) { if (completion) { @@ -3893,7 +3884,7 @@ public: return self.residualCamera; } - mbgl::CameraOptions mapCamera = self.mbglMap.getCameraOptions(); + mbgl::CameraOptions mapCamera = self.mbglMap.getCameraOptions(cameraOptions.padding.value_or(mbgl::EdgeInsets())); CLLocationCoordinate2D centerCoordinate = MGLLocationCoordinate2DFromLatLng(cameraOptions.center ? *cameraOptions.center : *mapCamera.center); double zoomLevel = cameraOptions.zoom ? *cameraOptions.zoom : self.zoomLevel; CLLocationDirection direction = cameraOptions.bearing ? mbgl::util::wrap(*cameraOptions.bearing, 0., 360.) : self.direction; diff --git a/src/mbgl/layout/symbol_projection.cpp b/src/mbgl/layout/symbol_projection.cpp index a58d90d4cf..aeea10ffa4 100644 --- a/src/mbgl/layout/symbol_projection.cpp +++ b/src/mbgl/layout/symbol_projection.cpp @@ -47,7 +47,7 @@ namespace mbgl { * For horizontal labels we want to do step 1 in the shader for performance reasons (no cpu work). * This is what `u_label_plane_matrix` is used for. * For labels aligned with lines we have to steps 1 and 2 on the cpu since we need access to the line geometry. - * This is what `updateLineLabels(...)` does. + * This is what `updateLineLabels(...)` in JS, `reprojectLineLabels()` in gl-native, does. * Since the conversion is handled on the cpu we just set `u_label_plane_matrix` to an identity matrix. * * Steps 3 and 4 are done in the shaders for all labels. diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index e97f0da3f1..88b559592e 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -77,13 +77,13 @@ void Transform::jumpTo(const CameraOptions& camera) { } /** - * Change any combination of center, zoom, bearing, and pitch, with a smooth animation - * between old and new values. The map will retain the current values for any options - * not included in `options`. + * Change any combination of center, zoom, bearing, pitch and edgeInsets, with a + * smooth animation between old and new values. The map will retain the current + * values for any options not included in `options`. */ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& animation) { - const EdgeInsets& padding = camera.padding; - LatLng startLatLng = getLatLng(padding, LatLng::Unwrapped); + const EdgeInsets& padding = camera.padding.value_or(state.edgeInsets); + LatLng startLatLng = getLatLng(LatLng::Unwrapped); const LatLng& unwrappedLatLng = camera.center.value_or(startLatLng); const LatLng& latLng = state.bounds != LatLngBounds::unbounded() ? unwrappedLatLng : unwrappedLatLng.wrapped(); double zoom = camera.zoom.value_or(getZoom()); @@ -110,9 +110,6 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim const Point<double> startPoint = Projection::project(startLatLng, state.scale); const Point<double> endPoint = Projection::project(latLng, state.scale); - ScreenCoordinate center = getScreenCoordinate(padding); - center.y = state.size.height - center.y; - // Constrain camera options. zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom()); const double scale = state.zoomScale(zoom); @@ -130,6 +127,7 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim state.panning = unwrappedLatLng != startLatLng; state.scaling = scale != startScale; state.rotating = bearing != startBearing; + const EdgeInsets startEdgeInsets = state.edgeInsets; startTransition(camera, animation, [=](double t) { Point<double> framePoint = util::interpolate(startPoint, endPoint, t); @@ -143,9 +141,14 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim if (pitch != startPitch) { state.pitch = util::interpolate(startPitch, pitch, t); } - - if (!padding.isFlush()) { - state.moveLatLng(frameLatLng, center); + if (padding != startEdgeInsets) { + // Interpolate edge insets + state.edgeInsets = { + util::interpolate(startEdgeInsets.top(), padding.top(), t), + util::interpolate(startEdgeInsets.left(), padding.left(), t), + util::interpolate(startEdgeInsets.bottom(), padding.bottom(), t), + util::interpolate(startEdgeInsets.right(), padding.right(), t) + }; } }, duration); } @@ -159,8 +162,8 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim Where applicable, local variable documentation begins with the associated variable or function in van Wijk (2003). */ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &animation) { - const EdgeInsets& padding = camera.padding; - const LatLng& latLng = camera.center.value_or(getLatLng(padding, LatLng::Unwrapped)).wrapped(); + const EdgeInsets& padding = camera.padding.value_or(state.edgeInsets); + const LatLng& latLng = camera.center.value_or(getLatLng(LatLng::Unwrapped)).wrapped(); double zoom = camera.zoom.value_or(getZoom()); double bearing = camera.bearing ? -*camera.bearing * util::DEG2RAD : getBearing(); double pitch = camera.pitch ? *camera.pitch * util::DEG2RAD : getPitch(); @@ -170,15 +173,12 @@ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &anima } // Determine endpoints. - LatLng startLatLng = getLatLng(padding, LatLng::Unwrapped).wrapped(); + LatLng startLatLng = getLatLng(LatLng::Unwrapped).wrapped(); startLatLng.unwrapForShortestPath(latLng); const Point<double> startPoint = Projection::project(startLatLng, state.scale); const Point<double> endPoint = Projection::project(latLng, state.scale); - ScreenCoordinate center = getScreenCoordinate(padding); - center.y = state.size.height - center.y; - // Constrain camera options. zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom()); pitch = util::clamp(pitch, util::PITCH_MIN, util::PITCH_MAX); @@ -278,6 +278,7 @@ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &anima state.panning = true; state.scaling = true; state.rotating = bearing != startBearing; + const EdgeInsets startEdgeInsets = state.edgeInsets; startTransition(camera, animation, [=](double k) { /// s: The distance traveled along the flight path, measured in @@ -304,9 +305,14 @@ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &anima if (pitch != startPitch) { state.pitch = util::interpolate(startPitch, pitch, k); } - - if (!padding.isFlush()) { - state.moveLatLng(frameLatLng, center); + if (padding != startEdgeInsets) { + // Interpolate edge insets + state.edgeInsets = { + util::interpolate(startEdgeInsets.top(), padding.top(), us), + util::interpolate(startEdgeInsets.left(), padding.left(), us), + util::interpolate(startEdgeInsets.bottom(), padding.bottom(), us), + util::interpolate(startEdgeInsets.right(), padding.right(), us) + }; } }, duration); } @@ -314,28 +320,16 @@ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &anima #pragma mark - Position void Transform::moveBy(const ScreenCoordinate& offset, const AnimationOptions& animation) { - ScreenCoordinate centerOffset = { offset.x, -offset.y, }; - ScreenCoordinate centerPoint = getScreenCoordinate() - centerOffset; - easeTo(CameraOptions().withCenter(state.screenCoordinateToLatLng(centerPoint)), animation); -} - -LatLng Transform::getLatLng(const EdgeInsets& padding, LatLng::WrapMode wrap) const { - if (padding.isFlush()) { - return state.getLatLng(wrap); - } else { - return screenCoordinateToLatLng(padding.getCenter(state.size.width, state.size.height)); - } + ScreenCoordinate centerOffset = { offset.x, offset.y }; + ScreenCoordinate pointOnScreen = state.edgeInsets.getCenter(state.size.width, state.size.height) - centerOffset; + // Use unwrapped LatLng to carry information about moveBy direction. + easeTo(CameraOptions().withCenter(screenCoordinateToLatLng(pointOnScreen, LatLng::Unwrapped)), animation); } -ScreenCoordinate Transform::getScreenCoordinate(const EdgeInsets& padding) const { - if (padding.isFlush()) { - return { state.size.width / 2., state.size.height / 2. }; - } else { - return padding.getCenter(state.size.width, state.size.height); - } +LatLng Transform::getLatLng(LatLng::WrapMode wrap) const { + return state.getLatLng(wrap); } - #pragma mark - Zoom double Transform::getZoom() const { @@ -364,7 +358,7 @@ void Transform::setMaxZoom(const double maxZoom) { #pragma mark - Bearing void Transform::rotateBy(const ScreenCoordinate& first, const ScreenCoordinate& second, const AnimationOptions& animation) { - ScreenCoordinate center = getScreenCoordinate(); + ScreenCoordinate center = state.edgeInsets.getCenter(state.size.width, state.size.height); const ScreenCoordinate offset = first - center; const double distance = std::sqrt(std::pow(2, offset.x) + std::pow(2, offset.y)); @@ -576,10 +570,10 @@ ScreenCoordinate Transform::latLngToScreenCoordinate(const LatLng& latLng) const return point; } -LatLng Transform::screenCoordinateToLatLng(const ScreenCoordinate& point) const { +LatLng Transform::screenCoordinateToLatLng(const ScreenCoordinate& point, LatLng::WrapMode wrapMode) const { ScreenCoordinate flippedPoint = point; flippedPoint.y = state.size.height - flippedPoint.y; - return state.screenCoordinateToLatLng(flippedPoint).wrapped(); + return state.screenCoordinateToLatLng(flippedPoint, wrapMode); } } // namespace mbgl diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 29ca9bd14e..0c018a6e48 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -47,8 +47,7 @@ public: @param offset The distance to pan the map by, measured in pixels from top to bottom and from left to right. */ void moveBy(const ScreenCoordinate& offset, const AnimationOptions& = {}); - LatLng getLatLng(const EdgeInsets& = {}, LatLng::WrapMode = LatLng::Wrapped) const; - ScreenCoordinate getScreenCoordinate(const EdgeInsets& = {}) const; + LatLng getLatLng(LatLng::WrapMode = LatLng::Wrapped) const; // Bounds @@ -105,7 +104,7 @@ public: // Conversion and projection ScreenCoordinate latLngToScreenCoordinate(const LatLng&) const; - LatLng screenCoordinateToLatLng(const ScreenCoordinate&) const; + LatLng screenCoordinateToLatLng(const ScreenCoordinate&, LatLng::WrapMode = LatLng::Wrapped) const; private: MapObserver& observer; diff --git a/src/mbgl/map/transform_state.cpp b/src/mbgl/map/transform_state.cpp index 596ed05195..f1d45c043c 100644 --- a/src/mbgl/map/transform_state.cpp +++ b/src/mbgl/map/transform_state.cpp @@ -33,24 +33,36 @@ void TransformState::getProjMatrix(mat4& projMatrix, uint16_t nearZ, bool aligne return; } - // Find the distance from the center point [width/2, height/2] to the - // center top point [width/2, 0] in Z units, using the law of sines. + const double cameraToCenterDistance = getCameraToCenterDistance(); + auto offset = getCenterOffset(); + + // Find the distance from the viewport center point + // [width/2 + offset.x, height/2 + offset.y] to the top edge, to point + // [width/2 + offset.x, 0] in Z units, using the law of sines. // 1 Z unit is equivalent to 1 horizontal px at the center of the map // (the distance between[width/2, height/2] and [width/2 + 1, height/2]) - const double halfFov = getFieldOfView() / 2.0; + const double fovAboveCenter = getFieldOfView() * (0.5 + offset.y / size.height); const double groundAngle = M_PI / 2.0 + getPitch(); - const double topHalfSurfaceDistance = std::sin(halfFov) * getCameraToCenterDistance() / std::sin(M_PI - groundAngle - halfFov); + const double aboveCenterSurfaceDistance = std::sin(fovAboveCenter) * cameraToCenterDistance / std::sin(M_PI - groundAngle - fovAboveCenter); // Calculate z distance of the farthest fragment that should be rendered. - const double furthestDistance = std::cos(M_PI / 2 - getPitch()) * topHalfSurfaceDistance + getCameraToCenterDistance(); + const double furthestDistance = std::cos(M_PI / 2 - getPitch()) * aboveCenterSurfaceDistance + cameraToCenterDistance; // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` const double farZ = furthestDistance * 1.01; matrix::perspective(projMatrix, getFieldOfView(), double(size.width) / size.height, nearZ, farZ); + // Move the center of perspective to center of specified edgeInsets. + // Values are in range [-1, 1] where the upper and lower range values + // position viewport center to the screen edges. This is overriden + // if using axonometric perspective (not in public API yet, Issue #11882). + // TODO(astojilj): Issue #11882 should take edge insets into account, too. + projMatrix[8] = -offset.x * 2.0 / size.width; + projMatrix[9] = offset.y * 2.0 / size.height; + const bool flippedY = viewportMode == ViewportMode::FlippedY; - matrix::scale(projMatrix, projMatrix, 1, flippedY ? 1 : -1, 1); + matrix::scale(projMatrix, projMatrix, 1.0, flippedY ? 1 : -1, 1); matrix::translate(projMatrix, projMatrix, 0, 0, -getCameraToCenterDistance()); @@ -134,16 +146,8 @@ ViewportMode TransformState::getViewportMode() const { #pragma mark - Camera options CameraOptions TransformState::getCameraOptions(const EdgeInsets& padding) const { - LatLng center; - if (padding.isFlush()) { - center = getLatLng(); - } else { - ScreenCoordinate point = padding.getCenter(size.width, size.height); - point.y = size.height - point.y; - center = screenCoordinateToLatLng(point).wrapped(); - } return CameraOptions() - .withCenter(center) + .withCenter(getLatLng()) .withPadding(padding) .withZoom(getZoom()) .withBearing(-bearing * util::RAD2DEG) @@ -222,6 +226,10 @@ double TransformState::getMaxZoom() const { return scaleZoom(max_scale); } +ScreenCoordinate TransformState::getCenterOffset() const { + return { 0.5 * (edgeInsets.left() - edgeInsets.right()), 0.5 * (edgeInsets.top() - edgeInsets.bottom()) }; +} + #pragma mark - Rotation float TransformState::getBearing() const { @@ -240,7 +248,6 @@ float TransformState::getPitch() const { return pitch; } - #pragma mark - State bool TransformState::isChanging() const { @@ -328,8 +335,7 @@ LatLng TransformState::screenCoordinateToLatLng(const ScreenCoordinate& point, L mat4 TransformState::coordinatePointMatrix() const { mat4 proj; getProjMatrix(proj); - float s = util::tileSize; - matrix::scale(proj, proj, s, s, 1); + matrix::scale(proj, proj, util::tileSize, util::tileSize, 1); matrix::multiply(proj, getPixelMatrix(), proj); return proj; } diff --git a/src/mbgl/map/transform_state.hpp b/src/mbgl/map/transform_state.hpp index f51660912c..9e501d600f 100644 --- a/src/mbgl/map/transform_state.hpp +++ b/src/mbgl/map/transform_state.hpp @@ -62,6 +62,10 @@ public: void setMaxZoom(double); double getMaxZoom() const; + // Viewport center offset, from [size.width / 2, size.height / 2], defined + // by |edgeInsets| in screen coordinates, with top left origin. + ScreenCoordinate getCenterOffset() const; + // Rotation float getBearing() const; float getFieldOfView() const; @@ -137,6 +141,8 @@ private: double ySkew = 1.0; bool axonometric = false; + EdgeInsets edgeInsets; + // cache values for spherical mercator math double Bc = Projection::worldSize(scale) / util::DEGREES_MAX; double Cc = Projection::worldSize(scale) / util::M2PI; diff --git a/src/mbgl/programs/symbol_program.cpp b/src/mbgl/programs/symbol_program.cpp index b538a23331..8a7dccd55e 100644 --- a/src/mbgl/programs/symbol_program.cpp +++ b/src/mbgl/programs/symbol_program.cpp @@ -66,7 +66,7 @@ Values makeValues(const bool isText, const bool pitchWithMap = values.pitchAlignment == style::AlignmentType::Map; const bool rotateWithMap = values.rotationAlignment == style::AlignmentType::Map; - // Line label rotation happens in `updateLineLabels` + // Line label rotation happens in `updateLineLabels`/`reprojectLineLabels`` // Pitched point labels are automatically rotated by the labelPlaneMatrix projection // Unpitched point labels need to have their rotation applied after projection const bool rotateInShader = rotateWithMap && !pitchWithMap && !alongLine; diff --git a/src/mbgl/text/collision_index.cpp b/src/mbgl/text/collision_index.cpp index 88e59bf51c..e3d832a854 100644 --- a/src/mbgl/text/collision_index.cpp +++ b/src/mbgl/text/collision_index.cpp @@ -348,25 +348,28 @@ std::pair<float,float> CollisionIndex::projectAnchor(const mat4& posMatrix, cons std::pair<Point<float>,float> CollisionIndex::projectAndGetPerspectiveRatio(const mat4& posMatrix, const Point<float>& point) const { vec4 p = {{ point.x, point.y, 0, 1 }}; matrix::transformMat4(p, p, posMatrix); + auto offset = transformState.getCenterOffset(); + auto size = transformState.getSize(); return std::make_pair( Point<float>( - (((p[0] / p[3] + 1) / 2) * transformState.getSize().width) + viewportPadding, - (((-p[1] / p[3] + 1) / 2) * transformState.getSize().height) + viewportPadding + (((p[0] / p[3] + 1) / 2) * size.width) + viewportPadding + offset.x, + (((-p[1] / p[3] + 1) / 2) * size.height) + viewportPadding + offset.y ), // See perspective ratio comment in symbol_sdf.vertex // We're doing collision detection in viewport space so we need // to scale down boxes in the distance - 0.5 + 0.5 * (transformState.getCameraToCenterDistance() / p[3]) + 0.5 + 0.5 * transformState.getCameraToCenterDistance() / p[3] ); } Point<float> CollisionIndex::projectPoint(const mat4& posMatrix, const Point<float>& point) const { vec4 p = {{ point.x, point.y, 0, 1 }}; matrix::transformMat4(p, p, posMatrix); - return Point<float>( - (((p[0] / p[3] + 1) / 2) * transformState.getSize().width) + viewportPadding, - (((-p[1] / p[3] + 1) / 2) * transformState.getSize().height) + viewportPadding - ); + auto offset = transformState.getCenterOffset(); + auto size = transformState.getSize(); + return Point<float> { + static_cast<float>((((p[0] / p[3] + 1) / 2) * size.width) + viewportPadding + offset.x), + static_cast<float>((((-p[1] / p[3] + 1) / 2) * size.height) + viewportPadding + offset.y) }; } } // namespace mbgl diff --git a/test/api/annotations.test.cpp b/test/api/annotations.test.cpp index d7cb572cb0..03330dc4c6 100644 --- a/test/api/annotations.test.cpp +++ b/test/api/annotations.test.cpp @@ -434,6 +434,73 @@ TEST(Annotations, VisibleFeatures) { EXPECT_EQ(features.size(), ids.size()); } +TEST(Annotations, ViewFrustumCulling) { + // The purpose of the test is to control that annotations outside screen + // rectangle are not rendered for different camera setup, especially when + // using edge insets - viewport center is then offsetted. + + // Important premise of this test is "static const float viewportPadding = 100;" + // as defined in collision_index.cpp: tests using edge insets are writen so that + // padding is 128 (half of viewSize width). If increasing viewportPadding, + // increase the padding in test cases below. + AnnotationTest test; + + auto viewSize = test.frontend.getSize(); + auto box = ScreenBox { {}, { double(viewSize.width), double(viewSize.height) } }; + + test.map.getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json")); + test.map.addAnnotationImage(namedMarker("default_marker")); + const LatLng center = { 5.0, 5.0 }; + test.map.jumpTo(CameraOptions().withCenter(center).withZoom(3.0)); + + LatLng tl = test.map.latLngForPixel(ScreenCoordinate(0, 0)); + LatLng br = test.map.latLngForPixel(ScreenCoordinate(viewSize.width, viewSize.height)); + + std::vector<LatLng> latLngs = { + tl, + test.map.latLngForPixel(ScreenCoordinate(viewSize.width, 0)), + test.map.latLngForPixel(ScreenCoordinate(0, viewSize.height)), + br, + center}; + + std::vector<mbgl::AnnotationID> ids; + for (auto latLng : latLngs) { + ids.push_back(test.map.addAnnotation(SymbolAnnotation { { latLng.longitude(), latLng.latitude() }, "default_marker" })); + } + + std::vector<std::pair<CameraOptions, std::vector<uint64_t>>> expectedVisibleForCamera = { + // Start with all markers visible. + { CameraOptions(), { 0, 1, 2, 3, 4 } }, + // Move center to topLeft: only former center and top left (now center) are visible. + { CameraOptions().withCenter(tl), { 0, 4 } }, + // Reset center. With pitch: only top row markers and center are visible. + { CameraOptions().withCenter(center).withPitch(45), { 0, 1, 4 } }, + // Reset pitch, and use padding to move viewport center: only topleft and center are visible. + { CameraOptions().withPitch(0).withPadding(EdgeInsets { viewSize.height * 0.5, viewSize.width * 0.5, 0, 0 }), { 0, 4 } }, + // Use opposite padding to move viewport center: only bottom right and center are visible. + { CameraOptions().withPitch(0).withPadding(EdgeInsets { 0, 0, viewSize.height * 0.5, viewSize.width * 0.5 }), { 3, 4 } }, + // Use top padding to move viewport center: top row and center are visible. + { CameraOptions().withPitch(0).withPadding(EdgeInsets { viewSize.height * 0.5, 0, 0, 0 }), { 0, 1, 4 } }, + // Use bottom padding: only bottom right and center are visible. + { CameraOptions().withPitch(0).withPadding(EdgeInsets { 0, 0, viewSize.height * 0.5, 0 }), { 2, 3, 4 } }, + // Left padding and pitch: top left, bottom left and center are visible. + { CameraOptions().withPitch(45).withPadding(EdgeInsets { 0, viewSize.width * 0.5, 0, 0 }), { 0, 2, 4 } } + }; + + for (unsigned i = 0; i < expectedVisibleForCamera.size(); i++) { + auto testCase = expectedVisibleForCamera[i]; + test.map.jumpTo(testCase.first); + test.frontend.render(test.map); + auto features = test.frontend.getRenderer()->queryRenderedFeatures(box, {}); + for (uint64_t id : testCase.second) { // testCase.second is vector of ids expected. + EXPECT_NE(std::find_if(features.begin(), features.end(), [&id](auto feature) { + return id == feature.id.template get<uint64_t>(); + }), features.end()) << "Point with id " << id << " is missing in test case " << i; + EXPECT_EQ(features.size(), testCase.second.size()) << " in test case " << i; + } + } +} + TEST(Annotations, TopOffsetPixels) { AnnotationTest test; diff --git a/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png b/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png Binary files differnew file mode 100644 index 0000000000..f161a95c8e --- /dev/null +++ b/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png diff --git a/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png b/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png Binary files differnew file mode 100644 index 0000000000..edb8d07a40 --- /dev/null +++ b/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png diff --git a/test/map/transform.test.cpp b/test/map/transform.test.cpp index 33d325dfb2..3d37312b17 100644 --- a/test/map/transform.test.cpp +++ b/test/map/transform.test.cpp @@ -288,25 +288,48 @@ TEST(Transform, Padding) { ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); - - transform.jumpTo(CameraOptions().withCenter(LatLng { 10, -100 }).withZoom(10.0)); + CameraOptions nonPaddedCameraOptions = CameraOptions().withCenter(LatLng { 10, -100 }).withZoom(10.0); + transform.jumpTo(nonPaddedCameraOptions); const LatLng trueCenter = transform.getLatLng(); ASSERT_DOUBLE_EQ(10, trueCenter.latitude()); ASSERT_DOUBLE_EQ(-100, trueCenter.longitude()); ASSERT_DOUBLE_EQ(10, transform.getZoom()); - const LatLng manualShiftedCenter = transform.getState().screenCoordinateToLatLng({ + const LatLng screenCenter = transform.screenCoordinateToLatLng({ + 1000.0 / 2.0, 1000.0 / 2.0, - 1000.0 / 4.0, + }); + const LatLng upperHalfCenter = transform.screenCoordinateToLatLng({ + 1000.0 / 2.0, + 1000.0 * 0.25, }); EdgeInsets padding(1000.0 / 2.0, 0, 0, 0); - const LatLng shiftedCenter = transform.getLatLng(padding); - ASSERT_NE(trueCenter.latitude(), shiftedCenter.latitude()); - ASSERT_NEAR(trueCenter.longitude(), shiftedCenter.longitude(), 1e-8); - ASSERT_DOUBLE_EQ(manualShiftedCenter.latitude(), shiftedCenter.latitude()); - ASSERT_DOUBLE_EQ(manualShiftedCenter.longitude(), shiftedCenter.longitude()); + // CameraOption center and zoom don't change when padding changes: center of + // viewport remains the same as padding defines viwport center offset in rendering. + CameraOptions paddedOptions = CameraOptions().withPadding(padding); + transform.jumpTo(paddedOptions); + const LatLng theSameCenter = transform.getLatLng(); + ASSERT_DOUBLE_EQ(trueCenter.latitude(), theSameCenter.latitude()); + ASSERT_DOUBLE_EQ(trueCenter.longitude(), theSameCenter.longitude()); + + // However, LatLng is now at the center of lower half - verify conversion + // from screen coordinate to LatLng. + const LatLng paddedLowerHalfScreenCenter = transform.screenCoordinateToLatLng({ + 1000.0 / 2.0, + 1000.0 * 0.75, + }); + ASSERT_NEAR(screenCenter.latitude(), paddedLowerHalfScreenCenter.latitude(), 1e-10); + ASSERT_NEAR(screenCenter.longitude(), paddedLowerHalfScreenCenter.longitude(), 1e-10); + + // LatLng previously in upper half center, should now be under screen center. + const LatLng paddedScreenCenter = transform.screenCoordinateToLatLng({ + 1000.0 / 2.0, + 1000.0 / 2.0, + }); + ASSERT_NEAR(upperHalfCenter.latitude(), paddedScreenCenter.latitude(), 1e-10); + ASSERT_NEAR(upperHalfCenter.longitude(), paddedScreenCenter.longitude(), 1e-10); } TEST(Transform, MoveBy) { @@ -440,7 +463,7 @@ TEST(Transform, Camera) { flyOptions.transitionFrameFn = [&](double t) { ASSERT_TRUE(t >= 0 && t <= 1); ASSERT_LE(latLng2.latitude(), transform.getLatLng().latitude()); - ASSERT_GE(latLng2.longitude(), transform.getLatLng({}, LatLng::Unwrapped).longitude()); + ASSERT_GE(latLng2.longitude(), transform.getLatLng(LatLng::Unwrapped).longitude()); }; flyOptions.transitionFinishFn = [&]() { // XXX Fix precision loss in flyTo: @@ -618,7 +641,7 @@ TEST(Transform, LatLngBounds) { // Try crossing the antimeridian from the right. transform.jumpTo(CameraOptions().withCenter(LatLng { 0.0, 200.0 })); - ASSERT_DOUBLE_EQ(transform.getLatLng({}, LatLng::Unwrapped).longitude(), 180.0); + ASSERT_DOUBLE_EQ(transform.getLatLng(LatLng::Unwrapped).longitude(), 180.0); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), -180.0); // -1 | 0 | +1 diff --git a/test/text/local_glyph_rasterizer.test.cpp b/test/text/local_glyph_rasterizer.test.cpp index ec8a728e9f..86dc87b6c7 100644 --- a/test/text/local_glyph_rasterizer.test.cpp +++ b/test/text/local_glyph_rasterizer.test.cpp @@ -43,9 +43,9 @@ public: MapAdapter map { frontend, MapObserver::nullObserver(), fileSource, MapOptions().withMapMode(MapMode::Static).withSize(frontend.getSize())}; - void checkRendering(const char * name) { + void checkRendering(const char * name, double imageMatchPixelsThreshold = 0.05, double pixelMatchThreshold = 0.1) { test::checkImage(std::string("test/fixtures/local_glyphs/") + name, - frontend.render(map), 0.05, 0.1); + frontend.render(map), imageMatchPixelsThreshold, pixelMatchThreshold); } }; @@ -85,6 +85,50 @@ TEST(LocalGlyphRasterizer, NoLocal) { return response; }; test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json")); - test.checkRendering("no_local"); + test.checkRendering("no_local", 0.001, 0.1); } +TEST(LocalGlyphRasterizer, NoLocalWithContentInsets) { + // Expectations: content insets imply rendering to offsetted viewport center. + // Rendered text should be on the same offset and keep the same size as + // with no offset. + LocalGlyphRasterizerTest test({}); + + test.fileSource->glyphsResponse = [&] (const Resource& resource) { + EXPECT_EQ(Resource::Kind::Glyphs, resource.kind); + Response response; + response.data = std::make_shared<std::string>(util::read_file("test/fixtures/resources/glyphs.pbf")); + return response; + }; + auto viewSize = test.frontend.getSize(); + test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json")); + + // Expected image was created using center offset as in the line below - content insets, + // with no pitch defined, should produce the same output. + // test.map.moveBy({ viewSize.width * 0.25, 0 }); + + test.map.jumpTo(CameraOptions().withPadding(EdgeInsets { 0, viewSize.width * 0.25 * 2, 0, 0 })); + test.checkRendering("no_local_with_content_insets", 0.001, 0.1); +} + +TEST(LocalGlyphRasterizer, NoLocalWithContentInsetsAndPitch) { + // Expectations: content insets imply rendering to offsetted viewport center. + // Rendered text should be on the same offset and keep the same size as + // with no offset. + LocalGlyphRasterizerTest test({}); + + test.fileSource->glyphsResponse = [&] (const Resource& resource) { + EXPECT_EQ(Resource::Kind::Glyphs, resource.kind); + Response response; + response.data = std::make_shared<std::string>(util::read_file("test/fixtures/resources/glyphs.pbf")); + return response; + }; + auto viewSize = test.frontend.getSize(); + test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json")); + + // Expected image was verified using no-padding render, offsetted to right + // using bitmap editor. + + test.map.jumpTo(CameraOptions().withPitch(45).withPadding(EdgeInsets { 0, viewSize.width * 0.4, 0, 0 })); + test.checkRendering("no_local_with_content_insets_and_pitch", 0.001, 0.1); +} |