Formato MBTiles

Mapa mundi

El formato MBTiles es una base de datos SQLite, que se utiliza para guardar mapas. El mapa puede ser de tipo raster o vectorial. En esta sección explicaré la estructura de datos (tablas) y valores para obtener los mapas de tipo raster.

El mapa puede tener varios zooms, y cada zoom está compuesto de imágenes cuadradas de 256 pixeles (256 pixeles de alto X 256 pixeles de ancho). Estas imágenes se les denominan tiles (azulejos).

No es necesario tener un access para lo que se va a contar a continuación, pero las capturas de pantalla son obtenidas del SQLiteStudio (https://sqlitestudio.pl/).

Tablas

MBTiles puede tener varias tablas, para obtener las imágenes raster sólo nos interesan 2 tablas: METADATA y TILES.

Tabla METADATA

Contiene información básica del mapa.

Esta tabla consta de 2 campos. NAME y VALUES.

NAME
Es el nombre de una propiedad
VALUES
Es el valor de dicha propiedad.

Algunas propiedades

Obligatorias:

name
Nombre del mapa.
format
Formato de las imágenes (tiles). (pbf, jpg, png,…)

Recomendadas (por lo que algunos ficheros pueden no tener dichas propiedades):

bounds

Define el perímetro del mapa. Las coordenadas LONGITUD, LATITUD están representadas en WGS84 y separadas por comas. Las coordenadas son izquierda, abajo, derecha, arriba.

Por ejemplo la tierra completa sería -180.0,-85,180,85.

Las coordenadas oeste y sur son negativas y las coordenadas este y norte son positivas.

minzoom
Zoom mínimo del mapa.
maxzoom
Zoom máximo del mapa.

Tabla TILES

Contiene las imágenes para formar el mapa.

Esta tabla consta de 4 campos:

zoom_level
Zoom al que pertenece el tile.
tile_column
Columna del tile.
tiel_row
Fila del tile
tile_data
Imagen en el formato definido la propiedad FORMAT de la tabla METADATA.
Tablas

Partiendo del fichero countries-raster.mbtiles. Podemos ver las 2 tablas comentadas anteriormente.

Haciendo la siguiente SQL sobre la tabla metadata.

SELECT *
FROM metadata

Obtenemos los siguientes valores.

attribution	
center	0,0,2
format	png
description	
bounds	-180,-85.738076382392,180,84.79842793857
minzoom	0
maxzoom	6
name	Countries

Vemos que el mapa tiene definidos 7 zooms (minzoom=0 maxzoom=6). El zoom mínimo no tiene porque empezar en 0.

Debido a que minzoom y maxzoom no son unas propiedades obligatorias, es mejor obtener los zooms que contiene un fichero a través de la tabla TILES con la siguiente SQL.

SELECT DISTINCT zoom_level
FROM tiles;

O

SELECT MIN(zoom_level)
FROM tiles;

SELECT MAX(zoom_level)
FROM tiles;

Las imágenes (tiles) son del formato png.

Los límites del mapa van desde la longitud 180 O, latitud 85.738076382392 S, a la longitud 180 E, latitud 85. 79842793857 N.

Si hacemos una SQL para obtener los tiles del zoom 1.

SELECT *
FROM tiles
WHERE zoom_level=1
SELECT FROM tiles

Vemos que el zoom 1 está formado por 4 tiles.

Podemos ver el tile_data haciendo doble clic.

Por ejemplo, el tile de la columna 0 fila 0.

Editar valores, pestaña Hex

Si vamos a la pestaña imagen. Vemos la imagen del tile.

Editar valores, pestaña Image

El zoom 1 del mapa estaría formado por los siguientes tiles.

Mapa, zoom 1

Vemos como el primer tile 0,0 está en la parte inferior izquierda de la ventana y el último tile 1,1 está en la parte superior derecha, es decir el origen de coordenadas está en la parte inferior izquierda, al revés de cómo se dibuja en las pantallas de los ordenadores donde el origen de coordenadas está en la parte superior izquierda.

No siempre los mapas están tan bien definidos, podemos encontrarnos cosas tan curiosas como estas.

Mapa curioso

Donde se puede apreciar que no hay tiles en las filas 316,315, de la columna 254

Referencias

Especificación del formato MBTiles

https://github.com/mapbox/mbtiles-spec

Dibujar/Ver MBTiles

En esta sección partiendo de los ficheros (CMarco.cpp y CFicheroMBTiles.cpp), voy a intentar explicar, cómo utilizar la clase CFicheroMBTiles (CFicheroMBTiles.h, CFicheroMBTiles.cpp) para visualizar un fichero MBTiles.

No va a ser una disección sobre la implementación de dicha clase y de cómo está implementada ni una explicación pormenorizadas de sus funciones y algoritmos.

El entorno de desarrollo en que se han desarrollado estos fuentes ha sido en eclipse para el framework wxWidgest.

Quien quiera utilizar el proyecto, deberá indicar donde están el compilador, los includes, las librerías de wxWidgets y de SQLite. Esto depende de la instalación que de cada uno.

Primero indicaremos donde está el compilador de C/C++.

En propiedades del proyecto(Project->Properties) en C/C++Build->Enviorement.

Opciones de Environment
Opciones de Environment

La variable MINGW_HOME deberá apuntar a la ubicación de nuestro compilador.

Variables del IDE
Variables del IDE

En mi caso utilizo mingw.

El siguiente paso, en indicar donde están las librerías, includes, etc.

En C/C++Build->Settings -> GCC C++ Compiler.

Includes.

Opciones de include
Opciones de include

Indicaremos dónde están los includes (include paths e includes files) para wxWidgets, que debe de apuntar en nuestra instalación de wxWidgest.

En mi caso es:

D:\Lenguajes\wxWidgets-3.1.0\lib\gcc_lib32_4_8_1\mswu

D:\Lenguajes\wxWidgets-3.1.0\include

Para SQLite.

D:\herramientas\db\sqlite\v3.55.5\fuentes

Include files

D:\Lenguajes\wxWidgets-3.1.0\include\wx\bitmap.h

Por último habrá que definir donde están las librerías (C/C++Build->Settings -> MinGW C++ Linker).

Opciones de herramientas
Opciones de herramientas

En mi caso las librerías están en en D:\Lenguajes\wxWidgets-3.1.0\lib\gcc_lib32_4_8_1 para wxWidges y en D:\herramientas\db\sqlite\v3.55.5\lib\lib32bits, para SQLite.

Una vez hechos estos pasos ya podemos compilar el proyecto.

Los fuentes (app.h,app.cpp, CMarco.h y CMarco.cpp) son los fuentes que implementa la aplicación (app) y la ventana (CMarco).

Los fuentes CFicheroMBTiles.h y CFicheroMBTiles.cpp implementan la clase CFicheroMBTiles y no son dependientes del framewok wxWidgest, por lo que se podría usar Windows o las MFCs de Windows o cualquier otro framework.

Lo primero será instanciar la clase CFicheroMBtiles.

CMarco.h

CFicheroMBTiles mFileTiles;

Después habrá que cargar el fichero MBTiles (CFicheroMBTiles.leerFichero). Esta función no carga los tiles, esta función básicamente lee la tabla METADATA para obtener información del fichero.

CMarco.cpp
CMarco::onAbrirFichero

lchEstado=mFileTiles.leerFichero((const char*)lstrPath.c_str());

Si todo ha ido correctamente la variable lchEstado tendrá el valor 0, un valor inferior a 0 será un error.

Obtenemos el número de zooms (CFicheroMBTiles.obtNumZooms) que tiene el fichero y generar un menú por cada zoom del fichero.

CMarco.cpp

CMarco::crearMenuItemZooms

luiNumZooms=mFileTiles.obtNumZooms();

En la clase CFicheroMBTiles, guardamos en un vector los zooms que forma el mapa.

Para obtener un determinado zoom accederemos a través el índice del vector (CFicheroMBTiles.obtInfoPosZoom). Dicho índice va desde 0 (min zoom) hasta Número de Zooms-1 (max zoom).

liZoom=mFileTiles.obtInfoPosZoom(luiIndi);

Una vez seleccionado un zoom (desde el menú de zooms), podremos cargar los tiles que forma dicho zoom en memoria (CFicheroMBTiles.cargarTiles).

CMarco.cpp
CMarco:: onCargarTile

mFileTiles.cargarTiles(miZoom,&luiNumTilesZoom)

En la variable luiNumTilesZoom devuelve el número de tiles que forma un determinado zoom.

Ahora en la clase CFicheroMBTiles, tenemos guardados los tiles del zoom que hemos cargado.

Estos tiles debemos de pasarlos a un bitmap, que será lo que dibujará nuestra aplicación.

Para hacer esto, primero definimos la estructura de datos (typdef(imagen) y un vector) que está definida por una estructura imagen. Donde guardaremos la posición x,y donde se dibujara el bitmap (tile), y la imagen del tile.

CMarco.h

typedef struct
{
 // guardamos la posicion x, y, donde se dibujara del bitmap
 wxPoint posBitmap;
 wxBitmap *graf;
} imagen;

Los tiles pasados a bitmap, los guardamos un vector (mVecBitmap).

std::vector mVecBitmap;

Una vez hemos cargado los tiles para un determinado zoom (CFicheroMBTiles.cargarTiles) debemos de pasar esos tiles a bitmaps, por lo que llamaremos a la función CMarco.tilesToAzulejos.

CMarco.cpp
CMarco::onCargarTile

if (luiNumItems>0)
 tilesToAzulejos();

Básicamente esta función se encargará de obtener los tiles cargados a través de la función CFicheroMBTiles.obtTileByIndice.

CMarco.cpp
CMarco::tilesToAzulejos

Tile=mFileTiles.obtTileByIndice(luiIndi,&lchEstado);

Y “traducirlo” a un bitmap a través de la función CMarco.creaBitmap

CMarco.cpp
CMarco::tilesToAzulejos

lpBitmap=creaBitmap(lTile.pRawImg,lTile.sizeBytes,liFila,liColumna);

Pasamos los datos de la imagen (tile) a un bitmap.

CMarco.cpp
CMarco::creaBitmap

Utilizando la clase wxMemoryInputStream, donde se le pasara los datos de la imagen(ppRaw) y el tamaño de la misma en bytes (piSize).

wxMemoryInputStream lMemory(ppRaw,piSize);

Después utilizaremos una clase wxImage, para cargar esos datos.

wxImage lImagen;
lImagen.LoadFile(lMemory,wxBITMAP_TYPE_JPEG);

Para finalizar pasamos dicha wxImage a un wxBitmap.

wxBitmap *lpBitmap;
lpBitmap=new wxBitmap(lImagen);

Este bitmap, será el que devuelva la función creaBitmap (CMarco.creaBitmap).

¿Cómo hacer esto con windows?

Primero creamos un HGLOBAL

HGLOBAL lhMem

Asignamos memoria.

lhMem=GlobalAlloc(GMEM_MOVEABLE, tamañoBytesImagen);

Ahora debemos de pasar(copiar) los datos de la imagen a la memoria HGLOBAL

lhMem=(HGLOBAL)GlobalLock(lhMem);
CopyMemory(lhMem, datosImagen, tamañoBytesImagen);
GlobalUnlock(lhMem);

Esta información debemos de pasarla a un ISTREAM.

IStream *lpStream;
CreateStreamOnHGlobal(lhMem,FALSE,&lpStream);

Y el último paso, pasar el IStream a un CImagen.

CImage* lCImagen;
lCImagen=new CImage();
lCImagen->Load(lpStream);

Una vez hecho esto, liberamos memoria.

lpStream->Release();
GlobalFree(lhMem);

La función sería algo como esto.

CImage* createImagen(unsigned char *ppuchBytes, unsigned int puiSize, char *pchEstado)
{
 CImage* lResult;
 HGLOBAL lhMem;
 IStream *lpStream;
 HRESULT lEstado;

 lResult=new CImage();

 //creamos la memoria
 lhMem=GlobalAlloc(GMEM_MOVEABLE,puiSize);
 if (lhMem==NULL)
 {
  TRACE("CRaster::cargaArray -> Error al ejecutar GlobalAlloc\n");
  *pchEstado=-1;
  return lResult;
 }

 //pasamos los bytes de la imagen a la memoria creada
 lhMem=(HGLOBAL)GlobalLock(lhMem);
 if (lhMem==NULL)
 {
  TRACE("Error al ejecutar GlobalLock\n");
  *pchEstado=-2;
  return lResult;
 }

 CopyMemory(lhMem,ppuchBytes,puiSize);
 GlobalUnlock(lhMem);
 //creamos el ISTrema
 lEstado=CreateStreamOnHGlobal(lhMem,FALSE,&lpStream);
 switch (lEstado)
 {
  case E_INVALIDARG:
	{
	 TRACE("Error al ejecutar CreateStreamOnHGlobal:E_INVALIDARG \n");
	 GlobalFree(lhMem);
       *pchEstado=-3;
       return lResult;;
	}
  case E_OUTOFMEMORY:
	{
	 TRACE("Error al ejecutar CreateStreamOnHGlobal:E_OUTOFMEMORY \n");
	 GlobalFree(lhMem);
       *pchEstado=-4;
       return lResult;
	}
 }
 //cargamos los bytes en CImagen
 lEstado=lResult->Load(lpStream);
 if (lEstado!=S_OK)
 {
  lpStream->Release();
  GlobalFree(lhMem);
  TRACE("Error al ejecutar m_ImagenRaster.Load\n");
  *pchEstado=-5;
  return lResult;
 }
 lpStream->Release();
 
 GlobalFree(lhMem);

 *pchEstado=0;
 return lResult;
}

Volvemos a wxWidgets.

Una vez creado el bitmap, lo guardamos en el vector de azulejos (mVecBitmap).

Recordar que ahora la aplicación tiene los tiles en memoria (bitmaps) y el objeto CFicheroMBTiles también tiene los mismos tiles. Debemos de liberar memoria, borrando los tiles del objeto CFicheroMBTiles. Por lo que utilizaremos la función CFicheroMBTiles.borrarTiles.

CMarco.h
CMarco::tilesToAzulejos

mFileTiles.borrarTiles();

Por último los tiles se dibujaran, en la función CMarco.onPaint.

CMarco.h
CMarco::onPaint

wxPaintDC lDC(this);

Obtenemos el número de tiles a dibujar.

luiNumTiles=mVecBitmap.size();

Por cada tile.

lImg=mVecBitmap[luiIndi];
lDc.DrawBitmap(*lImg.graf,lImg.posBitmap,false);

Aplicación visorMBTiles:

Es un visor muy simple y solo se ha hecho para mostrar los conceptos básicos de cómo utilizar la clase CFicheroMBTiles.

Tiene muchas carencias como por ejemplo dibujará todos los tiles aunque no se vayan a mostrar en la ventana (los mapas suelen tener un tamaño bastante grande).

lDc.DrawBitmap(*lImg.graf,lImg.posBitmap,false);

También se podría mejorar la clase CFicheroMBTiles, para que solo cargue los tiles que vamos a visualizar y no cargar TODOS los tiles de un determinado zoom.

En el visor tenemos 2 opciones de menú:

Abrir MBTile:

EVT_MENU(ID_MENU_OPEN_FILE, CMarco::onAbrirFichero)

Abrir MBTile Perímetro:

EVT_MENU(ID_MENU_OPEN_FILE_PERIMETRO, CMarco::onAbrirFicheroPerimetro)

Las dos opciones hacen lo mismo, cargar el fichero. Pero si utilizamos Abrir MBTile Perímetro, podremos ver el perímetro de los tiles que forman un determinado zoom, junto con su columna y fila.

Visor de tiles

Descarga de ficheros

Fuentes del programa visor:

Nombre Fichero Fecha Tamaño Contador Descarga
visorMBTiles32 visorMBTiles32.zip 2024-01-28 17433 bytes 111

Mapas de ejemplo (countries-raster):

Nombre Fichero Fecha Tamaño Contador Descarga
Mapas countries-raster.zip 2024-01-28 7038978 bytes 128

Referencias.

https://github.com/pedro-vicente/render_mbtiles.

https://github.com/pedro-vicente/lib_mbtiles.

Mapas en formato MBTiles.

http://centrodedescargas.cnig.es/CentroDescargas/loadMapMov.do.

https://wiki.openstreetmap.org/wiki/MBTiles.