Codebase list mkgmap / upstream/0.0.0+svn4565 scripts / gmapi-builder.py
upstream/0.0.0+svn4565

Tree @upstream/0.0.0+svn4565 (Download .tar.gz)

gmapi-builder.py @upstream/0.0.0+svn4565raw · history · blame

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
#!/usr/bin/python

"""
    Simple convertor  for files in Garmins MapSource format to the directory
    structure RoadTrip for OS X uses.

    Copyright (c) 2009, Berteun Damman
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
        * Redistributions of source code must retain the above copyright
          notice, this list of conditions and the following disclaimer.
        * Redistributions in binary form must reproduce the above copyright
          notice, this list of conditions and the following disclaimer in the
          documentation and/or other materials provided with the distribution.
        * Neither the name of the OpenStreetMap Project nor the
          names of its contributors may be used to endorse or promote products
          derived from this software without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY
    EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    First of all: This program is not a replacement for Garmins MapConverter.exe.
    It was specifically written to convert a bunch of .IMG and .TDB files of
    the OpenStreetmap project to a .gmapi directory structure which can be
    installed under OS X. As such it is geared towards the versions of the format
    used by OSM. This means it will most certain not work on non-OSM files (this
    has not been tested in anyway!).

    The conversion itself is fairly simple, under Windows you have:
    * Registry entries indicating the TDB-file, the Base Image and the Image dir,
      and optionally a TYP file and some other types.
    * A single TDB-file
    * A whole collection of IMG files.

    Under Mac you have:
    * An XML file which contains the information of the Windows Register
    * For each IMG file a directory with the same basename as the IMG-file,
      and with the subfiles of this IMG extracted into this directory; the IMG-files
      are in fact containers (they are similar to disk images).

    This program does the conversion.
"""

import logging
import optparse
import os
import shutil
import struct
import sys
import StringIO

# Logger setup
logging.NORMAL = logging.INFO + 5
logging.addLevelName(logging.NORMAL, 'NORMAL')
logger = logging.getLogger('logger')
logger.setLevel(logging.DEBUG)

# Program options can increase the verbosity
cons = logging.StreamHandler(sys.stdout)
cons.setLevel(logging.NORMAL)
logger.addHandler(cons)

def error_exit(msg):
    sys.exit(msg)

def write(msg, *args, **kwargs):
    logger.log(logging.NORMAL, msg, *args, **kwargs)

class EndOfFile(IOError):
    pass

class FileFormatError(Exception):
    pass

# Auxiliary functions that make life easier to read bytes, shorts, int and
# so on from a file.
def getX(length, fmt):
    def get(f):
        v = f.read(length)
        if len(v) < length:
            raise EndOfFile("End of file reached on '%s'." % f.name)
        return struct.unpack(fmt, v)[0]
    return get

# See the documentation of struct for an explanation of these
# format specifiers. Notably, the < indicates little endian
# format.
get_short  = getX(2, '<h')
get_ushort = getX(2, '<H')
get_int    = getX(4, '<i')
get_uint   = getX(4, '<I')
get_byte   = getX(1, 'B')

def get_str(f):
    c = f.read(1)
    s = ''
    while c != '\x00':
        s += c
        c = f.read(1)
    return s

def get_nstr(f, n):
    c = f.read(n)
    if len(c) < n:
        raise EndOfFile("End of file reached on '%s'." % f.name)
    s = struct.unpack('%ds' % n, c)[0]
    return s

# For the 4.X TDB file format, 24bit integers are used; already here
# for the future.
#def get_middle(f):
#    l = list(f.read(3))
#    if len(l) < 3:
#        raise EndOfFile("End of file reached on '%s'." % f.name)
#    sign, l[2] = l[2] >> 7, l[2] & 0x7F
#    v = -sign * (1<<23) + (l[2] << 16) + (l[1] << 8) + l[0]

def todegrees(n):
    return (n * 360.0) / (2 ** 32)

# A TDB file consists of a sequence of blocks, the format of each
# block is simply:
# 1 Byte: Block ID
# 2 Bytes: Block length (l):
# l Bytes: Data

class Block(object):
    def __init__(self, f):
        self.bid = get_byte(f)
        self.length = get_ushort(f)
        self.data = StringIO.StringIO(f.read(self.length))
        if len(self.data.buf) < self.length:
            raise EndOfFile("End of file reached early on '%s'." % f.name)

class TDBFile(object):
    """This represents the TDBFile with its known blocks, such as
       the header, copyright block, overview block and detailed maps."""
    def __init__(self, filename):
        self.f = open(filename, 'rb')
        self.header_block = None
        self.copyright_block = None
        self.overview_block = None
        self.trademark_block = None
        self.detail_blocks = []
        self._parse()

    def _parse(self):
        """Dispatches subparsers for *known* blocks"""
        block_parsers = {
            0x50: self.parse_header,
            0x44: self.parse_copyright,
            0x42: self.parse_overview,
            0x4C: self.parse_detail,
            0x52: self.parse_trademark,
        }

        while self.f.read(1):
            self.f.seek(-1, 1)
            b = Block(self.f)
            if b.bid in block_parsers:
                block_parsers[b.bid](b)
            else:
                logger.info('Unknown Block: %02X, length: %d, %s' % (b.bid, b.length, repr(b.data.buf)))
        self.f.close()

    def parse_header(self, block):
        hb = {}
        hb['Product ID'] = get_ushort(block.data)
        hb['Family ID']  = get_ushort(block.data)

        tdb_version = get_ushort(block.data)
        hb['TDB Major Version'] = tdb_version / 100
        hb['TDB Minor Version'] = tdb_version % 100
        hb['TDB Version'] = "%d.%02d" % (hb['TDB Major Version'], hb['TDB Minor Version'])

        hb['Map Series'] = get_str(block.data)

        prod_version = get_ushort(block.data)
        hb['Product Major Version'] = prod_version / 100
        hb['Product Minor Version'] = prod_version % 100
        hb['Product Version'] = "%d.%02d" % (hb['Product Major Version'], hb['Product Minor Version'])

        hb['Map Family'] = get_str(block.data)

        self.header_block = hb

    def parse_copyright(self, block):
        cl = []
        while block.data.pos < len(block.data.buf):
            copyright_code = get_byte(block.data)
            where_code = get_byte(block.data)
            extra = get_short(block.data)
            copyright_string = get_str(block.data)
            if copyright_code == 0x00:
                cl.append({'Source': copyright_string.decode('latin1').encode('utf-8')})
            elif copyright_code == 0x06:
                cl.append({'Copyright': copyright_string.decode('latin1').encode('utf-8')})
            elif copyright_code == 0x07:
                cl.append({'Bitmap': copyright_string, 'Scale factor': extra})
            else:
                cl.append({'Unknown (%02X)' % copyright_code: copyright_string, 'Extra': extra})
        self.copyright_block = cl

    def parse_overview(self, block):
        ob = {}
        ob['Map Number'] = get_uint(block.data)
        ob['Parent Map'] = get_uint(block.data)
        ob['Latitude North'] = "%7.4f" % todegrees(get_int(block.data))
        ob['Longitude East'] = "%7.4f" % todegrees(get_int(block.data))
        ob['Latitude South'] = "%7.4f" % todegrees(get_int(block.data))
        ob['Longitude West'] = "%7.4f" % todegrees(get_int(block.data))
        ob['Description'] = get_str(block.data)
        self.overview_block = ob

    def parse_detail(self, block):
        db = {}
        db['Map Number'] = get_uint(block.data)
        db['Parent Map'] = get_uint(block.data)
        db['Latitude North'] = "%7.4f" % todegrees(get_int(block.data))
        db['Longitude East'] = "%7.4f" % todegrees(get_int(block.data))
        db['Latitude South'] = "%7.4f" % todegrees(get_int(block.data))
        db['Longitude West'] = "%7.4f" % todegrees(get_int(block.data))
        db['Description'] = get_str(block.data)
        # Unknown
        block.data.seek(4, 1)
        # Could it be that these values have changed in v4?
        db['RGN Size'] = get_uint(block.data)
        db['TRE Size'] = get_uint(block.data)
        db['LBL Size'] = get_uint(block.data)
        self.detail_blocks.append(db)

    def parse_trademark(self, block):
        tb = {}
        block.data.seek(1, 1)
        tb['Trademark'] = get_str(block.data)
        self.trademark_block = tb

    def print_header(self):
        for f in ['TDB Version', 'Product ID', 'Family ID', 'Map Series', 'Map Family', 'Product Version']:
            write("%-20s%s" % (f + ':', str(self.header_block[f])))

    def print_copyright(self):
        for c in self.copyright_block:
            write('\n        '.join(["%-20s%s" % (f + ":", str(c[f])) for f in c]))

    def print_trademark(self):
        write("%-20s%s" % ('Trademark:', self.trademark_block['Trademark'].encode('utf-8')))

    def print_overview(self):
        logger.info('Overview map:')
        for f in ['Map Number', 'Parent Map', 'Latitude North', 'Longitude East',
            'Latitude South', 'Longitude West', 'Description']:
            logger.info("    %-20s%s" % (f + ':', str(self.overview_block[f])))

    def print_detail_blocks(self):
        logger.debug('Detailed maps:')
        for db in self.detail_blocks:
            logger.debug("   %-20s%s" % ('Map Number:', str(db['Map Number'])))
            logger.debug("   %-20s%s" % ('Parent Map:', str(db['Parent Map'])))
            logger.debug("   %-20s%s" % ('Description:', str(db['Description'])))
            logger.debug("   N: %s, S: %s, W: %s, E: %s" % (db['Latitude North'], db['Latitude South'],
                 db['Longitude West'], db['Longitude East']))
            logger.debug("   RGN: %d, TRE: %d, LBL: %d" % (db['RGN Size'], db['TRE Size'], db['LBL Size']))
            logger.debug("")


    def print_dump(self):
        """Gives a short textual description of the data."""
        if self.header_block:
            self.print_header()
        else:
            logger.warning('TDB file contains no header block.')
        write("")
        if self.copyright_block:
            self.print_copyright()
        else:
            logger.info('TDB file contains no copyright block.')
        write("")
        if self.trademark_block:
            self.print_trademark()
            write("")
        else:
            logger.info('TDB file contains no trademark block.')
            logger.info("")
        if self.overview_block:
            self.print_overview()
        else:
            logger.warning('TDB file contains no overview map.')
        logger.info("")
        if self.detail_blocks:
            self.print_detail_blocks()
        else:
            logger.warning('TDB file contains no detail blocks.')

class SubFile(object):
    """Represents a subfile in the IMG file

    The image is like a FAT file system; for each file it lists
    the sectors on which a part of the file can be found. If the
    file is larger than 240 sectors, it split into parts.
    """
    def __init__(self, name, extension):
        self.name = name
        self.extension = extension
        self.fullname = '%s.%s' % (name, extension)
        self.size = None
        self.part_list = []

    def add_part(self, part, sectors):
        self.part_list.append((part, sectors))

    def merge_parts(self):
        """Merges all the sectors in the parts into 
            a large (sorted) sector list which can be used to dump the file.
        """
        self.part_list.sort()
        for n in range(len(self.part_list)):
            if self.part_list[n][0] != n:
                raise FileFormatError('Missing part: %d of %s in IMG-file.' % (n, self.fullname))
        self.sector_list = []
        for (n, sl) in self.part_list:
            self.sector_list.extend(sl)

class IMGFile(object):
    """Very crude representation of the IMG file

    We only want to extract the subfiles, so we ignore lots of infomation in this file."""
    def __init__(self, filename):
        self.f = open(filename)
        self.basename = os.path.basename(filename[:filename.rfind('.')])
        self._check_file()
        self.block_size = self._get_block_size()
        self._read_files()

    def _check_file(self):
        first_b = get_byte(self.f)
        if first_b != 0x00:
            raise FileFormatError("%s is not a Garmin IMG file, or it is encrypted." % self.f.name)
        self.f.seek(0x10)
        if get_str(self.f) != "DSKIMG":
            raise FileFormatError("%s is not a Garmin IMG file, or is an unknown version." % self.f.name)
        self.f.seek(0x41)
        if get_str(self.f) != "GARMIN":
            raise FileFormatError("%s is not a Garmin IMG file, or is an unknown version." % self.f.name)

    def _get_block_size(self):
        # Probably 512
        self.f.seek(0x61)
        e1 = get_byte(self.f)
        e2 = get_byte(self.f)
        bs = 1<<(e1 + e2)
        return bs

    def _read_files(self):
        """Scans the FAT for files"""
        files = {}
        file_count = 0
        while True:
            # FAT starts at 0x600, each entry is
            # exactly 512 bytes, padded if necessary.
            self.f.seek(0x600 + file_count * 512)
            if get_byte(self.f) == 0:
                break

            filename = get_nstr(self.f, 8)
            file_type = get_nstr(self.f, 3)
            size = get_uint(self.f)

            self.f.seek(1, 1)
            part_no = get_byte(self.f)
            fullname ='%s.%s' % (filename, file_type)
            self.f.seek(14, 1)
            sector_list = []
            for n in range(240):
                sector_no = get_ushort(self.f)
                if sector_no != -1:
                    sector_list.append(sector_no)

            if not fullname in files:
                files[fullname] = SubFile(filename, file_type)
            if part_no == 0:
                files[fullname].size  = size

            files[fullname].add_part(part_no, sector_list)
            file_count += 1

        for fn in files:
            files[fn].merge_parts()
        self.files = files

    def dump(self, base_dir):
        """Dumps the subfiles of this IMG file"""
        output_dir = os.path.join(base_dir, self.basename)
        os.mkdir(output_dir)
        for sfn in self.files:
            subfile = self.files[sfn]
            out = open(os.path.join(output_dir, subfile.fullname), 'w')
            for s in subfile.sector_list:
                self.f.seek(s * self.block_size)
                out.write(self.f.read(self.block_size))
            out.truncate(subfile.size)
            logger.debug('Wrote: %s/%s' % (output_dir, subfile.fullname))
            out.close()

    def close(self):
        self.f.close()

    def print_info(self):
        logger.debug("Contents of %s:" % (self.basename))
        for fn in self.files:
            logger.debug("    %s: Size: %d" % (self.files[fn].fullname, self.files[fn].size))

def parse_options(option_list):
    usage = 'usage: %prog: [-h] [-o <outputdir>] [-s <typfile>] -t <tdbfile> -b <base-image> <file1.img> [<file2.img> [<file3.img> ... ]]'
    oparser = optparse.OptionParser(usage=usage)
    oparser.add_option("-o", "--output", dest="output", default="./",
            help="specify the output directory for the .gmapi folder")
    oparser.add_option("-t", "--tdbfile", dest="tdbfile",
            help="the name of this mapset's TDB file")
    oparser.add_option("-b", "--baseimg", dest="baseimg",
            help="the name of the base img")
    oparser.add_option("-i", "--idxfile", dest="mdx",
            help="the name of this mapset's index (mdx) file")
    oparser.add_option("-m", "--mdrfile", dest="mdr",
            help="the name of this mapset's MDR file")
    oparser.add_option("-v", "--verbose", dest="verbosity",
            action="count", help="verbosity")
    oparser.add_option("-s", "--style", dest="style",
            help="an optional style-file (TYP)")
    oparser.add_option("-d", "--dryrun", dest="dryrun",
            action="store_true", default=False)

    (options, args) = oparser.parse_args(option_list)

    if options.verbosity:
        if options.verbosity == 1:
            cons.setLevel(logging.INFO)
        elif options.verbosity > 1:
            cons.setLevel(logging.DEBUG)

    if not options.tdbfile:
        oparser.print_help()
        error_exit("\nError: You must specify a TDB-file with -t!")

    if not options.baseimg:
        oparser.print_help()
        error_exit("\nError: You must specify the base image with -b!")
    elif not os.path.isfile(options.baseimg):
            error_exit("\nError: Baseimage not found.")

    if options.style:
        if not os.path.isfile(options.style):
            error_exit("\nError: Style file not found.")

    if options.mdx:
        if not os.path.isfile(options.mdx):
            error_exit("\nError: Index (mdx) file not found.")

    if options.mdr:
        if not os.path.isfile(options.mdr):
            error_exit("\nError: MDR file not found.")
            
    if not args:
        oparser.print_help()
        error_exit('No filenames specified!')

    return (options, args)

def prepare_output_dir(tdbfile, options):
    dir_prefix = tdbfile.header_block['Map Series']
    if not os.path.isdir(options.output):
        error_exit('Output dir does not exist')
    gmapi = os.path.join(options.output, dir_prefix + '.gmapi')
    gmap = os.path.join(gmapi, dir_prefix + '.gmap')
    if os.path.exists(gmapi):
        logger.info("Removing existing file '%s' recursively" % gmapi)
        if os.path.isdir(gmapi):
            shutil.rmtree(gmapi)
        else:
            os.unlink(gmapi)
    if os.path.exists(gmapi):
        error_exit("Could not remove existing '%s', please do so yourself, or specify another output dir.")
    # This directory is empty indeed
    os.mkdir(gmapi)
    os.mkdir(gmap)
    return gmap

def write_xml_file(tdbfile, options, output_dir):
    def write_field(field, value, indent):
        f.write('%s<%s>%s</%s>\n' % (indent * '    ', field, value, field))
    f = open(os.path.join(output_dir, 'Info.xml'), 'w')
    f.write('<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n')
    f.write('<MapProduct xmlns="http://www.garmin.com/xmlschemas/MapProduct/v1">\n\n')
    write_field('Name', tdbfile.header_block['Map Family'], 1)
    f.write('\n')
    write_field('DataVersion', str(tdbfile.header_block['Product Major Version']) + ("%02d" % tdbfile.header_block['Product Minor Version']), 1)
    f.write('\n')
    write_field('DataFormat', 'Original', 1)
    f.write('\n')
    # It appears the windows converter does not write the family ID
    # if it is zero, so neither do we!"
    if tdbfile.header_block['Family ID'] > 0:
        write_field('ID', tdbfile.header_block['Family ID'], 1)
        f.write('\n')
    if options.mdx:
        write_field('IDX', os.path.basename(options.mdx), 1)
        f.write('\n')
    if options.mdr: 
        mdr = os.path.basename(options.mdr)
        mdr = mdr[:mdr.rfind('.')]
        write_field('MDR', mdr, 1)
        f.write('\n')
    if options.style:
        write_field('TYP', os.path.basename(options.style), 1)
        f.write('\n')
    f.write('    <SubProduct>\n')
    write_field('Name', tdbfile.header_block['Map Series'], 2)
    write_field('ID', tdbfile.header_block['Product ID'], 2)
    baseimg = os.path.basename(options.baseimg)
    baseimg = baseimg[:baseimg.rfind('.')]
    write_field('BaseMap', baseimg, 2)
    write_field('TDB', os.path.basename(options.tdbfile), 2)
    write_field('Directory', 'OSMTiles', 2)
    f.write('    </SubProduct>\n')
    f.write('</MapProduct>\n')
    f.close()

if __name__ == '__main__':
    (options, filenames) = parse_options(sys.argv[1:])
    try:
        tdbfile = TDBFile(options.tdbfile)
        tdbfile.print_dump()
    except IOError:
        error_exit("Could not open '%s' for reading." % options.tdbfile)

    if not options.dryrun:
        output_dir = prepare_output_dir(tdbfile, options)
        write_xml_file(tdbfile, options, output_dir)

    try:
        if not options.dryrun:
            imgoutput = os.path.join(output_dir, 'OSMTiles')
            os.mkdir(imgoutput)
            shutil.copy(options.tdbfile, os.path.join(imgoutput, os.path.basename(options.tdbfile)))
            if options.style:
                shutil.copy(options.style, os.path.join(output_dir, os.path.basename(options.style)))

            if options.mdx:
                shutil.copy(options.mdx, os.path.join(output_dir, os.path.basename(options.mdx)))

            if options.mdr:
                mdr_file = os.path.basename(options.mdr)

        for fn in filenames:
            logger.info("Processing " + fn)
            imgfile = IMGFile(fn)
            imgfile.print_info()
            if not options.dryrun:
                if fn != options.mdr:
                    imgfile.dump(imgoutput)
                else:
                    logger.info("MDR file")
                    imgfile.dump(output_dir)
            imgfile.close()
    except IOError:
            error_exit("Could not open '%s' for reading." % fn)
    except FileFormatError, m:
            error_exit(m)