]> git.street.me.uk Git - andy/viking.git/blob - tools/viking-cache.py
Change tool to require specifying the mode of operation.
[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 ##
169 ## Start of code here
170 ##
171 parser = OptionParser(usage="""usage: %prog -m <mode> [options] input output
172
173 When either the input or output refers to a Viking legacy cache ('vcl'), is it the root directory of the cache, typically ~/.viking-maps
174
175 Examples:
176
177 Export Viking's legacy cache files of a map type to an mbtiles file:
178 $ ./viking-cache.py -m vlc2mbtiles -t 17 ~/.viking-maps OSM_Cycle.mbtiles
179
180 Note you can use the http://github.com/mapbox/mbutil mbutil script to further handle .mbtiles
181 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'
182
183 Import from an MB Tiles file into Viking's legacy cache file layout, forcing overwrite of existing tiles:
184 $ ./viking-cache.py -m mbtiles2vlc -t 321 -f world.mbtiles ~/.viking-maps
185 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
186 """)
187    
188 parser.add_option('-t', '--tileid', dest='tileid',
189     action="store",
190     help='''Tile id of Viking map cache to use (19 if not specified as this is Viking's default (MaqQuest))''',
191     type='string',
192     default='19')
193
194 parser.add_option('-n', '--nooptimize', dest='nooptimize',
195     action="store_true",
196     help='''Do not attempt to optimize the mbtiles output file''',
197     default=False)
198
199 parser.add_option('-f', '--force', dest='force',
200     action="store_true",
201     help='''Force overwrite of existing tiles''',
202     default=False)
203
204 parser.add_option('-m', '--mode', dest='mode',
205     action="store",
206     help='''Mode of operation which must be specified. "vlc2mbtiles", "mbtiles2vlc", "vlc2osm", "osm2vlc"''',
207     type='string',
208     default='none')
209
210 (options, args) = parser.parse_args()
211
212 if options.__dict__.get('mode') ==  'none':
213     sys.stderr.write ("\nError: Mode not specified\n")
214     parser.print_help()
215     sys.exit(1)
216
217 if len(args) != 2:
218     parser.print_help()
219     sys.exit(1)
220
221 in_fd, out_fd = args
222
223 if options.__dict__.get('mode') == 'vlc2mbtiles':
224     # to mbtiles
225     if os.path.isdir(args[0]) and not os.path.isfile(args[0]):
226         vikcache_to_mbtiles(in_fd, out_fd, **options.__dict__)
227 else:
228     if options.__dict__.get('mode') == 'mbtiles2vlc':
229         # to VikCache
230         if os.path.isfile(args[0]):
231             mbtiles_to_vikcache(in_fd, out_fd, **options.__dict__)
232