]> git.street.me.uk Git - andy/viking.git/blob - tools/viking-cache.py
viking-cache.py tool: Allow specifying zoom limits in creating mbtiles file
[andy/viking.git] / tools / viking-cache.py
1 #!/usr/bin/env python
2 #
3 # Inspired by MBUtils:
4 #  http://github.com/mapbox/mbutil
5 #
6 # Licensed under BSD
7 #
8 import sqlite3, sys, logging, time, os, re
9
10 from optparse import OptionParser
11
12 logger = logging.getLogger(__name__)
13
14 #
15 # Functions from mbutil for sqlite DB format and connections
16 #  utils.py:
17 #
18 def flip_y(zoom, y):
19     return (2**zoom-1) - y
20
21 def mbtiles_setup(cur):
22     cur.execute("""
23         create table tiles (
24             zoom_level integer,
25             tile_column integer,
26             tile_row integer,
27             tile_data blob);
28             """)
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);""")
34
35 def mbtiles_connect(mbtiles_file):
36     try:
37         con = sqlite3.connect(mbtiles_file)
38         return con
39     except Exception as e:
40         logger.error("Could not connect to database")
41         logger.exception(e)
42         sys.exit(1)
43
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""")
48
49 def write_database(cur):
50     logger.debug('analyzing db')
51     cur.execute("""ANALYZE;""")
52
53 def optimize_database(cur):
54     logger.debug('cleaning db')
55     cur.execute("""VACUUM;""")
56
57 #
58 # End functions from mbutils
59 #
60
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)
65     cur = con.cursor()
66     optimize_connection(cur)
67     mbtiles_setup(cur)
68     image_format = 'png'
69     count = 0
70     start_time = time.time()
71     msg = ""
72     onlydigits_re = re.compile ('^\d+$')
73
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
80         m = p.match(ff);
81         if m:
82             s = p.split(ff)
83             if len(s) > 2:
84                 #print (s[1])
85                 # For some reason Viking does '17-zoom level' - so need to reverse that
86                 z = 17 - int(s[1])
87                 #print (z)
88                 for r2, xs, ignore in os.walk(os.path.join(directory_path, ff)):
89                     if z <= kwargs.get('maxzoom') and z >= kwargs.get('minzoom'):
90                         for x in xs:
91                             # Try to ignore any non cache directories
92                             m2 = onlydigits_re.match(x);
93                             if m2:
94                                 #print('x:'+directory_path+'/'+ff+'/'+x)
95                                 for r3, ignore, ys in os.walk(os.path.join(directory_path, ff, x)):
96                                     for y in ys:
97                                         # Legacy viking cache file names only made from digits
98                                         m3 = onlydigits_re.match(y);
99                                         if m3:
100                                             #print('tile:'+directory_path+'/'+ff+'/'+x+'/'+y)
101                                             f = open(os.path.join(directory_path, ff, x, y), 'rb')
102                                             # Viking in xyz so always flip
103                                             y = flip_y(int(z), int(y))
104                                             cur.execute("""insert into tiles (zoom_level,
105                                                          tile_column, tile_row, tile_data) values
106                                                          (?, ?, ?, ?);""",
107                                                          (z, x, y, sqlite3.Binary(f.read())))
108                                             f.close()
109                                             count = count + 1
110                                             if (count % 100) == 0:
111                                                 for c in msg: sys.stdout.write(chr(8))
112                                                 msg = "%s tiles inserted (%d tiles/sec)" % (count, count / (time.time() - start_time))
113                                                 sys.stdout.write(msg)
114
115     msg = "\nTotal tiles inserted %s \n" %(count)
116     sys.stdout.write(msg)
117     if count == 0:
118         print ("No tiles inserted. NB This method only works with the Legacy Viking cache layout")
119     else:
120         write_database(cur)
121         if not kwargs.get('nooptimize'):
122             sys.stdout.write("Optimizing...\n")
123             optimize_database(con)
124     return
125
126 def mbtiles_to_vikcache(mbtiles_file, directory_path, **kwargs):
127     logger.debug("Exporting MBTiles to disk")
128     logger.debug("%s --> %s" % (mbtiles_file, directory_path))
129     con = mbtiles_connect(mbtiles_file)
130     count = con.execute('select count(zoom_level) from tiles;').fetchone()[0]
131     done = 0
132     msg = ''
133     base_path = directory_path
134     if not os.path.isdir(base_path):
135         os.makedirs(base_path)
136
137     start_time = time.time()
138
139     tiles = con.execute('select zoom_level, tile_column, tile_row, tile_data from tiles;')
140     t = tiles.fetchone()
141     while t:
142         z = t[0]
143         x = t[1]
144         y = t[2]
145         # Viking in xyz so always flip
146         y = flip_y(int(z), int(y))
147         # For some reason Viking does '17-zoom level' - so need to reverse that
148         vz = 17 - int(t[0])
149         tile_dir = os.path.join(base_path, 't'+ kwargs.get('tileid') + 's' + str(vz) + 'z0', str(x))
150         if not os.path.isdir(tile_dir):
151             os.makedirs(tile_dir)
152         # NB no extension for VikCache files
153         tile = os.path.join(tile_dir,'%s' % (y))
154         # Only overwrite existing tile if specified
155         if not os.path.isfile(tile) or kwargs.get('force'):
156             f = open(tile, 'wb')
157             f.write(t[3])
158             f.close()
159         done = done + 1
160         if (done % 100) == 0:
161             for c in msg: sys.stdout.write(chr(8))
162             msg = "%s / %s tiles imported (%d tiles/sec)" % (done, count, done / (time.time() - start_time))
163             sys.stdout.write(msg)
164         t = tiles.fetchone()
165     msg = "\nTotal tiles imported %s \n" %(done)
166     sys.stdout.write(msg)
167     return
168
169 def cache_converter_to_osm (vc_path, target_path, **kwargs):
170     msg = ''
171     count = 0
172     onlydigits_re = re.compile ('^\d+$')
173     etag_re = re.compile ('\.etag$')
174     path_re = re.compile ('^t'+kwargs.get('tileid')+'s(-?\d+)z\d+$')
175
176     if not os.path.isdir(target_path):
177         os.makedirs(target_path)
178
179     start_time = time.time()
180     for ff in os.listdir(vc_path):
181         # Find only dirs related to this tileset
182         m = path_re.match(ff);
183         if m:
184             s = path_re.split(ff)
185             if len(s) > 2:
186                 #print (s[1])
187                 # For some reason Viking does '17-zoom level' - so need to reverse that
188                 z = 17 - int(s[1])
189                 tile_dirz = os.path.join(target_path, str(z))
190
191                 if not os.path.isdir(tile_dirz):
192                     #print (os.path.join(vc_path, ff) +":"+ tile_dirz)
193                     os.rename(os.path.join(vc_path, ff), tile_dirz)
194
195                 for r2, xs, ignore in os.walk(tile_dirz):
196                     for x in xs:
197                         tile_dirx = os.path.join(tile_dirz, str(x))
198
199                         # No need to move X dir
200
201                         for r3, ignore, ys in os.walk(tile_dirx):
202                             for y in ys:
203                                 m2 = onlydigits_re.match(y);
204                                 if m2:
205                                     # Move and append extension to everything else
206                                     # OSM also in flipped y, so no need to change y
207
208                                     # Only overwrite existing tile if specified
209                                     target_tile = os.path.join(tile_dirx, y + ".png")
210                                     if not os.path.isfile(target_tile) or kwargs.get('force'):
211                                         os.rename(os.path.join(tile_dirx, y), target_tile)
212
213                                     count = count + 1
214                                     if (count % 100) == 0:
215                                         for c in msg: sys.stdout.write(chr(8))
216                                         msg = "%s tiles moved (%d tiles/sec)" % (count, count / (time.time() - start_time))
217                                         sys.stdout.write(msg)
218                                 else:
219                                     # Also rename etag files appropriately
220                                     m3 = etag_re.search(y);
221                                     if m3:
222                                         target_etag = y.replace (".etag", ".png.etag")
223                                         if not os.path.isfile(os.path.join(tile_dirx,target_etag)) or kwargs.get('force'):
224                                             os.rename(os.path.join(tile_dirx, y), os.path.join(tile_dirx, target_etag))
225                                     else:
226                                         # Ignore all other files
227                                         continue
228
229     msg = "\nTotal tiles moved %s \n" %(count)
230     sys.stdout.write(msg)
231     return
232
233 #
234 # Mainly for testing usage.
235 # Don't expect many people would want to convert back to the old layout
236 #
237 def cache_converter_to_viking (osm_path, target_path, **kwargs):
238     msg = ''
239     count = 0
240     ispng = re.compile ('\.png$')
241
242     if not os.path.isdir(target_path):
243         os.makedirs(target_path)
244
245     start_time = time.time()
246     for r1, zs, ignore in os.walk(osm_path):
247         for z in zs:
248             # For some reason Viking does '17-zoom level' - so need to reverse that
249             vz = 17 - int(z)
250             tile_dirz = os.path.join(target_path, 't'+ kwargs.get('tileid') + 's' + str(vz) + 'z0')
251
252             if not os.path.isdir(tile_dirz):
253                 os.rename(os.path.join(osm_path, z), tile_dirz)
254
255             for r2, xs, ignore in os.walk(tile_dirz):
256                 for x in xs:
257
258                     tile_dirx = os.path.join(tile_dirz, x)
259                     # No need to move X dir
260
261                     for r3, ignore, ys in os.walk(tile_dirx):
262                         for y in ys:
263                             m = ispng.search(y);
264                             if m:
265                                 # Move and remove extension to everything else
266                                 # OSM also in flipped y, so no need to change y
267
268                                 # Only overwrite existing tile if specified
269                                 y_noext = y
270                                 y_noext = y_noext.replace (".png", "")
271                                 target_tile = os.path.join(tile_dirx, y_noext)
272                                 if not os.path.isfile(target_tile) or kwargs.get('force'):
273                                     os.rename(os.path.join(tile_dirx, y), target_tile)
274
275                                 count = count + 1
276                                 if (count % 100) == 0:
277                                     for c in msg: sys.stdout.write(chr(8))
278                                     msg = "%s tiles moved (%d tiles/sec)" % (count, count / (time.time() - start_time))
279                                     sys.stdout.write(msg)
280
281     msg = "\nTotal tiles moved %s \n" %(count)
282     sys.stdout.write(msg)
283     return
284
285 def get_tile_path (tid):
286     # Built in Tile Ids
287     tile_id = int(tid)
288     if tile_id == 13:
289         return "OSM-Mapnik"
290     elif tile_id == 15:
291       return "BlueMarble"
292     elif tile_id == 17:
293         return "OSM-Cyle"
294     elif tile_id == 19:
295         return "OSM-MapQuest"
296     elif tile_id == 21:
297         return "OSM-Transport"
298     elif tile_id == 22:
299         return "OSM-Humanitarian"
300     elif tile_id == 212:
301         return "Bing-Aerial"
302     # Default extension Map ids (from data/maps.xml)
303     elif tile_id == 29:
304         return "CalTopo"
305     elif tile_id == 101:
306         return "pnvkarte"
307     elif tile_id == 600:
308         return "OpenSeaMap"
309     else:
310         return "unknown"
311
312 ##
313 ## Start of code here
314 ##
315 parser = OptionParser(usage="""usage: %prog -m <mode> [options] input output
316
317 When either the input or output refers to a Viking legacy cache ('vcl'), is it the root directory of the cache, typically ~/.viking-maps
318
319 Examples:
320
321 Export Viking's legacy cache files of a map type to an mbtiles file:
322 $ ./viking-cache.py -m vlc2mbtiles -t 17 ~/.viking-maps OSM_Cycle.mbtiles
323
324 Note you can use the http://github.com/mapbox/mbutil mbutil script to further handle .mbtiles
325 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
327 Import from an MB Tiles file into Viking's legacy cache file layout, forcing overwrite of existing tiles:
328 $ ./viking-cache.py -m mbtiles2vlc -t 321 -f world.mbtiles ~/.viking-maps
329 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
331 Convert from Viking's Legacy cache format to the more standard OSM layout style for a built in map type:
332 $ ./viking-cache.py -m vlc2osm -t 13 -f ~/.viking-maps ~/.viking-maps
333 Here the tiles get automatically moved to ~/.viking-maps/OSM-Mapnik
334
335 Correspondingly change the Map layer property to use OSM style cache layout in Viking.
336
337 Convert from Viking's Legacy cache format to the more standard OSM layout style for a extension map type:
338 $ ./viking-cache.py -m vlc2osm -t 110 -f ~/.viking-maps ~/.viking-maps/StamenWaterColour
339 Here one must specify the output directory name explicitly and set your maps.xml file with the name=StamenWaterColour for the id=110 entry
340 """)
341
342 parser.add_option('-t', '--tileid', dest='tileid',
343     action="store",
344     help='''Tile id of Viking map cache to use (19 if not specified as this is Viking's default (MaqQuest))''',
345     type='string',
346     default='19')
347
348 parser.add_option('-n', '--nooptimize', dest='nooptimize',
349     action="store_true",
350     help='''Do not attempt to optimize the mbtiles output file''',
351     default=False)
352
353 parser.add_option('-f', '--force', dest='force',
354     action="store_true",
355     help='''Force overwrite of existing tiles''',
356     default=False)
357
358 parser.add_option('-m', '--mode', dest='mode',
359     action="store",
360     help='''Mode of operation which must be specified. "vlc2mbtiles", "mbtiles2vlc", "vlc2osm", "osm2vlc"''',
361     type='string',
362     default='none')
363
364 # Primary to help in limiting the generated DB size
365 parser.add_option('', '--max-zoom', dest='maxzoom',
366     action="store",
367     help='''Maximum (OSM) zoom level to use in writing to mbtiles''',
368     type='int',
369     default=25)
370
371 parser.add_option('', '--min-zoom', dest='minzoom',
372     action="store",
373     help='''Minimum (OSM) zoom level to use in writing to mbtiles''',
374     type='int',
375     default=1)
376
377 (options, args) = parser.parse_args()
378
379 if options.__dict__.get('mode') ==  'none':
380     sys.stderr.write ("\nError: Mode not specified\n")
381     parser.print_help()
382     sys.exit(1)
383
384 if len(args) != 2:
385     parser.print_help()
386     sys.exit(1)
387
388 in_fd, out_fd = args
389
390 if options.__dict__.get('mode') == 'vlc2mbtiles':
391     # to mbtiles
392     if os.path.isdir(args[0]) and not os.path.isfile(args[0]):
393         vikcache_to_mbtiles(in_fd, out_fd, **options.__dict__)
394 else:
395     if options.__dict__.get('mode') == 'mbtiles2vlc':
396         # to VikCache
397         if os.path.isfile(args[0]):
398             mbtiles_to_vikcache(in_fd, out_fd, **options.__dict__)
399     else:
400         if options.__dict__.get('mode') == 'vlc2osm':
401             # Main forward conversion
402             is_default_re = re.compile ("\.viking-maps\/?$")
403             out_fd2 = is_default_re.search(out_fd)
404             if out_fd2:
405                 # Auto append default tile name to the path
406                 tile_path = get_tile_path(options.__dict__.get('tileid'))
407                 if tile_path == "unknown":
408                     sys.stderr.write ("Could not convert tile id to a name")
409                     sys.stderr.write ("Specifically set the output directory to something other than the default")
410                     sys.exit(2)
411                 else:
412                     print ("Using tile name %s" %(tile_path) )
413                     out_fd2 = os.path.join(out_fd, tile_path)
414             else:
415                 out_fd2 = out_fd
416
417             if os.path.isdir(args[0]):
418                 cache_converter_to_osm(in_fd, out_fd2, **options.__dict__)
419         else:
420             if options.__dict__.get('mode') == 'osm2vlc':
421                 # Convert back if needs be
422                 if os.path.isdir(args[0]):
423                     cache_converter_to_viking(in_fd, out_fd, **options.__dict__)