2GIS to your advantage. As we added the card to Apple Watch

2GIS to your advantage. As we added the card to Apple Watch
Apple Watch quickly gained popularity and became the most popular watch in the world, ahead of Rolex and other manufacturers. The idea of ​​creating an application for watches is vital in the office of the 2GIS since 2015.
Before us, a full-fledged application with a map on the clock was released only by Apple itself. The Yandex.Map application displays only the traffic jams widgets and the travel time to home and work. Yandex.Navigator, Google Maps, Waze and Maps.Me are not available on the clock at all.
In fact, because of the many limitations of the system and the complexity in the development, companies either do not make applications for watches at all, or make them very simple. You can not just take a map on the clock. But we could.
Look under the cutline to see how the pet project has grown into a full-fledged product.
We decided to make a map. What was at the start?
Experience of development on the clock - 2 days of work on a test project.
Experience with SpriteKit - 0 days.
Experience writing MapKit - 0 days.
Doubts that something can go wrong - ∞.
Iteration 1 - flight of thought
We are serious people, so we decided to work out a plan first. We considered that we are working in a rigidly planned sprint, we have five story points for "small-scale tasks" and complete ignorance of where to begin.
A map is a very large picture. We can show the pictures on the clock, so we will cope with the display of the map.
We have a service that can cut a card into pieces:

If you cut this picture and put it in WKImage, you get the simplest working prototype for five cents.
And if you add PanGesture to this picture and install a new picture on each svayp, we get a simulation of the interaction with the map.
/Rejoice /It sounds awful, looks about the same, works even worse, but, in fact, the task is completed.
Iteration 2 is the minimum prototype of
Continuous loading of pictures costs the battery in hours. And the load time itself suffers. We wanted to get something more full and responsive. We could hear from the edge of the ear that the clock supports SpriteKit - the only framework under WatchOS, with the ability to use the coordinates, zoom and customize all this magnificence for yourself.
After a couple of hours of StackOverflow Driven Development (SDD) we get the second iteration:
One SKSpriteNode, one WKPanGestureRecognizer.

/We are happy /Yes this is MapKit for 6 cents, completely working. Urgently in release!
Iteration 3 - add tiles and zoom
When emotions were asleep, wondered where to go next.
Understood what is most important:
Replace the picture with tiles.
Put 4 tiles into the bundle of the application and merge them together.
Provide a zoom picture.
Let's throw 4 tiles into the bundle of the application, then put them on a certain:
let rootNode = SKSpriteNode ()

with the help of simple mathematics we will put them together.
Zoom is done through WKCrownDelegate:

    internal func crownDidRotate (
_ crownSequencer: WKCrownSequencer ?,
rotationalDelta: Double
) {
self.scale + = CGFloat (rotationalDelta * 2)
self.rootNode.setScale (self.scale)



/Rejoice /Well now it's for sure! A couple of fixes, and in the master.


Iteration 4 - optimize the interaction with the card


The next day it turned out that for SpriteKit anchorPoint does not affect the zoom. The zoom completely ignores the anchorPoint and takes place relative to the rootNode center. It turns out that for each step of the zoom we need to adjust the position.


It would also be nice to load tiles from the server, and not store the whole world in the memory of the clock.
Do not forget that the tiles should be tied to the coordinates so that they do not lie in the center of SKScene, but on the appropriate places on the map.


Tiles look like this:



For each zoomLevel ("z"), there is a set of tiles. For z = 1 we have 4 tiles make up the whole world.



for z = 2 - in order to cover the whole world, you need already 16 tiles,
for z = 3 - 64 tiles.
for z = 18 ≈ 68 * 10 ^ 9 tiles.
Now they need to be put in the world of SpriteKit.


The size of one tile is 256 256 pt, then
for z = ? the size of the "world" will be 512
512 pt,
for z = ? the size of the "world" will be 1024 * 1024 pt.
For the sake of simplicity, let's put the tiles into the world as follows:



We encode the tile:

    let kTileLength: CGFloat = 256
struct TilePath {
let x: Int
let y: Int
let z: Int


We define the coordinate of the tile in such a world:

    var position: CGPoint {
let x = CGFloat (self.x)
let y = CGFloat (self.y)
let offset: CGFloat = pow (? CGFloat (self.z - 1))
return CGPoint (x: kTileLength * (-offset + x),
y: kTileLength * (offset - y - 1))
var center: CGPoint {
return self.position + CGPoint (x: kTileLength, y: kTileLength) * ???r3r3368.}


Location is convenient, because it allows you to bring everything into the coordinates of the real world: latitude /longitude = ? which is just in the center of the "world".


latitude /longitude of the real world are transformed into our world as follows:

    extension CLLocationCoordinate2D {
//relative position in the world (-1

? func tileLocation () -> CGPoint {
var siny = sin (self.latitude * .pi /180)
siny = min (max (siny, -1), 1 )
let y = CGFloat (log ((1 + siny) /(1-siny)))
return CGPoint (
.x: kTileLength * (0.5 + CGFloat (self.longitude) /360),
: kTileLength * (0.5 - y /(4 * .pi))
//absolute position in the world for the desired zoomLevel
func location (for z: Int) -> CGPoint {
let tile = self.tileLocation ()
let zoom: CGFloat = pow (? CGFloat (z))
let offset = kTileLength * ???r3r3368 return CGPoint (
.x: (tile.x-offset) * zoom ,
y: (-tile.y + offset) * zoom

With zoom, the lefties have problems. I had to spend a couple of days off, in order to assemble the whole mathematical apparatus and provide an ideal merger of the tiles. That is, the tile for z = 1 should ideally go to four tiles for z = 2 at zoom and vice versa, the fourth tiles for z = 2 must go to one tile for z = 1.

In addition, it was necessary to turn the linear zoom into exponential, since the zoom changes from 1 <= z <= 18, а карта масштабируется, как 2^z.
Smooth zoom is ensured by constant tile position adjustment. It is important that the tiles are stitched exactly in the middle: that is, that the level 1 tile should go to 4 tiles of level 2 with a zoom of 1.5.
SpriteKit under the hood uses float. For z = 1? we get the coordinate spread (-???/???), and the accuracy of the float is 7 bits. At the output, we have an error in the region of 30 pt. To avoid the occurrence of "gaps" between the halves, place the visible tile as close to the center of SKScene.
/Rejoice /After all these movements we got a prototype ready for testing.
Since the application did not really have TK, we found a couple of volunteers to do a little testing. Special problems were not found, and they decided to roll out into a stop.
After the release, it turned out that on the clock of the first series the processor does not have time to draw the first frame of the card in 10 seconds and falls on timeout. It was necessary to initially create a card completely empty, in order to keep within 10 seconds, and then gradually load the substrate. First, develop all levels of the map - and then load the tiles for them.
It took a lot of time to debug the network, configure the cache correctly and a small Memory Footprint so that WatchOS does not try to kill our application for as long as possible.
After profiling the application, we found out that instead of the usual tiles, you can use Retina tiles, almost without harm to the load time and memory consumption, and have already switched to them in the new release.
Results and plans for the future
We can already display the route on the map with the maneuvers built in the main application. The function will be available in one of the next releases.
The project, which initially seemed unrealistic, proved to be extremely useful to me personally. Often I use the application to understand, whether soon to leave on the necessary stop. I believe that in the winter it will be even more useful.
At the same time, I was once again convinced that the complexity of the project, the belief of others in the success of the task or the availability of free time at work is not so important. The main thing is a desire to make a project and a tedious, gradual movement towards the goal. As a result, we have a full-fledged MapKit, which is almost unlimited and works with 3 WatchOS. It can be modified as you want, without waiting for Apple to roll out a suitable API for development.
For those interested, I can lay out the finished project. The code level is far from production. But, according to the military principle, it does not matter how it works, the main thing that works!
+ 0 -

Add comment