iOS Moving annotations on MapBox map - swift

Is it possible to move an annotation without removing and adding a new annotation?
I'd like to stick with MapBox because the future support of offline maps.
Thank you in advance!!

As of Mapbox iOS SDK v3.2.0, it is not possible to update the coordinates of an annotation that has been added to a map. Here is the relevant ticket on Github.

You can do a custom development for moving marker. I have done it and it's work as expected.
If you want to move a marker along your polyline, I would start by holding a reference to your MGLPointAnnotation and update its coordinate property on a timer or as the user moves along with it.

You can do this by keeping a reference to the annotation instance and then updating the coordinates of that instance.
If I assume you're adding your map (MGLMapView) to a View Controller, then a basic approach could be to add a property to that view controller so that you can keep track of the reference. For this example, I'll use an MGLPointAnnotation:
class ViewControllerWithAMap: UIViewController {
var movingPointAnnotation: MGLPointAnnotation?
...
}
Make this reference an optional so that you know when to initiate it and then just update the coordinates. The last part may seem counterintuitive, but all you really need to do now is add the annotation to the map again. For example:
if self.movingPointAnnotation == nil {
self.movingPointAnnotation = MGLPointAnnotation()
}
guard self.movingPointAnnotation != nil else {
print("What?! Could not initiate moving annotation.")
return // something weird, give up
}
self.movingPointAnnotation!.coordinate = myNewCLLocationCoordinate2D
self.mapView.addAnnotation(self.movingPointAnnotation!)
Regarding that last part where you add the annotation over and over again, I tried only adding the annotation once, and then just updating its coordinates afterward, but the annotation did not move.

Related

Swift PDFKit add annotation to beginning of page annotation array?

In Swift PDFKit is there a way to add an annotation to to the beginning of the page annotations array. I want to add my annotation to the beginning of the array because it is an image and I have other annotations I want to be able to edit that are ontop of it.
The only way I can find to add an annotation is with the following, but it adds it to the end of the array overlaying all other annotations that need to be editable.
page.addAnnotation(myannotation)
Thanks for any help!!
Here is a terrible solution, but it works . . .
I made a tmp list of all the annotations, remove all annotations on the page, and then added them all back with the image added first.
let list = page.annotations
for i in page.annotations{
page.removeAnnotation(i)
}
// add myannotation first
page.addAnnotation(myannotation)
// add other annotations back
for i in list {
page.addAnnotation(i)
}

How to access callout view on clicking on map annotation using swift in xctest

I want to access callout views and do some UIAutomation on those views. I'm able to click on map markers/annotations but not able to access the callout view.
The following code used to tap on the marker:
let marker = app.otherElements.matching(identifier: "mapMarker").element(boundby: 0)
marker.tap();
After this, I'm getting the callout view of the respected marker/annotation.
I need to access that callout.
Please suggest me on this.
You should create a breakpoint after the callout is snown,
then type po print(app.debugDescription) (or simply po app in XCode 11) in lldb in order to view the whole hierarchy of UI elements.
Locate the needed element and access it further in code.
Also, consider rewriting your marker code in a shorter way:
let marker = app.otherElements["mapMarker"].firstMatch
Please notice firstMatch aborts search of elements after it found the first one.
Drop firstMatch, if you want to check that the element is unique
let marker = app.otherElements["mapMarker"]
Same as Smart Monkey said, but to add more code based off the comment from ablarg:
Ex: "mapMarker" being the accessibility ID for the element
let mapMarker = app.maps.otherElements["mapMarker"].firstMatch
let mapMarkerExists = mapMarker.waitForExistence(timeout: 3)
if mapMarkerExists {
mapMarker.tap()
}
waitForExistence(timeout:) returns a bool, so if the element appears before the timeout expires (it finds the element) take action (tap) on the element.
Make sure the element is enabled for accessibility and has the accessibility ID set.

Is it possible to make MGLPolyLines selectable? - Swift, MapBox

Is it possible to make an MGLPolyLine touchable/selectable/have user interaction? In my project, the user needs to touch the polyline. There was this question asked before but it is outdated by about 2 years. Have they (MapBox) updated this?
I've just checked back and it looks like this has been implemented though I'm not sure which Mapbox release rolled this out.
If you take a look at the simple Mapbox example, Annotation Models, that demos an MGLPolyline and interspaced circular annotations, you can make a simple mod to the supplied code and see for yourself. The demo looks like this:
If you look into the viewController code, add a couple of lines below the polyline creation:
let polyline = CustomPolyline(coordinates: &coordinates, count: UInt(coordinates.count))
polyline.title = "Polyline" // New line
polyline.subtitle = "Pretty Poly". // New line
// Set the custom `color` property, later used in the `mapView:strokeColorForShapeAnnotation:` delegate method.
polyline.color = .darkGray
Now you can tap and see a basic callout:
This example subclasses MGLPolyline (CustomPolyline) so that its appearance can be altered slightly but that doesn't change anything with regards to the tappability.

No setBounds function for Leaflet imageOverlay

I'm reading an imageOverlay URL from an ArcGIS webserver that uses the leaflet getBound() coordinates as part of the URL (we have large maps that are filtered for the current window 'extent'). Apologies for not including the actual path (I'm working with sensitive client data). Eg:
http://myarcgiswebserver.com/MapServer/export/dpi=96&format=png32&bbox=27.119750976562504%2C-31.194007509998823%2C32.39044189453126%2C-29.692824739380754&size=1719%2C434
[bbox] = current imageBounds
When dragging my map the imageOverlay url is updated correctly but my leaflet window is no longer aligned to the imageBound values that were set when first adding the imageOverlay which results in a skewed output (this is my assumption):
The only workaround is to remove the existing imageOverlay and add a new one (which ruins the user experience as the map disappears then reappears each time the window is dragged or zoomed).
Am i approaching this problem incorrectly or would the introduction of a function to update the current imageBounds resolve this? Perhaps not a new function but the expansion of setUrl with additional parameters...?
Many thanks for any feedback...
As #ghybs pointed out, your use case might be better served by using the WMS
interface of your ArcGIS server.
Anyway, you say
The only workaround is to remove the existing imageOverlay and add a new one (which ruins the user experience as the map disappears then reappears each time the window is dragged or zoomed).
Well, that glitch is due to you probably doing something like:
Remove old overlay
Add new overlay
Wait until the image is received from the network
Wait one frame so the new overlay is shown
and instead you should be doing something like:
Add new overlay
Wait until the image is received from the network
Remove old overlay
Wait one frame so the new overlay is shown
The problem is just the async wait and the possible race conditions there, but should be easy to hack together, e.g.:
var activeOverlay = null;
var overlayInRequest = null;
map.on('moveend zoomend', {
// If we are already requesting a new overlay, ignore it.
// This might need some additional debouncing logic to prevent
// lots of concurrent requests
if (overlayInRequest) {
overlayInRequest.off('load', showOverlay);
}
overlayInRequest = L.imageOverlay( computeUrl( map.getBounds() ), myOverlayOptions );
overlayInRequest.on('load', showOverlay);
});
function showOverlay(ev) {
activeOverlay.remove();
activeOverlay = overlayInRequest;
activeOverlay.addTo(map);
overlayInRequest = undefined;
}
If you use an ImageOverlay but change its url dynamically, with a new image that reflects a new bounding box, then indeed that is the reason for the behaviour you describe: you display an image that has been generated using a new bbox, but positioned in the initial bbox, since the image overlay remains at the same geographical position on the map.
Instead, it sounds to me that you should use a TileLayer.WMS.
It would automatically manage the bounding box update for you. You may need to find the correct options to fit your service provider required URL syntax, though.
Example: http://playground-leaflet.rhcloud.com/yel/1/edit?html,output

How do I center and show an infobox in bing maps?

My code does a .pantolatlong then a .showinfobox
The info box does not appear, unless I remove the pantolatlong. I guess it is stopping it. I tried adding it to the endpan event but that did not work.
What is the simplest way to pan to a pushpin and display the infobox for it?
I was using setcenter, but I discovered that sometimes setcenter pans, and this breaks it.
After some insane googling, I came up with the solution, and I'll share it here so that others can hopefully not have the grief I went through.
I created and power my bing map using pure javascript, no sdk or iframe solutions. In my code, I generate the javascript to add all of the pins I want to the map, and inject it using an asp.net label.
If you call the setCenter() method on your Bing Map, it is supposed to instantly set the map, surprise surprise, to the coordinates you specify. And it does... most of the time. Occasionally though, it decides to pan between points. If you do a SetCenter, followed by a ShowInfoBox, it will work great, unless it decides to pan.
The solution? Being great programmers we are, we dive into the sdk, and it reveals there are events we can hook into to deal with these. There is an onendpan event, which is triggered after a pan is completed. There is also an onchangeview event, which triggers when the map jumps.
So we hook into these events, and try to display the infobox for our pushpin shape... but nothing happens. Why not?
You have to give it a few milliseconds to catch its breath, for unknown reasons, when the event is called. Using a setTimeout with 10 milliseconds seems to be fine. Your box will appear great after this.
The next problem is, you only want it to appear when it pans via whatever you used to make it flick between your pushpins (in my case, a table with onclick methods). I create/destroy the event handlers on the fly, although there are other options such as using a global variable to track if the user is panning, or if the system is panning in response to a click.
Finally, you have the one bug that comes from this. If you click a place in your list, and it jumps/pans to that location, the infobox will display fine. If the user dismisses it though, then clicks again on the list item, the map does not move, and therefore no events are triggered.
My solution to this is to detect if the map moved or not, by recording its long/lat, and using another setTimeout method, detecting if they changed 100ms later. If they did not, display the infobox.
There are other things you need to keep track of, as there is no way I can see to pass parameters to the eventhandlers so I use global javascript variables for this - you have to know which pushpin shape you are displaying, and also keep track of the previous mapcoordinates before checking to see if they changed.
It took me a while to piece all this together, but it seems to work. Here is my code, some sections are removed:
// An array of our pins to allow panning to them
var myPushPins = [];
// Used by the eventhandler
var eventPinIndex;
var oldMapCenter;
// Zoom in and center on a pin, then show its information box
function ShowPushPin(pinIndex) {
eventPinIndex = pinIndex;
oldMapCenter = map.GetCenter();
map.AttachEvent("onendpan", EndPanHandler);
map.AttachEvent("onchangeview", ChangeViewHandler);
setTimeout("DetectNoMapChange();", 200);
map.SetZoomLevel(9);
map.SetCenter(myPushPins[pinIndex].GetPoints()[0]);
}
function EndPanHandler(e) {
map.DetachEvent("onendpan", EndPanHandler);
setTimeout("map.ShowInfoBox(myPushPins[eventPinIndex]);", 10);
}
function ChangeViewHandler(e) {
map.DetachEvent("onchangeview", ChangeViewHandler);
setTimeout("map.ShowInfoBox(myPushPins[eventPinIndex]);", 10);
}
function DetectNoMapChange(centerofmap) {
if (map.GetCenter().Latitude == oldMapCenter.Latitude && map.GetCenter().Longitude == oldMapCenter.Longitude) {
map.ShowInfoBox(myPushPins[eventPinIndex]);
}
}
Here is another way:
function addPushpin(lat,lon,pinNumber) {
var pinLocation = new Microsoft.Maps.Location(lat, lon);
var pin = new Microsoft.Maps.Pushpin(map.getCenter(), { text: pinNumber.toString() });
pinInfobox = new Microsoft.Maps.Infobox(pinLocation,
{ title: 'Details',
description: 'Latitude: ' + lat.toString() + ' Longitude: ' + lon.toString(),
offset: new Microsoft.Maps.Point(0, 15)
});
map.entities.push(pinInfobox);
map.entities.push(pin);
pin.setLocation(pinLocation);
map.setView({ center: pinLocation});
}