Utilizzare mappe offline su iOS e OSX, Custom & Off-line Maps Using Overlay Tiles

IOS_OSX_maps_OFFLINENuove funzionalità MapKit

Durante il WWDC 2013 Apple ha introdotto molte nuove funzionalità tra le quali il MapKit per iOS 7 e ha aggiunto questo framework in OSX 10.9 (Mavericks). Uno dei principali cambiamenti, che a mio parere non hanno ottenuto abbastanza rilevanza nella comunità degli sviluppatori, è stata l’introduzione di alcune classi di base che permettono la completa personalizzazione e il supporto per le mappe offline. In questo articolo ho intenzione di descrivere la nuova classe MKTileOverlay e presentare un esempio, sia per iOS e OSX, che dimostra le nuove funzionalità.

Per le versioni precedenti di iOS c’erano molte applicazioni che in App Store  supportavano mappe differenti da quelle fornite dal sistema operativo: consideriamo ad esempio le applicazioni di navigazione che hanno richiesto il supporto per la navigazione offline, cioè la possibilità di vedere la mappa anche senza connessione ad internet. Un altro requisito per quel particolare tipo di applicazioni che aveva bisogno di mostrare informazioni proprietarie (come “Pagine Gialle” apps) o le informazioni tecniche (ad esempio, quando ci fu l’esigenza di mostrare le curve di livello per la montagna o per rappresentare il livello del mare).

Ci sono stati diversi problemi a causa di questa limitazione: prima di tutto l’esperienza complessiva della mappatura era completamente diversa da questi diversi approcci e nella maggior parte dei casi era scadente se confrontato con il rendimento delle mappe del sistema operativo (sia con  dati di Apple che Google). Inoltre dal punto di vista dello sviluppatore c’erano il problema di fornire il giusto codice mappatura  per supportare i dati della mappa del fornitore: non c’era una soluzione unica, ma molte. Alcuni frameworkd erano commerciali e costosi, altri erano open source, ma con la mancanza di supporto e, infine, c’erano un sacco di soluzioni basate su browser web le cui prestazioni erano lontanie dalle mappe native e diverse da quelle difficili da integrare con Objective-C.

Quello che stiamo per mostrarvi in questo articolo è come sono cambiate drasticamente queste cose e come è facile integrare i propri contenuti nella mappa all’interno del framework common MapKit.

Map overlays
Alla base della nostra discussione c’è il concetto di “map overlay”. Questo non è nuovo in MapKit, ma con iOS7 le cose sono cambiate. Le sovrapposizioni sono essenzialmente parte di una mappa che può essere sovrapposta sulla mappa base, cioè la parte della mappa che rappresenta la terra, i confini, le strade, e così via. In genere l’uso di sovrapporre è quello di sottolineare alcune regioni della mappa avere una proprietà comune: ad esempio, per evidenziare un determinato paese o per rappresentare le varie intensità di un terremoto che si è verificato in una determinata area o al fine di evidenziare un percorso di strada in un app per la navigazione.

Dal punto di vista dello sviluppatore , un overlay è qualsiasi oggetto conforme al protocollo MKOverlay . Questo protocollo definisce le proprietà e i metodi minimi necessari per definire una sovrapposizione : questi sono la coordinata al centro approssimativo e il rettangolo di selezione che racchiude completamente la sovrapposizione (boundingMapRect). Queste due proprietà consentono a MapKit di determinare se un overlay specifico è attualmente visibile o meno sulla mappa in modo che il framework possa adottare le azioni necessarie per visualizzare questo overlay. Quando un oggetto overlay viene aggiunto alla mappa utilizzando un addOverlay del MKMapView i metodi il controllo passano al framework che , quando determina che un overlay specifico deve essere visualizzato , chiama il map view delegate chiedendogli di fornire la rappresentazione grafica dell’ overlay . Prima di iOS7 Apple forniva un insieme di classi compatibili al MKOverlay  e sono state associate al loro MKOverlayView corrispondente. Ad esempio per rappresentare una sovrapposizione circolare abbiamo potuto usare la classe incorporata MKCircle e quindi fornire , per il rendering , la classe MKCircleView associata , senza la necessità di definire il nostro oggetto.

Con iOS7 le cose sono cambiate: ora il MKOverlayView è stato sostituito dal MKOverlayRenderer. Anche se questo cambiamento non richiede un difficile refactoring per tradurre il codice pre-iOS7 a iOS7, grazie al fatto che Apple ha fatto una mappatura 1:1 dei metodi della vecchia classe per la nuova classe, concettualmente il cambiamento è significativo: ora la rappresentazione grafica della sovrapposizione non è più fornita da una sottoclasse UIView, che in genere è considerata una classe pesante, ma essa è fornito da una classe, MKOverlayRenderer, che è molto più leggera e discende direttamente da NSObject. Tuttavia il mapping tra la classe di vecchio e nuovo è completo, quindi nell’esempio di un cerchio possiamo vedere MKCircleView sostituito da MKCircleRenderer.

Infine le sovrapposizioni sono impilate sulla mappa, in modo che viene dato un Z-index che fornisce la posizione relativa delle sovrapposizioni tra loro e con le parti fisse della mappa. Prima di iOS7 potevate impilare (stacks) queste sovrapposizioni e definire le loro posizioni in un array, con iOS7 due stacks sono definiti , e vengono chiamati “livelli”: un livello è “al di sopra del livello strade”, l’altro livello è la “sopra del livello etichette “. Questa è una distinzione importante e utile perché ora possiamo cambiare come il rendering overlay  interagisce con la mappa specificando se si trova sopra o sotto le etichette.

Tile overlays

Qualunque sia la complessità e la dimensione della sovrapposizione (overlay) , li abbiamo visti fino ad ora sovrapporsi come forme specifiche. Con il nuovo MapKit fornito con iOS 7 e OSX Mavericks , c’è un nuovo tipo di overlay chiamato tiled overlay . Si può considerare questo tipo di sovrapposizione come un particolare strato di copertina su tutta la mappa : grazie alle sue grandi dimensioni, questa sovrapposizione è “piastrellata” (tiled) , cioè è divisa in bitmap areas per ridurre la memoria necessaria per visualizzare i dati e rendere  rendering overlay efficiente. Lo scopo di questa implementazione concreta del protocollo MKOverlay , chiamato MKTileOverlay ( insieme alla sua controparte di rendering in classe MKTileOverlayRenderer ) , è quello di rappresentare efficacemente l’intera serie di tiles (piastrelle) sullo mappa piana e per diversi livelli di zoom . Questo ultimo punto è importante : quando si sta visualizzando una mappa utilizzando il disegno bitmap ( da confrontare con il disegno vettoriale) è possibile ottenere un’implementazione efficiente solo se la bitmap specifica che rappresenta una parte di un’area della mappa ha i dettagli adatti per la livello di zoom corrente . Ciò significa che se mostriamo la mappa completa dell’Europa non abbiamo bisogno di presentare le strade e le città che dovrebbero essere rappresentate come punti e solo per i più grandi , appena ci focalizziamo in una determinata area , allora non possiamo continuare a rappresentare l’area scalando lo stesso tile, perché non contiene le informazioni richieste e anche perché ci piacerebbe vedere effetti di scala evidenti . La soluzione  è quella di dividere la gamma di zoom su un continuo dei livelli discreti e per ogni livello quindi fornire il set di tiles richiesti che mostrerà i dettagli del caso per i livelli . E ‘ evidente che se teniamo la costante di formato delle “mattonelle” bitmap singole (ad esempio 256 x 256 pixel ), poi per ogni livello di zoom dobbiamo aumentare il numero di tiles (tessere/mattonelle) di un fattore 4 : si può vedere questo nella foto qui sotto : l’ unico tile europeo  a livello di zoom 3 , quando lo zoom a livello di zoom 4 è stato diviso , e dettagli ulteriormente , con quattro nuove tessere diverse , aventi tutte la stessa dimensione del tile originale .

map_tiles_offline-mapping

Modelli di URL (URL TEMPLATES)

La tiled overlay class funziona in modo efficiente , ad esempio fa un lazy loading dei tiles : questo significa che un bitmap tile viene cercato e caricato solo quando è necessario per essere visualizzato . Per conoscere la posizione del tile , lo sviluppatore deve definire nella definizione dell’overlay tile il cosiddetto URL template . Questa è una stringa che rappresenta un modello per l’URL finale che verrà utilizzato per recuperare il tile : questo template conterrà alcuni segnaposti che verranno sostituiti da valori efficaci per ottenere l’URL finale . Ogni tile può essere caratterizzato da 4 parametri : x e y per gli indici tiles nella mappa piana , z per il livello di zoom e infine per ridimensionare la risoluzione dell’immagine bitmap ( fattore di scala ) . I segnaposto corrispondenti per questi parametri sono : { x } { y } { z } {scale }  . Così ad esempio , il templates URL OpenStreetMap sarà http://c.tile.openstreetmap.org/{ z }/{ x }/ {y }.png e ad esempio il tile con indice X = 547 Y = 380 e zoom livello Z = 10 , che racchiude completamente la città di Roma , sarà rappresentato dalla URL: http://c.tile.openstreetmap.org/10/547/380.png ( vedi sotto l’ immagine presa dalla nostra OSX demo app ).

iOS_offline_map_rome_tile

Si noti che un modello di URL (template) può essere un  http:// template per recuperare i tiles da internet, ma potrebbe anche essere un file :/ / template se vogliamo recuperare i file dal disco: in questo modo possiamo salvare i nostri tiles nell’ application bundle, o scaricare e installare un pacchetto completo di tiles per una certa città, e quindi visualizzare le mappe anche se il dispositivo non è connesso a Internet.

Il meccanismo che viene utilizzato dal framework per tradurre un tiles richiesto da coordinate (x, y, z; scala) a una bitmap efficace è composto da diversi passaggi: questo dà allo sviluppatore la possibilità di agganciare il proprio codice per personalizzare efficacemente il modo in cui i tiles vengono generati. Questo può essere fatto dalla sottoclasse MKOverlayTile. Si noti che questo non è necessario se l’impostazione del URL template è abbastanza per voi.

Quando the map framework necessita uno specific map tile, chiama il loadTileAtPath:result: del MKOverlayTile class (or subclass):

- (void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *tileData, NSError *error))result;

Il primo metodo argument e’ chiamato path ed è una struttura MKTileOverlayPath  che contiene il tile coordinate:

typedef struct {
	NSInteger x;
	NSInteger y;
	NSInteger z;
	CGFloat contentScaleFactor; // The screen scale that the tile will be shown on. Either 1.0 or 2.0.
} MKTileOverlayPath;

Il secondo argomento del metodo è un blocco di completamento che deve essere chiamato quando i dati tiles sono stati ritirato: questo blocco completamento sarà chiamato passando i dati e un oggetto errore. L'implementazione predefinita MKTileOverlay chiamerà il metodo-URLForTilePath per recuperare l'indirizzo e poi userà NSURLConnection per caricare i dati tiles in modo asincrono.

Se vogliamo personalizzare il comportamento dei tiles di carico si può usare facilmente la sottoclasse MKTileOverlay e ridefinire il loadTileAtPath:result: con la nostra implementazione del meccanismo di caricamento. Ad esempio possiamo realizzare il nostro meccanismo di di tiles caching (diverso da quello fornito dal sistema tramite NSURLConnection) per restituire i dati memorizzati nella cache prima di far scattare la chiamata di rete, o potremmo usare un watermark per default tile se stiamo spedendo una versione freemium della nostra mappa offline.

Un modo più semplice per collegare il meccanismo di tiles di carico è di ridefinire la nostra sottoclasse -URLForTilePath: method:

1
- (NSURL *)URLForTilePath:(MKTileOverlayPath)path;

Lo scopo di questo metodo è quello di restituire l’URL specificato il percorso del tile. L’implementazione di default è solo per compilare il modello di URL, come sopra specificato. È necessario ridefinire questo metodo se il meccanismo di template URL non è sufficiente per le vostre esigenze. Un caso tipico è quando si vuole passare l’URL di una sorta di “identificatore del dispositivo” per confermare l’ammissibilità di quella specifica applicazione per accedere all’URL (ad esempio, se si fornisce un limite alla quantità di dati che può essere letto da un utente in un determinato momento o se si vuole far pagare per questi dati), un altro caso, se si dispone di più server di tiles e si vuole fare una sorta di bilanciamento “in-app” load balancing o regional-based API access (ad esempio si dispone di server in più sedi e in base alla posizione efficace il dispositivo desidera accedere al server più vicino).

Il Tile renderer

Come tutteìi gli overlay  (le sovrapposizioni) sono associati a un renderer, anche il tile overlay  ha la sua concreta classe del renderer : MKTileOverlayRenderer. Normalmente non è necessaria la sottoclasse di questo renderer così il vostro map delegate  -mapView:rendererForOverlay: method può semplicemente creare un’istanza di default tile overlay renderer inizializzato con il valore predefinito o la sottoclasse tile overlay instance. Possibili applcationi di un renderer overlay personalizzati sono quando si ha bisogno di manipolare ulteriormente l’immagine bitmap, ad esempio aggiunta di una filigrana (watermark) o per applicare un filtro, e questa manipolazione è indipendente dalla sorgente tile. Nel codice demo ho definito un renderer personalizzato da utilizzare specificamente per la mappa di Google, il cui effetto è quello di aggiungere una sorta di mosaico colorato traslucido sopra le tiles.

Il codice demo

È possibile ottenere il codice demo dal Github. Questo codice funziona sia su iOS 7 e OSX 10.9 e il suo scopo è quello di presentare una mappa e dare all’utente la possibilità di passare tra diversi set di tiles: Apple (sistema), Google, OpenStreetMap e offline da un sottoinsieme di tiles di OpenStreetMap in bundle all’interno dell’ app. In tutti i casi ho applicato uno strato di sovrapposizione supplementare per visualizzare la griglia tile con la x, y, z path associato ad ogni griglia. (Nota: in OSX se non segno di codice del prodotto utilizzando un certificato di OSX Developer Program, non sarete in grado di vedere i tiles di Apple: gli altri tre set di tiles saranno invece visibili ). Vedrete come è possibile sfruttare appieno tutte le caratteristiche comuni ai MapKit (zoom, rotazione, pan, sovrapposizioni personalizzate e anche le annotazioni che non ho incluso nella demo) e l’unica differenza è nella fonte tiles e come sono resi.

map-offline-tiles-demo_codeCome si può vedere nell’ applicazione demo, c’è main view controller (iOS) e un window controller (OSX). In entrambi i casi la vista principale contiene un’istanza di MKMapView e un controllo segmentato per passare tra le diverse visualizzazioni. Sulla mappa ho istanziato due sovrapposizioni. Il primo è la sovrapposizione della griglia:

// load grid tile overlay
 self.gridOverlay = [[GridTileOverlay alloc] init];
 self.gridOverlay.canReplaceMapContent=NO;
 [self.mapView addOverlay:self.gridOverlay level:MKOverlayLevelAboveLabels];

Questa è un tile overlay di sottoclasse GridTileOverlay. Esso non sostituisce il contenuto mappa (ciò significa che è efficace sovrapposto sul contenuto mappa) e il suo scopo è di attirare, appena sopra le etichette, il tile grid (griglia delle tessere).

Il metodo reloadOverlay viene chiamato ogni volta che il selettore di tipo overlay è cambiato o quando viene caricata la vista. Rimuove qualsiasi tileOverlay esistente e lo sostituisce con uno nuovo:

-(void)reloadTileOverlay {

	// remove existing map tile overlay
	if(self.tileOverlay) {
		[self.mapView removeOverlay:self.tileOverlay];
	}

	// define overlay
	if(self.overlayType==CustomMapTileOverlayTypeApple) {
		// do nothing
		self.tileOverlay = nil;
	} else if(self.overlayType==CustomMapTileOverlayTypeOpenStreet || self.overlayType==CustomMapTileOverlayTypeGoogle) {
		// use online overlay
		NSString *urlTemplate = nil;
		if(self.overlayType==CustomMapTileOverlayTypeOpenStreet) {
			urlTemplate = @"http://c.tile.openstreetmap.org/{z}/{x}/{y}.png";
		} else {
			urlTemplate = @"http://mt0.google.com/vt/x={x}&y={y}&z={z}";
		}
		self.tileOverlay = [[MKTileOverlay alloc] initWithURLTemplate:urlTemplate];
		self.tileOverlay.canReplaceMapContent=YES;
		[self.mapView insertOverlay:self.tileOverlay belowOverlay:self.gridOverlay];
	}
	else if(self.overlayType==CustomMapTileOverlayTypeOffline) {
		NSString *baseURL = [[[NSBundle mainBundle] bundleURL] absoluteString];
		NSString *urlTemplate = [baseURL stringByAppendingString:@"/tiles/{z}/{x}/{y}.png"];
		self.tileOverlay = [[MKTileOverlay alloc] initWithURLTemplate:urlTemplate];
		self.tileOverlay.canReplaceMapContent=YES;
		[self.mapView insertOverlay:self.tileOverlay belowOverlay:self.gridOverlay];
	}
}

 

Nel caso di mappe di Apple nessuna sovrapposizione in più è stata aggiunta, naturalmente : basta usare la mappa di base . Quando si sceglie di visualizzare il Google e OpenStreetMap useremo una classe MKTileOverlay standard con il modello URL appropriato . In entrambi i casi verrà aggiunta la sovrapposizione con la proprietà set canReplaceMapContent su YES : questo  sostituisce completamente la base Apple Map  e si eviterà che questi dati vengano caricati . Si noti che aggiungiamo la tileOverlay appena sotto il gridOverlay . Infine, il caso non in linea utilizza ancora una base overlay class , ma con un modello di file URL : notiamo che creiamo il percorso da una struttura di directory gerarchica  all’interno del bundle. In questo caso i nuovi tiles sostituiscono quelli di base e vengono inseriti sotto la griglia .

Il nostro controller , essendo un delegato di MKMapView , risponde al – MapView : rendererForOverlay : . Questo è richiesto da ogni applicazione che utilizza sovrapposizioni in quanto questo è il punto in cui l’applicazione dice efficacemente al sistema come disegnare una sovrapposizione che è attualmente visibile nella mappa . Nel nostro caso abbiamo appena controllato che l’overlay è una sovrapposizione di tiles (questo è un caso generale nel considerare il fatto che potremmo avere altri tipi di sovrapposizioni ), e in base alla selezione usiamo lo standard MKTileOverlayRenderer o un WatermarkTileOverlayRenderer renderer personalizzato . Quest’ultimo è utilizzato per applicare un effetto semitrasparente colorato in modo casuale sulla cima dei tiles , ottenendo come risultato un effetto mosaico vetroso (vitreous mosaic effect).

Conclusioni

La possibilità di passare facilmente tra diversi tipi di mappa , ma mantenendo la stessa ” esperienza  di navigazione mappa” è una delle caratteristiche più rivoluzionarie introdotte con iOS 7 , altro che la lunga attesa per l’introduzione di mappe native all’interno OSX . Questo fornisce la stessa infrastruttura di mappa qualunque sia il contenuto . Ovviamente la generazione di mappe personalizzate  è un altro compito enorme e altamente specializzato che non possiamo trattare qui : ma per gli sviluppatori questo è un grande passo avanti .

Bibliografia

  • Location and Maps Programming Guide from Apple Developer Library
  • WWDC 2013 session 304 video: What’s new in Map Kit from Apple WWDC 2013 videos
  • MBXMapKit GitHub project by Mapbox – A simple library to intergrate Mapbox maps on top of MapKit, one of the first applications of tiled overlays

Posted by Carlo Vigiani

3 commenti:

  1. I totally consider that this is an incredible web site and I will be returning to
    read even more.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

*