4 # http://github.com/mapbox/mbutil
8 import sqlite3, sys, logging, time, os, re
10 from optparse import OptionParser
12 logger = logging.getLogger(__name__)
15 # Functions from mbutil for sqlite DB format and connections
19 return (2**zoom-1) - y
21 def mbtiles_setup(cur):
29 cur.execute("""create table metadata
30 (name text, value text);""")
31 cur.execute("""create unique index name on metadata (name);""")
32 cur.execute("""create unique index tile_index on tiles
33 (zoom_level, tile_column, tile_row);""")
35 def mbtiles_connect(mbtiles_file):
37 con = sqlite3.connect(mbtiles_file)
39 except Exception as e:
40 logger.error("Could not connect to database")
44 def optimize_connection(cur):
45 cur.execute("""PRAGMA synchronous=0""")
46 cur.execute("""PRAGMA locking_mode=EXCLUSIVE""")
47 cur.execute("""PRAGMA journal_mode=DELETE""")
49 def write_database(cur):
50 logger.debug('analyzing db')
51 cur.execute("""ANALYZE;""")
53 def optimize_database(cur):
54 logger.debug('cleaning db')
55 cur.execute("""VACUUM;""")
58 # End functions from mbutils
61 # Based on disk_to_mbtiles in mbutil
62 def vikcache_to_mbtiles(directory_path, mbtiles_file, **kwargs):
63 logger.debug("%s --> %s" % (directory_path, mbtiles_file))
64 con = mbtiles_connect(mbtiles_file)
66 optimize_connection(cur)
70 start_time = time.time()
72 onlydigits_re = re.compile ('^\d+$')
74 #print ('tileid ' + kwargs.get('tileid'))
75 # Need to split tDddsDdzD
76 # note zoom level can be negative hence the '-?' term
77 p = re.compile ('^t'+kwargs.get('tileid')+'s(-?\d+)z\d+$')
78 for ff in os.listdir(directory_path):
79 # Find only dirs related to this tileset
85 # For some reason Viking does '17-zoom level' - so need to reverse that
88 for r2, xs, ignore in os.walk(os.path.join(directory_path, ff)):
90 # Try to ignore any non cache directories
91 m2 = onlydigits_re.match(x);
93 #print('x:'+directory_path+'/'+ff+'/'+x)
94 for r3, ignore, ys in os.walk(os.path.join(directory_path, ff, x)):
96 # Legacy viking cache file names only made from digits
97 m3 = onlydigits_re.match(y);
99 #print('tile:'+directory_path+'/'+ff+'/'+x+'/'+y)
100 f = open(os.path.join(directory_path, ff, x, y), 'rb')
101 # Viking in xyz so always flip
102 y = flip_y(int(z), int(y))
103 cur.execute("""insert into tiles (zoom_level,
104 tile_column, tile_row, tile_data) values
106 (z, x, y, sqlite3.Binary(f.read())))
109 if (count % 100) == 0:
110 for c in msg: sys.stdout.write(chr(8))
111 msg = "%s tiles inserted (%d tiles/sec)" % (count, count / (time.time() - start_time))
112 sys.stdout.write(msg)
114 msg = "\nTotal tiles inserted %s \n" %(count)
115 sys.stdout.write(msg)
117 print ("No tiles inserted. NB This method only works with the Legacy Viking cache layout")
120 if not kwargs.get('nooptimize'):
121 sys.stdout.write("Optimizing...\n")
122 optimize_database(con)
125 def mbtiles_to_vikcache(mbtiles_file, directory_path, **kwargs):
126 logger.debug("Exporting MBTiles to disk")
127 logger.debug("%s --> %s" % (mbtiles_file, directory_path))
128 con = mbtiles_connect(mbtiles_file)
129 count = con.execute('select count(zoom_level) from tiles;').fetchone()[0]
132 base_path = directory_path
133 if not os.path.isdir(base_path):
134 os.makedirs(base_path)
136 start_time = time.time()
138 tiles = con.execute('select zoom_level, tile_column, tile_row, tile_data from tiles;')
144 # Viking in xyz so always flip
145 y = flip_y(int(z), int(y))
146 # For some reason Viking does '17-zoom level' - so need to reverse that
148 tile_dir = os.path.join(base_path, 't'+ kwargs.get('tileid') + 's' + str(vz) + 'z0', str(x))
149 if not os.path.isdir(tile_dir):
150 os.makedirs(tile_dir)
151 # NB no extension for VikCache files
152 tile = os.path.join(tile_dir,'%s' % (y))
153 # Only overwrite existing tile if specified
154 if not os.path.isfile(tile) or kwargs.get('force'):
159 if (done % 100) == 0:
160 for c in msg: sys.stdout.write(chr(8))
161 msg = "%s / %s tiles imported (%d tiles/sec)" % (done, count, done / (time.time() - start_time))
162 sys.stdout.write(msg)
164 msg = "\nTotal tiles imported %s \n" %(done)
165 sys.stdout.write(msg)
168 def cache_converter_to_osm (vc_path, target_path, **kwargs):
171 onlydigits_re = re.compile ('^\d+$')
172 etag_re = re.compile ('\.etag$')
173 path_re = re.compile ('^t'+kwargs.get('tileid')+'s(-?\d+)z\d+$')
175 if not os.path.isdir(target_path):
176 os.makedirs(target_path)
178 start_time = time.time()
179 for ff in os.listdir(vc_path):
180 # Find only dirs related to this tileset
181 m = path_re.match(ff);
183 s = path_re.split(ff)
186 # For some reason Viking does '17-zoom level' - so need to reverse that
188 tile_dirz = os.path.join(target_path, str(z))
190 if not os.path.isdir(tile_dirz):
191 #print (os.path.join(vc_path, ff) +":"+ tile_dirz)
192 os.rename(os.path.join(vc_path, ff), tile_dirz)
194 for r2, xs, ignore in os.walk(tile_dirz):
196 tile_dirx = os.path.join(tile_dirz, str(x))
198 # No need to move X dir
200 for r3, ignore, ys in os.walk(tile_dirx):
202 m2 = onlydigits_re.match(y);
204 # Move and append extension to everything else
205 # OSM also in flipped y, so no need to change y
207 # Only overwrite existing tile if specified
208 target_tile = os.path.join(tile_dirx, y + ".png")
209 if not os.path.isfile(target_tile) or kwargs.get('force'):
210 os.rename(os.path.join(tile_dirx, y), target_tile)
213 if (count % 100) == 0:
214 for c in msg: sys.stdout.write(chr(8))
215 msg = "%s tiles moved (%d tiles/sec)" % (count, count / (time.time() - start_time))
216 sys.stdout.write(msg)
218 # Also rename etag files appropriately
219 m3 = etag_re.search(y);
221 target_etag = y.replace (".etag", ".png.etag")
222 if not os.path.isfile(os.path.join(tile_dirx,target_etag)) or kwargs.get('force'):
223 os.rename(os.path.join(tile_dirx, y), os.path.join(tile_dirx, target_etag))
225 # Ignore all other files
228 msg = "\nTotal tiles moved %s \n" %(count)
229 sys.stdout.write(msg)
233 # Mainly for testing usage.
234 # Don't expect many people would want to convert back to the old layout
236 def cache_converter_to_viking (osm_path, target_path, **kwargs):
239 ispng = re.compile ('\.png$')
241 if not os.path.isdir(target_path):
242 os.makedirs(target_path)
244 start_time = time.time()
245 for r1, zs, ignore in os.walk(osm_path):
247 # For some reason Viking does '17-zoom level' - so need to reverse that
249 tile_dirz = os.path.join(target_path, 't'+ kwargs.get('tileid') + 's' + str(vz) + 'z0')
251 if not os.path.isdir(tile_dirz):
252 os.rename(os.path.join(osm_path, z), tile_dirz)
254 for r2, xs, ignore in os.walk(tile_dirz):
257 tile_dirx = os.path.join(tile_dirz, x)
258 # No need to move X dir
260 for r3, ignore, ys in os.walk(tile_dirx):
264 # Move and remove extension to everything else
265 # OSM also in flipped y, so no need to change y
267 # Only overwrite existing tile if specified
269 y_noext = y_noext.replace (".png", "")
270 target_tile = os.path.join(tile_dirx, y_noext)
271 if not os.path.isfile(target_tile) or kwargs.get('force'):
272 os.rename(os.path.join(tile_dirx, y), target_tile)
275 if (count % 100) == 0:
276 for c in msg: sys.stdout.write(chr(8))
277 msg = "%s tiles moved (%d tiles/sec)" % (count, count / (time.time() - start_time))
278 sys.stdout.write(msg)
280 msg = "\nTotal tiles moved %s \n" %(count)
281 sys.stdout.write(msg)
284 def get_tile_path (tid):
294 return "OSM-MapQuest"
296 return "OSM-Transport"
298 return "OSM-Humanitarian"
301 # Default extension Map ids (from data/maps.xml)
312 ## Start of code here
314 parser = OptionParser(usage="""usage: %prog -m <mode> [options] input output
316 When either the input or output refers to a Viking legacy cache ('vcl'), is it the root directory of the cache, typically ~/.viking-maps
320 Export Viking's legacy cache files of a map type to an mbtiles file:
321 $ ./viking-cache.py -m vlc2mbtiles -t 17 ~/.viking-maps OSM_Cycle.mbtiles
323 Note you can use the http://github.com/mapbox/mbutil mbutil script to further handle .mbtiles
324 such as converting it into an OSM tile layout and then pointing a new Viking Map at that location with the map type of 'On Disk OSM Layout'
326 Import from an MB Tiles file into Viking's legacy cache file layout, forcing overwrite of existing tiles:
327 $ ./viking-cache.py -m mbtiles2vlc -t 321 -f world.mbtiles ~/.viking-maps
328 NB: You'll need to a have a corresponding ~/.viking/maps.xml definition for the tileset id when it is not a built in id
330 Convert from Viking's Legacy cache format to the more standard OSM layout style for a built in map type:
331 $ ./viking-cache.py -m vlc2osm -t 13 -f ~/.viking-maps ~/.viking-maps
332 Here the tiles get automatically moved to ~/.viking-maps/OSM-Mapnik
334 Correspondingly change the Map layer property to use OSM style cache layout in Viking.
336 Convert from Viking's Legacy cache format to the more standard OSM layout style for a extension map type:
337 $ ./viking-cache.py -m vlc2osm -t 110 -f ~/.viking-maps ~/.viking-maps/StamenWaterColour
338 Here one must specify the output directory name explicitly and set your maps.xml file with the name=StamenWaterColour for the id=110 entry
341 parser.add_option('-t', '--tileid', dest='tileid',
343 help='''Tile id of Viking map cache to use (19 if not specified as this is Viking's default (MaqQuest))''',
347 parser.add_option('-n', '--nooptimize', dest='nooptimize',
349 help='''Do not attempt to optimize the mbtiles output file''',
352 parser.add_option('-f', '--force', dest='force',
354 help='''Force overwrite of existing tiles''',
357 parser.add_option('-m', '--mode', dest='mode',
359 help='''Mode of operation which must be specified. "vlc2mbtiles", "mbtiles2vlc", "vlc2osm", "osm2vlc"''',
363 (options, args) = parser.parse_args()
365 if options.__dict__.get('mode') == 'none':
366 sys.stderr.write ("\nError: Mode not specified\n")
376 if options.__dict__.get('mode') == 'vlc2mbtiles':
378 if os.path.isdir(args[0]) and not os.path.isfile(args[0]):
379 vikcache_to_mbtiles(in_fd, out_fd, **options.__dict__)
381 if options.__dict__.get('mode') == 'mbtiles2vlc':
383 if os.path.isfile(args[0]):
384 mbtiles_to_vikcache(in_fd, out_fd, **options.__dict__)
386 if options.__dict__.get('mode') == 'vlc2osm':
387 # Main forward conversion
388 is_default_re = re.compile ("\.viking-maps\/?$")
389 out_fd2 = is_default_re.search(out_fd)
391 # Auto append default tile name to the path
392 tile_path = get_tile_path(options.__dict__.get('tileid'))
393 if tile_path == "unknown":
394 sys.stderr.write ("Could not convert tile id to a name")
395 sys.stderr.write ("Specifically set the output directory to something other than the default")
398 print ("Using tile name %s" %(tile_path) )
399 out_fd2 = os.path.join(out_fd, tile_path)
403 if os.path.isdir(args[0]):
404 cache_converter_to_osm(in_fd, out_fd2, **options.__dict__)
406 if options.__dict__.get('mode') == 'osm2vlc':
407 # Convert back if needs be
408 if os.path.isdir(args[0]):
409 cache_converter_to_viking(in_fd, out_fd, **options.__dict__)