]> git.street.me.uk Git - andy/viking.git/blob - tools/viking-cache.py
Fix renamed 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                     for x in xs:
90                         # Try to ignore any non cache directories
91                         m2 = onlydigits_re.match(x);
92                         if m2:
93                             #print('x:'+directory_path+'/'+ff+'/'+x)
94                             for r3, ignore, ys in os.walk(os.path.join(directory_path, ff, x)):
95                                 for y in ys:
96                                     # Legacy viking cache file names only made from digits
97                                     m3 = onlydigits_re.match(y);
98                                     if m3:
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
105                                                      (?, ?, ?, ?);""",
106                                                      (z, x, y, sqlite3.Binary(f.read())))
107                                         f.close()
108                                         count = count + 1
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)
113
114     msg = "\nTotal tiles inserted %s \n" %(count)
115     sys.stdout.write(msg)
116     if count == 0:
117         print ("No tiles inserted. NB This method only works with the Legacy Viking cache layout")
118     else:
119         write_database(cur)
120         if not kwargs.get('nooptimize'):
121             sys.stdout.write("Optimizing...\n")
122             optimize_database(con)
123     return
124
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]
130     done = 0
131     msg = ''
132     base_path = directory_path
133     if not os.path.isdir(base_path):
134         os.makedirs(base_path)
135
136     start_time = time.time()
137
138     tiles = con.execute('select zoom_level, tile_column, tile_row, tile_data from tiles;')
139     t = tiles.fetchone()
140     while t:
141         z = t[0]
142         x = t[1]
143         y = t[2]
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
147         vz = 17 - int(t[0])
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'):
155             f = open(tile, 'wb')
156             f.write(t[3])
157             f.close()
158         done = done + 1
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)
163         t = tiles.fetchone()
164     msg = "\nTotal tiles imported %s \n" %(done)
165     sys.stdout.write(msg)
166     return
167
168 def cache_converter_to_osm (vc_path, target_path, **kwargs):
169     msg = ''
170     count = 0
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+$')
174
175     if not os.path.isdir(target_path):
176         os.makedirs(target_path)
177
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);
182         if m:
183             s = path_re.split(ff)
184             if len(s) > 2:
185                 #print (s[1])
186                 # For some reason Viking does '17-zoom level' - so need to reverse that
187                 z = 17 - int(s[1])
188                 tile_dirz = os.path.join(target_path, str(z))
189
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)
193
194                 for r2, xs, ignore in os.walk(tile_dirz):
195                     for x in xs:
196                         tile_dirx = os.path.join(tile_dirz, str(x))
197
198                         # No need to move X dir
199
200                         for r3, ignore, ys in os.walk(tile_dirx):
201                             for y in ys:
202                                 m2 = onlydigits_re.match(y);
203                                 if m2:
204                                     # Move and append extension to everything else
205                                     # OSM also in flipped y, so no need to change y
206
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)
211
212                                     count = count + 1
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)
217                                 else:
218                                     # Also rename etag files appropriately
219                                     m3 = etag_re.search(y);
220                                     if m3:
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))
224                                     else:
225                                         # Ignore all other files
226                                         continue
227
228     msg = "\nTotal tiles moved %s \n" %(count)
229     sys.stdout.write(msg)
230     return
231
232 #
233 # Mainly for testing usage.
234 # Don't expect many people would want to convert back to the old layout
235 #
236 def cache_converter_to_viking (osm_path, target_path, **kwargs):
237     msg = ''
238     count = 0
239     ispng = re.compile ('\.png$')
240
241     if not os.path.isdir(target_path):
242         os.makedirs(target_path)
243
244     start_time = time.time()
245     for r1, zs, ignore in os.walk(osm_path):
246         for z in zs:
247             # For some reason Viking does '17-zoom level' - so need to reverse that
248             vz = 17 - int(z)
249             tile_dirz = os.path.join(target_path, 't'+ kwargs.get('tileid') + 's' + str(vz) + 'z0')
250
251             if not os.path.isdir(tile_dirz):
252                 os.rename(os.path.join(osm_path, z), tile_dirz)
253
254             for r2, xs, ignore in os.walk(tile_dirz):
255                 for x in xs:
256
257                     tile_dirx = os.path.join(tile_dirz, x)
258                     # No need to move X dir
259
260                     for r3, ignore, ys in os.walk(tile_dirx):
261                         for y in ys:
262                             m = ispng.search(y);
263                             if m:
264                                 # Move and remove extension to everything else
265                                 # OSM also in flipped y, so no need to change y
266
267                                 # Only overwrite existing tile if specified
268                                 y_noext = y
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)
273
274                                 count = count + 1
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)
279
280     msg = "\nTotal tiles moved %s \n" %(count)
281     sys.stdout.write(msg)
282     return
283
284 def get_tile_path (tid):
285     # Built in Tile Ids
286     tile_id = int(tid)
287     if tile_id == 13:
288         return "OSM-Mapnik"
289     elif tile_id == 15:
290       return "BlueMarble"
291     elif tile_id == 17:
292         return "OSM-Cyle"
293     elif tile_id == 19:
294         return "OSM-MapQuest"
295     elif tile_id == 21:
296         return "OSM-Transport"
297     elif tile_id == 22:
298         return "OSM-Humanitarian"
299     elif tile_id == 212:
300         return "Bing-Aerial"
301     # Default extension Map ids (from data/maps.xml)
302     elif tile_id == 29:
303         return "CalTopo"
304     elif tile_id == 101:
305         return "pnvkarte"
306     elif tile_id == 600:
307         return "OpenSeaMap"
308     else:
309         return "unknown"
310
311 ##
312 ## Start of code here
313 ##
314 parser = OptionParser(usage="""usage: %prog -m <mode> [options] input output
315
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
317
318 Examples:
319
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
322
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'
325
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
329
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
333
334 Correspondingly change the Map layer property to use OSM style cache layout in Viking.
335
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
339 """)
340
341 parser.add_option('-t', '--tileid', dest='tileid',
342     action="store",
343     help='''Tile id of Viking map cache to use (19 if not specified as this is Viking's default (MaqQuest))''',
344     type='string',
345     default='19')
346
347 parser.add_option('-n', '--nooptimize', dest='nooptimize',
348     action="store_true",
349     help='''Do not attempt to optimize the mbtiles output file''',
350     default=False)
351
352 parser.add_option('-f', '--force', dest='force',
353     action="store_true",
354     help='''Force overwrite of existing tiles''',
355     default=False)
356
357 parser.add_option('-m', '--mode', dest='mode',
358     action="store",
359     help='''Mode of operation which must be specified. "vlc2mbtiles", "mbtiles2vlc", "vlc2osm", "osm2vlc"''',
360     type='string',
361     default='none')
362
363 (options, args) = parser.parse_args()
364
365 if options.__dict__.get('mode') ==  'none':
366     sys.stderr.write ("\nError: Mode not specified\n")
367     parser.print_help()
368     sys.exit(1)
369
370 if len(args) != 2:
371     parser.print_help()
372     sys.exit(1)
373
374 in_fd, out_fd = args
375
376 if options.__dict__.get('mode') == 'vlc2mbtiles':
377     # to mbtiles
378     if os.path.isdir(args[0]) and not os.path.isfile(args[0]):
379         vikcache_to_mbtiles(in_fd, out_fd, **options.__dict__)
380 else:
381     if options.__dict__.get('mode') == 'mbtiles2vlc':
382         # to VikCache
383         if os.path.isfile(args[0]):
384             mbtiles_to_vikcache(in_fd, out_fd, **options.__dict__)
385     else:
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)
390             if out_fd2:
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")
396                     sys.exit(2)
397                 else:
398                     print ("Using tile name %s" %(tile_path) )
399                     out_fd2 = os.path.join(out_fd, tile_path)
400             else:
401                 out_fd2 = out_fd
402
403             if os.path.isdir(args[0]):
404                 cache_converter_to_osm(in_fd, out_fd2, **options.__dict__)
405         else:
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__)