Swift IOS Overlay - swift

My IOS map isn't rendering the NOAA tiles on top of it. Can someone give my a clue what is wrong? Here is my viewcontroler. It's just a standard mapkit viewer. Anyways, it is not working and is just giving me a standard satellite map when I run this code. Does anyone know what I am doing wrong?
Thanks for any help you can give me.
Have a great rest of your weekend.
import UIKit
import MapKit
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
//Linking MapView
override func viewDidLoad() {
super.viewDidLoad()
self.mapView.delegate = self
self.mapView.mapType = MKMapType.satellite
let centre = CLLocationCoordinate2D(latitude: 39.2189, longitude: -76.0690)
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let region = MKCoordinateRegion(center: centre, span: span)
self.mapView.setRegion(region, animated: false)
self.mapView.regionThatFits(region)
let template = "https://tileservice.charts.noaa.gov/tiles/50000_1/{z}/{x}/{y}.png"
let carte_indice = MKTileOverlay(urlTemplate:template)
carte_indice.isGeometryFlipped = true
carte_indice.canReplaceMapContent = false
self.mapView.addOverlay(carte_indice)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func mapView(
mapView: MKMapView!, rendererFor overlay: MKOverlay!) -> MKOverlayRenderer!
{
if overlay is MKTileOverlay{
var renderer = MKTileOverlayRenderer(overlay:overlay)
renderer.alpha = 0.8
return renderer
}
return nil
}
}

After a lot of mulling over it, I solved my issue. Posting the solution if anyone needs it.
Thanks to GJ Nilsen for helping me figure this out. The problem was that the NOAA tiles didn't have coverage for everywhere, so I solved it by simplify forcing it to load.
if overlay is MKTileOverlay{
var renderer = MKTileOverlayRenderer(overlay:overlay)
renderer.alpha = 0.8
return renderer
}
else{
var renderer = MKTileOverlayRenderer(overlay:overlay)
renderer.alpha = 0.8
return renderer
}

The NOAA RNC Map Tile server is being taken out of commission, and some of the tiles my app uses started being disrupted around the same time you posted this question. This is part of the larger transfer from Raster to Electronic charting, and NOAA's discontinuation of paper charts.
https://nauticalcharts.noaa.gov/updates/coast-survey-to-shut-down-the-raster-navigational-chart-tile-service-and-other-related-services/
I am working to find a similar solution, but all of the new services I have found require a pay model of some sort (MBTile conversion, ArcGIS, etc) ... I miss the good old free NOAA map tiles!

Related

How Do I Draw a String in an MKOverlayRender

The use case I have is one where I want to draw and label counties in a state. Annotations don't seem like the right approach to solve this problem. First of all, the label refers to region rather than a point. Second, there are far too many; so, I would have to selectively show and hide annotations based on zoom level (actually something more like the size of the MKCoordinateRegion span). Lastly, county labels are not all that relevant unless the user starts zooming in.
Just as a side note, county boundaries may be present in map tiles, but they are not emphasized. Moreover, there are a multitude of other boundaries I might want to draw that are completely absent from map tiles.
Ultimately, what I want to do is create an overlay for each county shape (counties are clickable and I can navigate to details) and another set of overlays for the labels. I separate county shapes and labels because county shapes are messy and I just use the center of the county. There is no guarantee with this approach that labels will not draw outside of county shapes, which means labels could end up getting clipped when other counties are drawn.
Drawing the county shapes was relatively easy or at least relatively well documented. I do not include any code on rendering shapes. Drawing text on the other hand is not straight forward, not well documented, and most of the posts on the subject are ancient. The lack of recent posts on the subject as well as the fact that most posts posit solutions that no longer work, use deprecated APIs, or only solve a part of the problem motivates this post. Of course, the lack of activity on this problem could be because my strategy is mind numbingly stupid.
I have posted a complete solution to the problem. If you can improve on the solution below or believe there is a better way, I would appreciate the feedback. Alternatively, if you are trying to find a solution to this problem, you will find this post more helpful than the dozens I have looked at, which on the whole got me to where I am now.
Below is a complete solution that can be run in an Xcode single view Playground. I am running Xcode 14.2. The most important bit of code is the overridden draw function of LabelOverlayRenderer. That bit of code is what I struggled to craft for more than a day. I almost gave up. Another key point is when drawing text, one uses CoreText. The APIs pertaining to drawing and managing text are many and most have had a lot of name changes and deprecation.
import UIKit
import MapKit
import SwiftUI
class LabelOverlayRenderer: MKOverlayRenderer {
let title: String
let center: CLLocationCoordinate2D
init(overlay: LabelOverlay) {
center = overlay.coordinate
title = overlay.title!
super.init(overlay: overlay)
}
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
context.saveGState()
// Set Drawing mode
context.setTextDrawingMode(.fillStroke)
// If I don't do this, the text is upside down.
context.textMatrix = CGAffineTransformMakeScale(1.0, -1.0);
// Text size is crazy big because label has to be miles across
// to be visible.
var attrs = [ NSAttributedString.Key : Any]()
attrs[NSAttributedString.Key.font] = UIFont(name: "Helvetica", size: 128000.0)!
attrs[NSAttributedString.Key.foregroundColor] = UIColor(Color.red)
let attributedString = NSAttributedString(string: title, attributes: attrs)
let line = CTLineCreateWithAttributedString(attributedString)
// Get the size of the whole string, so the string can
// be centered. CGSize is huge because I don't want
// to clip or wrap the string. The range setting
// is just cut and paste. Looks like a place holder.
// Ideally, it is the range of that portion
// of the string for which I want the size.
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
let size = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, CGSize(width: 1000000, height: 1000000), nil)
// Center is lat-lon, but map is in meters (maybe? definitely
// not lat-lon). Center string and draw.
var p = point(for: MKMapPoint(center))
p.x -= size.width/2
p.y += size.height/2
// There is no "at" on CTLineDraw. The string
// is positioned in the context.
context.textPosition = p
CTLineDraw(line, context)
context.restoreGState()
}
}
class LabelOverlay: NSObject, MKOverlay {
let title: String?
let coordinate: CLLocationCoordinate2D
let boundingMapRect: MKMapRect
init(title: String, coordinate: CLLocationCoordinate2D, boundingMapRect: MKMapRect) {
self.title = title
self.coordinate = coordinate
self.boundingMapRect = boundingMapRect
}
}
class MapViewCoordinator: NSObject, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let overlay = overlay as? LabelOverlay {
return LabelOverlayRenderer(overlay: overlay)
}
fatalError("Unknown overlay type!")
}
}
struct MyMapView: UIViewRepresentable {
func makeCoordinator() -> MapViewCoordinator {
return MapViewCoordinator()
}
func updateUIView(_ view: MKMapView, context: Context){
// Center on Georgia
let center = CLLocationCoordinate2D(latitude: 32.6793, longitude: -83.62245)
let span = MKCoordinateSpan(latitudeDelta: 4.875, longitudeDelta: 5.0003)
let region = MKCoordinateRegion(center: center, span: span)
view.setRegion(region, animated: true)
view.delegate = context.coordinator
let coordinate = CLLocationCoordinate2D(latitude: 32.845084, longitude: -84.3742)
let mapRect = MKMapRect(x: 70948460.0, y: 107063759.0, width: 561477.0, height: 613908.0)
let overlay = LabelOverlay(title: "Hello World!", coordinate: coordinate, boundingMapRect: mapRect)
view.addOverlay(overlay)
}
func makeUIView(context: Context) -> MKMapView {
// Create a map with constrained zoom gestures only
let mapView = MKMapView(frame: .zero)
mapView.isPitchEnabled = false
mapView.isRotateEnabled = false
let zoomRange = MKMapView.CameraZoomRange(
minCenterCoordinateDistance: 160000,
maxCenterCoordinateDistance: 1400000
)
mapView.cameraZoomRange = zoomRange
return mapView
}
}
struct ContentView: View {
var body: some View {
VStack {
MyMapView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Google Maps strange behavior swift 4 ios

Update: Dose it have to do anything with language? as my app is sporting English and Arabic.
Hi everyone I am struggling with this since days and I have tried everything but can not find any solution, the problem is when I run the app through xCode it all works fine like in the following screenshot.
Then when i unplug the device and reopen the app it stops working, the app is running fine but i dont see any map but just the markers like the following screenshot.
Any help will be appreciated.
AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
GMSServices.provideAPIKey(googleApiKey)
GMSPlacesClient.provideAPIKey(googleApiKey)
return true
}
ViewController
//Step 1
#IBOutlet weak var mapview: GMSMapView!
//Step 2
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.mapview.settings.scrollGestures = true
self.mapview.settings.zoomGestures = true
self.mapview.isMyLocationEnabled = true
self.mapview.settings.myLocationButton = true
self.mapview.delegate = self
self.mapview.layoutIfNeeded()
}
}
//Step 3 After we have the current location items are loaded from server by user location and the markers are added in GetBusinesses function
extension ItemsList: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
guard status == .authorizedWhenInUse else {
return
}
self.locationManager.startUpdatingLocation()
self.mapview.camera = GMSCameraPosition(target: (self.locationManager.location?.coordinate)!, zoom: 14, bearing: 0, viewingAngle: 0)
self.GetBusinesses(data: "&isOpen=2&cat=\(Prefrences.getSelectedCategoryId())")
}
}
to show map in custom views you have to do it that way,
let camera = GMSCameraPosition.camera(withLatitude: lat , longitude: long, zoom: 15.0)
let mapView = GMSMapView.map(withFrame: CGRect(x: 2, y: 2, width: self.MapView.frame.width - 4, height: self.MapView.frame.height - 2), camera: camera)
self.MapView.addSubview(mapView)
self.MapView.clipsToBounds = true
self.MapView.contentMode = .center
you need to create a mapView using the created mapView on the storyboard frame and add it as a subView in the main MapView you created
Okay so after struggling many days and trying every stupid possible way to fix it, I finally found a solution to it. The main problem was if your app is supporting multiple languages then google map will show this kind of behaviour. Following are the steps to fix it.
Step 1:
If you have the GMSMapView view in your viewController, break the outlets of your GMSMapView since it will be created dynamically.
Step 2:
Set the application language as per the user settings.
Step 3:
Create custom GMSMapView object set delegate methods and other necessary settings and add it to its container.
That's it, you are good to go. What I understood from this is that you have to set your app language first before creating the GMSMapView (I can be wrong as well). If anyone knows why this method is working kindly explain it.
//Our Custom MapView Var
var mapview: GMSMapView!
//View viewDidLoad() function
override func viewDidLoad() {
super.viewDidLoad()
/*
TODO:
Get selected language from preferences and set accordingly.
*/
UserDefaults.standard.set(["en"], forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
let camera = GMSCameraPosition.camera(withLatitude: 55.277397,
longitude: 25.199514,
zoom:12)
self.mapview = GMSMapView.map(withFrame: self.mMapContainer.bounds, camera: camera)
self.mapview.delegate = self
self.mapview.isMyLocationEnabled = true
self.mapview.settings.myLocationButton = true
self.mapview.isMyLocationEnabled = true
self.mapview.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mMapContainer.addSubview(self.mapview)
}

How do I get the Lat Long of where the user tapped? in Swift v3 and google maps sdk

I'm coding an app in Swift3. I'm using the google maps sdk. I've implemented a GMSMapView. I'm trying to ascertain what lat long that the user tapped in the GMSMapView.
Below is the reference for the android development version of what I think would help. But I can't find the equivalent for iOS.
https://developers.google.com/android/reference/com/google/android/gms/maps/GoogleMap.OnMapClickListener
edit: The "possible duplicate" didn't solve my problem.
I found the solution at: https://developers.google.com/maps/documentation/ios-sdk/events
by googling for didTapAtCoordinate, which I somehow knew was part of the SDK.
The most important line for me was: mapView.delegate = self. It's possible that some other solutions I tried would have worked had I used mapView.delegate = self, but nothing else had mentioned it. Also, the func mapView is of course important so I can use the coordinates of where the user tapped.
import UIKit
import GoogleMaps
override func loadView() {
let camera = GMSCameraPosition.camera(withLatitude: 1.285,
longitude: 103.848,
zoom: 12)
let mapView = GMSMapView.map(withFrame: .zero, camera: camera)
mapView.delegate = self
self.view = mapView
}
// MARK: GMSMapViewDelegate
class DemoViewController: UIViewController, GMSMapViewDelegate {
}
func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) {
print("You tapped at \(coordinate.latitude), \(coordinate.longitude)")
}//end func

SWIFT: How to make an app zoom to your current location on launching?

I'm trying to build an app that when you open it it zooms to your current location. Below is my code so far.
class MapVC: UIViewController {
#IBOutlet weak var mapView: MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
mapView.zoomToUserLocation()
}
}
extension MKMapView {
func zoomToUserLocation() {
print(userLocation.location?.coordinate)
guard let coordinate = userLocation.location?.coordinate else { return }
let region = MKCoordinateRegionMakeWithDistance(coordinate, 10000, 10000)
setRegion(region, animated: true)
}
}
This code does not solve the problem. I believe this is because the app doesn't have time after receiving location authorisation to obtain the user location. I'm looking to write a function that would wait for the app to have a location and then call the zoomToUserLocation function. I've already tried this with a do-while loop which didn't work. I could set a delay but that would mean the zoom was done at a specific time and instead I want the zoom to be done as soon as possible. I've found solutions to this in objective C but couldn't translate it.
This method might be helpful setUserTrackingMode(_:animated:), but I think that's not what you are looking for.
The solution that may fit best for you is to implement locationManager(_:didChangeAuthorization:) and locationManager(_:didUpdateLocations:) on the CLLocationManager's delegate. Then you call zoomToUserLocation() in didUpdateLocations.
Another observation, is that you shouldn't animate the map inside viewDidLoad() 'cause the view it not on screen yet. This should be done in viewDidAppead().
you need both a span and a region...the span allows you to set how far you want to zoom in...
let span = MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
if let center = location?.coordinate {
let region = MKCoordinateRegion(center: center, span: span)
self.mapView.setRegion(region, animated: true)
self.mapView.showsUserLocation = true

Failed to set maprect to show all annotations

I have 2 annotations to display on the mapview, but unable to set the maprect to show all of them on screen without requiring users to zoom out.
I tried with showAnnotations but no luck. Anyone has been able to do this in Swift and Xcode 6.1.1?
Here is my code:
class ViewController: UIViewController, MKMapViewDelegate {
#IBOutlet var map: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
var mapView = map
// 1
let point1 = CLLocationCoordinate2D(latitude: 38.915565, longitude: -77.093524)
let point2 = CLLocationCoordinate2D(latitude: 38.890693, longitude: -76.933318)
//2
let annotation = MKPointAnnotation()
annotation.setCoordinate(point1)
annotation.title = "point1"
map.addAnnotation(annotation)
let annotation2 = MKPointAnnotation()
annotation2.setCoordinate(point2)
annotation2.title = "point2"
map.addAnnotation(annotation2)
//3
// option1: set maprect to cover all annotations, doesn't work
var points = [annotation, annotation2]
var rect = MKMapRectNull
for p in points {
let k = MKMapPointForCoordinate(p.coordinate)
rect = MKMapRectUnion(rect, MKMapRectMake(k.x, k.y, 0.1, 0.1))
println("result: x = \(rect.origin.x) y = \(rect.origin.y)")
}
map.setVisibleMapRect(rect, animated: true)
// option 2: using showAnnotations, doesn't work
//map.showAnnotations(points, animated: true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
This is what I got currently:
This is what I expected to see:
Thanks for your help.
I finally found out why the pins of the annotations had not been displayed in the visible region of the screen. I think the MapKit framework behaves a bit different than in the previous versions. Since I use autolayout to allow the map to expand to the entire screen for all devices (iPhones, iPad), setVisibleMapRect or mapView.showAnnotations of the map should be invoked in mapViewDidFinishLoadingMap, not in viewDidLoad of the view controller
For example:
func mapViewDidFinishLoadingMap(_ mapView: MKMapView) {
// this is where visible maprect should be set
mapView.showAnnotations(mapView.annotations, animated: true)
}
I had this same problem when I called
viewDidLoad() {
mapView.showAnnotations(myAnnotations, animated: false)
}
However, moving the call to viewDidLayoutSubviews() also seems to fix the problem (not that isInitialLoad is initialized to true in viewDidLoad).
viewDidLayoutSubviews() {
if isInitialLoad {
mapView.showAnnotations(myAnnotations, animated: false)
isInitialLoad = false
}
}
The difference (I think it is an advantage) of putting the call in viewDidLayoutSubviews is that the map hasn't actually displayed yet, so your initial display is that area defined by the annotations. However, it seems that it is called every time the map zooms, so you need to be sure to only call it the first time.
For me using showing annotations after map did finish loading did not work.
func mapViewDidFinishLoadingMap(mapView: MKMapView!) {
// this is where visible maprect should be set
mapView.showAnnotations(mapView.annotations, animated: true)
}
Besides showing the annotation, I needed to calculate polylines to connect the annotations and map finished loading was triggered too early.
Instead I tried mapViewDidFinishRenderingMap and it worked perfectly fine. See example below:
//MARK: - Show all objects after adding them on the map
func mapViewDidFinishRenderingMap(mapView: MKMapView, fullyRendered: Bool) {
mapView.showAnnotations(mapStages, animated: true)
}
You can try this.
I created an extension to show all the annotations using some code from here and there in swift 2.3. This will not show all annotations if they can't be shown even at maximum zoom level.
import MapKit
extension MKMapView {
func fitAllAnnotations() {
var zoomRect = MKMapRectNull;
for annotation in annotations {
let annotationPoint = MKMapPointForCoordinate(annotation.coordinate)
let pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
setVisibleMapRect(zoomRect, edgePadding: UIEdgeInsetsMake(20, 20, 20, 20), animated: true)
}
}