# Copyright (C) 2008 by Intevation GmbH
# Authors:
# Anthony Lenton <anthony@except.com.ar>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with Thuban for details.

""" A Polish Map File parser.  This module contains the MPParser class, 
    that does the real weight lifting for the importMP module. """

import re

class MPParseError(Exception):
    def __init__(self, lNum, reason):
        self.reason = reason
        self.lineNumber = lNum
    def __str__(self):
        return "Line %d: %s" % (self.lineNumber, repr(self.reason))
    def __repr__(self):
        return "Line %d: %s" % (self.lineNumber, repr(self.reason))

# These dictionaries should probably be moved in to a separate file
polylineTypes = {'0x01': 'Major Highway-thick',
                 '0x02': 'Principal Highway-thick',
                 '0x03': 'Principal Highway-medium',
                 '0x04': 'Arterial Road-medium',
                 '0x05': 'Arterial Road-thick',
                 '0x06': 'Road-thin',
                 '0x07': 'Alley-thick',
                 '0x08': 'Ramp',
                 '0x09': 'Ramp',
                 '0x0a': 'Unpaved Road-thin',
                 '0x0b': 'Major Highway Connector-thick',
                 '0x0c': 'Roundabout',
                 '0x14': 'Railroad',
                 '0x15': 'Shoreline',
                 '0x16': 'Track/Trail',
                 '0x18': 'Stream-thin',
                 '0x19': 'Time-Zone',
                 '0x1a': 'Ferry',
                 '0x1b': 'Ferry',
                 '0x1c': 'Political Boundary',
                 '0x1d': 'County Boundary',
                 '0x1e': 'International Boundary',
                 '0x1f': 'River',
                 '0x20': 'Land Contour (thin)',
                 '0x21': 'Land Contour (medium)',
                 '0x22': 'Land Contour (thick)',
                 '0x23': 'Depth Contour (thin)',
                 '0x24': 'Depth Contour (medium)',
                 '0x25': 'Depth Contour (thick)',
                 '0x26': 'Intermittent River',
                 '0x27': 'Airport Runway',
                 '0x28': 'Pipeline',
                 '0x29': 'Power line',
                 '0x2a': 'Marine Boundary (no line)',
                 '0x2b': 'Marine Hazard (no line)',
                 '0x0100': 'Miscellaneous line',
                 '0x0101': 'Line',
                 '0x0102': 'Cartographic line',
                 '0x0103': 'Road',
                 '0x0104': 'Clearing line',
                 '0x0105': 'Contour line',
                 '0x0106': 'Overhead cable',
                 '0x0107': 'Bridge',
                 '0x0108': 'Recommended route',
                 '0x0109': 'Chart border',
                 '0x0300': 'Depth contour',
                 '0x0301': 'Depth contour value',
                 '0x0307': 'Intertidal zone border',
                 '0x0400': 'Obstruction line',
                 '0x0401': 'Submarine cable',
                 '0x0402': 'Submarine pipeline',
                 '0x0403': 'Pile barrier',
                 '0x0404': 'Fishing stakes',
                 '0x0405': 'Supply pipeline area',
                 '0x0406': 'Submarine cable area',
                 '0x0407': 'Dumping ground',
                 '0x0408': 'Explosive dumping ground',
                 '0x0409': 'Danger line',
                 '0x040a': 'Overhead cable',
                 '0x040b': 'Submerged construction',
                 '0x040c': 'Pier/jetty',
                 '0x0500': 'Restriction',
                 '0x0501': 'Anchoring prohibited',
                 '0x0502': 'Fishing prohibited',
                 '0x0503': 'Prohibited area',
                 '0x0504': 'Military practice area',
                 '0x0505': 'Anchoring and fishing prohibited',
                 '0x0506': 'Limit of nature reservation',
                 '0x0507': 'Restricted area',
                 '0x0508': 'Minefield',
                 '0x0600': 'Miscellaneous line',
                 '0x0601': 'Cartographic line',
                 '0x0602': 'Traffic separation line',
                 '0x0603': 'International maritime boundary',
                 '0x0604': 'Straight territorial sea baseline',
                 '0x0605': 'Seaward limit of territorial sea',
                 '0x0606': 'Anchorage area',
                 '0x0607': 'Quarantine anchorage area',
                 '0x0608': 'Fishery zone',
                 '0x0609': 'Swept area',
                 '0x060a': 'Traffic separation zone',
                 '0x060b': 'Limit of exclusive economic zone',
                 '0x060c': 'Established direction of traffic flow',
                 '0x060d': 'Recommended direction of traffic flow',
                 '0x060e': 'Harbour limit',
                 '0x060f': 'Inadequately surveyed area',
                 '0x0610': 'Inshore traffic zone',
                 '0x0611': 'Limit of traffic lane',
                 '0x0701': 'River channel',
                 '0x0702': 'Submerged object',
                 '0x0706': 'Chart boundary'}

polygonTypes = {'0x01': 'City',
                '0x02': 'City',
                '0x03': 'City',
                '0x04': 'Military',
                '0x05': 'Car Park (Parking Lot)',
                '0x06': 'Parking Garage',
                '0x07': 'Airport',
                '0x08': 'Shopping Centre',
                '0x09': 'Marina',
                '0x0a': 'University',
                '0x0b': 'Hospital',
                '0x0c': 'Industrial',
                '0x0d': 'Reservation',
                '0x0e': 'Airport Runway',
                '0x13': 'Man made area',
                '0x14': 'National park',
                '0x15': 'National park',
                '0x16': 'National park',
                '0x17': 'City Park',
                '0x18': 'Golf',
                '0x19': 'Sport',
                '0x1a': 'Cemetery',
                '0x1e': 'State Park',
                '0x1f': 'State Park',
                '0x28': 'Ocean',
                '0x3b': 'Blue-Unknown',
                '0x32': 'Sea',
                '0x3b': 'Blue-Unknown',
                '0x3c': 'Lake',
                '0x3d': 'Lake',
                '0x3e': 'Lake',
                '0x3f': 'Lake',
                '0x40': 'Lake',
                '0x41': 'Lake',
                '0x42': 'Lake',
                '0x43': 'Lake',
                '0x44': 'Lake',
                '0x45': 'Blue-Unknown',
                '0x46': 'River',
                '0x47': 'River',
                '0x48': 'River',
                '0x49': 'River',
                '0x4b': 'Background',
                '0x4c': 'Intermittent River/Lake',
                '0x4d': 'Glacier',
                '0x4e': 'Orchard or plantation',
                '0x4f': 'Scrub',
                '0x50': 'Woods',
                '0x51': 'Wetland',
                '0x52': 'Tundra',
                '0x53': 'Flats',
                '0x0100': 'Land - white',
                '0x0101': 'Land - non-urban',
                '0x0102': 'Land - urban',
                '0x0103': 'Chart exclusion area',
                '0x0104': 'Chart background',
                '0x0105': 'Bridge',
                '0x0300': 'Depth area - white 1',
                '0x0301': 'Intertidal zone',
                '0x0302': 'Depth area - blue 1',
                '0x0303': 'Depth area - blue 2',
                '0x0304': 'Depth area - blue 3',
                '0x0305': 'Depth area - blue 4',
                '0x0306': 'Depth area - blue 5',
                '0x0307': 'Depth area - white',
                '0x0400': 'Obstruction (invisible)',
                '0x0401': 'Submarine cable (invisible)',
                '0x0402': 'Submarine pipeline (invisible)',
                '0x0403': 'Pile barrier (invisible)',
                '0x0404': 'Fishing stakes (invisible)',
                '0x0405': 'Supply pipeline area/line (invisible)',
                '0x0406': 'Submarine cable area/line (invisible)',
                '0x0407': 'Dumping ground (invisible)',
                '0x0408': 'Explosive dumping ground (invisible)',
                '0x0409': 'Danger line (invisible)',
                '0x040a': 'Overhead cable (invisible)',
                '0x040b': 'Submerged construction (invisible)',
                '0x040c': 'Pier/jetty (invisible)',
                '0x0500': 'Restriction area/line (invisible)',
                '0x0501': 'Anchoring prohibited (invisible)',
                '0x0502': 'Fishing prohibited (invisible)',
                '0x0503': 'Prohibited area (invisible)',
                '0x0504': 'Military practice area (invisible)',
                '0x0505': 'Anchoring and fishing prohibited (invisible)',
                '0x0506': 'Limit of nature reservation (invisible)',
                '0x0507': 'Restricted area (invisible)',
                '0x0508': 'Minefield (invisible)',
                '0x0600': 'Miscellaneous area',
                '0x0601': 'Cartographic area',
                '0x0602': 'Traffic separation area',
                '0x0603': 'International maritime boundary',
                '0x0604': 'Straight territorial sea baseline',
                '0x0605': 'Seaward limit of territorial sea',
                '0x0606': 'Anchorage area',
                '0x0607': 'Quarantine anchorage area',
                '0x0608': 'Fishery zone',
                '0x0609': 'Swept area',
                '0x060a': 'Traffic separation zone',
                '0x060b': 'Limit of exclusive economic zone',
                '0x060c': 'Established direction of traffic flow',
                '0x0701': 'Fishing area',
                '0x0702': 'Restricted area',
                '0x0703': 'Anchorage area',
                '0x0704': 'Fishing Hot Spots chart'}

class MPParser(object):
    """ Polish Map File parser.  The main method is 'parse', which
        receives a file object as its single argument.  The results of
        the parsing can then be retrieved via 'pois', 'polygons', 'polylines',
        and other attributes.
    """
    fields = {'poi': ['Data0', 'Label', 'Origin0'],
              'rgn10': ['Data0', 'Label', 'Origin0'],
              'rgn20': ['Data0', 'Label', 'Origin0'],
              'polyline': ['Type', 'Data0', 'Origin0', 'Label'],
              'rgn40': ['Type', 'Data0', 'Origin0', 'Label'],
              'polygon': ['Type', 'Data0', 'Origin0', 'Label'],
              'rgn80': ['Type', 'Data0', 'Origin0', 'Label'],
              'img id': ['Name'] }
    def __init__(self):
        self.pois = []
        self.polygons = []
        self.polylines = []
        self.parsedLines = 0
        self.parsedSections = 0
        self.mapName = ""

    def parse (self, fp):
        """ Parse a map file. 'fp' should be a readable file object.
            After parsing the results can be accessed using 'pois', 'polylines',
            'polygons', and other attributes.
            Returns a dictionary with the results of the parsing, but this can
            be most often discarded directly """
        def sectionBounder (line):
            return line[0] == '[' and line[-1] == ']'
        def sectionEnd (line):
            return (sectionBounder (line) and
                   (line.lower() == '[end]' or
                   line.lower().startswith('[end-')))
        def sectionStart (line):
            return sectionBounder (line) and not sectionEnd (line)
        parsed = {}
        for kind in self.fields.keys():
            parsed[kind] = []
        inSection = False
        sectionName = None
        lNum = 0 # Number of lines parsed so far
        sNum = 0 # Number of sections parsed so far
        for line in fp.readlines():
            lNum += 1
            line = line.strip()
            if len(line) == 0 or line[0] == ';': # Drop blank lines and comments
                continue
            if not inSection:
                if not sectionBounder (line):
                    raise MPParseError (lNum, "Line not within a section")
                elif sectionEnd (line):
                    raise MPParseError (lNum, "Section End without a section")
                else:
                    sectionName = line[1:-1].lower()
                    section = {}
                    inSection = True
                    sNum += 1
            else:
                if not sectionBounder (line):
                    if sectionName in self.fields.keys():
                        parts = line.split('=',1)
                        if len(parts) != 2:
                            raise MPParseError (lNum, "Invalid line")
                        key, value = map(str.strip, parts)
                        if key in self.fields[sectionName]:
                            section[key] = value
                elif sectionStart (line):
                    raise MPParseError (lNum, "Section Start within a section")
                else:
                    if line.startswith ('[end-') and line[5:-1] != sectionName:
                        raise MPParseError (lNum, "Section name mismatch")
                    if sectionName in self.fields.keys():
                        parsed[sectionName].append (section)
                    inSection = False
        # These last checks are a bit pedantic (not needed to do our
        # job) but we're validating the source file a bit.
        if inSection:
            raise MPParseError (lNum, "End-of-file found within a section")
        if len(parsed['img id']) == 0:
            raise MPParseError (lNum, "No [IMG ID] section found")
        elif len(parsed['img id']) > 1:
            raise MPParseError (lNum, "Multiple [IMG ID] sections found")
        else:
            self.mapName = parsed['img id'][0].get ('Name', "")

        # Ok, enough checks, set the data!
        self.parsedLines = lNum
        self.parsedSections = sNum

        self.pois = parsed['poi'] + parsed['rgn10'] + parsed['rgn20']
        twople = re.compile(r'\([-+]?(?P<lat>\d+(\.\d*)?|\.\d+), ' \
                             r'*[-+]?(?P<long>\d+(\.\d*)?|\.\d+)\)')
        for poi in self.pois:
            assert poi.has_key ('Data0') or poi.has_key ('Origin0')
            data = poi.get('Data0', poi.get('Origin0'))
            match = twople.match (data)
            if not match is None:
                poi['long'] = -float (match.group('long'))
                poi['lat'] = -float (match.group('lat'))
        self.polygons = parsed['polygon'] + parsed['rgn80']
        for poly in self.polygons:
            poly['long'] = []
            poly['lat'] = []
            assert poly.has_key ('Data0') or poly.has_key ('Origin0')
            data = poly.get('Data0', poly.get('Origin0'))
            while len(data) > 0:
                match = twople.match (data)
                if not match is None:
                    poly['long'].append (-float (match.group('long')))
                    poly['lat'].append (-float (match.group('lat')))
                    data = data[match.end() + 1:]
            if poly['long'][-1] != poly['long'][0] or poly['lat'][-1] != poly['lat'][0]:
                poly['long'].append(poly['long'][0])
                poly['lat'].append(poly['lat'][0])
            try:
                poly['parsedType'] = polygonTypes[poly['Type']]
            except KeyError, e:
                # Attempt to read 0xe as 0x0e, for example
                newType = '0x0' + poly['Type'][2:]
                poly['parsedType'] = polygonTypes.get(newType, "")

        self.polylines = parsed['polyline'] + parsed['rgn40']
        for poly in self.polylines:
            poly['long'] = []
            poly['lat'] = []
            assert poly.has_key ('Data0') or poly.has_key ('Origin0')
            data = poly.get('Data0', poly.get('Origin0'))
            while len(data) > 0:
                match = twople.match (data)
                if not match is None:
                    poly['long'].append (-float (match.group('long')))
                    poly['lat'].append (-float (match.group('lat')))
                    data = data[match.end() + 1:]
            try:
                poly['parsedType'] = polylineTypes[poly['Type']]
            except KeyError, e:
                # Attempt to read 0xe as 0x0e, for example
                newType = '0x0' + poly['Type'][2:]
                poly['parsedType'] = polylineTypes.get(newType, "")
        return parsed

