I do a lot of trip planning on google maps, creating a custom my map for 
each trip with tracks/lines and waypoints.  I've always exported the google 
map as a KML, done a conversion to GPX using one of the many utilities 
available for that purpose and finally take the GPX file and drop it into 
the appropriate OSMAnd tracks directory on my android phone.

The problem I've always had with the KML to GP conversion is all the track 
colors and the icon symbols and colors are lost in translation, everything 
ends up a default color and symbol in OSMAnd.

I finally sat down and wrote a quick python utility to do the KML to GPX 
conversion directly using a table to translate a subset of KML icons to 
approximate OSMAnd equivalents.

I'm sharing the code in case it's useful to anyone else. 

-- 
You received this message because you are subscribed to the Google Groups 
"OsmAnd" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to osmand+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/osmand/9183838c-128b-40d0-b1e9-2c132b0b4b46n%40googlegroups.com.
# 8/11/2020 Tom Musolf - based on some code I found on the internet from Tim 
Abell 
#
# Quick hack (no error checking & I'm not a python guy) to convert Google My 
Maps KML export files to 
# GPX files for OSMAnd mapping program on Android phones.
#
# There are lots of KML to GPX converters out there, like GPSVisualizer.com, 
but they all lose the KML icon and color info
# during the translation.  In addition, OSMAnd has it's own GPX extensions for 
it's custom icons.
# What this program does is convert KML waypoints and tracks/lines into their 
OSMAnd GPX equivalents.
#
# KML Layers
#               Layers are ignored, but all tracks and points found in the KML 
file are translated into their OSMAnd GPX file equivalent.
#
# Tracks
#               Track name and description are translated.
#               Tracks will carry the color specified in the KML file into 
OSMAnd.  
#               Line width is not translated because it is not supported in 
OSMAnd.  
#               A default transparency value for the track is specified using 
the TrackTransparency variable. 
#
# Points 
#               Name and description are translated.
#               The KML icon is translated to an OSMAnd equivalent using the 
iconDictionary translation table.  
#               This table contains
#                       OSMAnd equivalent icon
#                       A color for the icon or you can specify using the icon 
color from the KML file
#                       Which of the 3 icon shapes that OSMAnd supports.  
#               If the KML icon is not found in the table then a 
default/unknown icon is used.
# Other
#               All other KML structures/tags are ignored
#
# Once you have your GPX file copy it to this location on your android phone: 
.../Android/data/net.osmand.plus/files/tracks
# Note: The tracks folder supports nested folders so you can create folders 
such as: .../tracks/hikes and .../tracks/BikeRoutes
# These folders and their GPX files will then show up in OSMAnd MyPlaces>Tracks
#
# I use OSMAnd configure map>GPX files>Appearance>Bold for the tracks I display 
with my GPX files.  With track transparency set to
# 55 (via this program) it lets me read street names and still see the track.
#
# Make sure you have python installed on your PC
#
#               Cmd format: py KMLToOSMAndGPX.py "input file" > "output file"
#
# If no output file is specified output goes to standard out.
#
# Again, NO ERROR CHECKING is done for presence of input file, valid KML file 
structure, etc.
#===========================================================================
# #!/usr/bin/python
# https://timwise.co.uk/2014/02/03/converting-kml-to-gpx-with-python/
# https://gist.github.com/timabell/8791116

import argparse
import xml.sax

parser = argparse.ArgumentParser(description='Convert annoying google android 
my tracks kml data to sensible gpx files')
parser.add_argument('input_file')

args = parser.parse_args()
input = args.input_file

#iconDictionary describes the mapping between a KML icon number and an OSMAnd 
icon name.
#It also contains a default OSMAnd color and shape to use for each OSMAnd icon 
type.
#
#iconDictionary format:
#               "KML icon number":["OSMAnd Icon name","HTML hex color code or 
flag to use KMLCOLOR","OSMAnd shape"]
#
#Color code is a standard HTML hex color code.  This is what OSMAnd uses
#As of 8/2020 OSMAnd icons do not support transparent colors.
#As of 8/2020 OSMAnd supports 3 icon shapes: circle, octagon, square
#
#Adding additional KML icons to the dictionary.
#
# For each icon you want to translate you need to add a new entry/line into the 
iconDictionary table. 
# To determine what the KML and OSMAnd icons are you want go through the 
following steps:
#
# KML icon number
#       1) Create a google my maps test file with the icons you want to use.
#       2) Export this map as a KML file.  
#       3) Open up the file in a text editor and look for your points. You can 
ignore all the <style> & <StyleMap> tags at the 
#          beginning of the KML file.  The points/waypoints/Placemarks will 
look like this:
#
#               <Placemark>
#                       <name>Mileage Marker dot</name>
#                       <styleUrl>#icon-1739-0288D1-nodesc</styleUrl>
#                       <Point>
#                               
<coordinates>-120.8427259,38.8170119,0</coordinates>
#                       </Point>
#               </Placemark>

#The <styleUrl> tag has the icon number.  In the preceeding example it's "1739".
#
# OSMAnd Icon name
#       1) Create some favorites using the icons you want.
#       2) Goto .../Android/data/net.osmand.plus/files/favourites.gpx    !!! 
yes, it's spelled the british way.
#       3) Open the favorites file in a text editor and look for the waypoints. 
 I
#          in the following example the icon name is: "special_trekking"
#
#               <wpt lat="39.2906659" lon="-121.4965106">
#                       <name>hiker, pale yellow</name>
#                       <extensions>
#                       <color>#eeee10</color>
#                       <icon>special_trekking</icon>
#                       <background>circle</background>
#                       </extensions>
#               </wpt>
#
# Put the string "KMLCOLOR", without the double quotes, in for a color value if 
you want to use the color specified in the KML file
# for a particular icon.
KMLCOLOR = "KMLCOLOR"

iconDictionary ={
        "unknown":["special_symbol_question_mark","e044bb","octagon"],          
        #unknown KML icon code - this entry will be used if the KML icon is not 
found the iconDictionary.
        "1765":["building_type_pyramid","785735","circle"],                     
                        #campsite
        "1525":["water_transport","a71de1","octagon"],                          
                        #river access
        "1739":["special_symbol_plus","1010a0","circle"],                       
                        #Mileage marker plus-KML dot
        "1596":["special_trekking","9e963a","cirlce"],                          
                        #hiking trailhead
        "1723":["tourism_viewpoint","d90000","octagon"],                        
                        #rapid
        "1602":["tourism_hotel","10c0f0","circle"],                             
                                #hotel, lodge
        "1528":["historic_castle","10c0f0","circle"],                           
                        #bridge
        "1577":["restaurants","10c0f0","circle"],                               
                                #retaurant, diner, dining
        "1650":["tourism_picnic_site","eecc22","circle"],                       
                        #picnic site
        "1644":["amenity_parking","10c0f0","circle"],                           
                        #parking area
        "1578":["shop_supermarket","10c0f0","circle"],                          
                        #grocery store, supermarket
        "1504":["air_transport","10c0f0","circle"],                             
                                #airport, airstrip
        "1581":["fuel","1010a0","circle"],                                      
                                        #gas station
        "1733":["amenity_toilets","10c0f0","circle"],                           
                        #toilet, restroom
        "1624":["amenity_hospital","d00d0d","circle"],                          
                        #hospital, doctor, emergency room
        "1608":["tourism_information","1010a0","circle"],                       
                        #tourism information
        "1535":["special_photo_camera","eecc22","circle"],                      
                        #POI #1, camera
        "1574":["special_flag_stroke","eecc22","circle"],                       
                        #POI #2, flag
        "1899":["special_marker","eecc22","circle"],                            
                        #POI #3, pin
        "1502":["special_star",KMLCOLOR,"circle"],                              
                                #POI #4, star
        "1501":["special_bookmark","eecc22","circle"],                          
                        #POI #5, ribbon/diamond
        "1603":["special_house","eecc22","circle"],                             
                                #house
        "1879":["amenity_biergarten","10c0f0","circle"],                        
                        #brewery, brew pub
        "1541":["special_symbol_exclamation_mark","ff0000","octagon"],          
        #danger #1 exclamation
        "1564":["special_symbol_exclamation_mark","ff0000","octagon"],          
        #danger #2 explosion
        "1710":["special_arrow_up_and_down","10c0f0","circle"],                 
                #river gauge, up/down arrow or thermometer
        "1655":["amenity_police","1010a0","circle"],                            
                        #ranger/police station #1
        "1657":["amenity_police","1010a0","circle"]                             
                                #ranger/police station #2
        }

# TrackTransparency: This value specifies the amount of transparency that a 
track will have when displayed in OSMAnd.
# this is a 2 digit hex value from 00 (fully transparent) to FF (fully opaque) 
# This value is concatinated to the front of the track color value. 
# For example: Bright pink is: F700FF if you want it very transparent, say only 
25%, you would set TrackTransparency to "40". 
# A 2 digit hex value goes from 0x00=0 to 0xFF=255 so 25% of 255 is 64 decimal 
which is 40 hex.
# So the <color> value for a 25% bright pink line would end up being "40F700FF" 
in the GPX file.
#
# As of 8/2020 transparency in the color value is only supported for OSMAnd 
tracks, not icons
TrackTransparency = "55"

#There are certain characters that can't be in HTML/XML name or description 
strings. 
#This function converts them to the HTML escaped version
html_escape_table = {
        "&": "&amp;",
        '"': "&quot;",
        "'": "&apos;",
        ">": "&gt;",
        "<": "&lt;",
        }
        
def html_escape(text):
        """Produce entities within text."""
        return "".join(html_escape_table.get(c,c) for c in text)        

class KmlParser(xml.sax.ContentHandler):
        def __init__(self):
                self.in_tag=0
                self.chars=""
                self.inPlacemark=0
                self.inDocument=0
                self.inLine=0
                self.name =""
                self.description=""
                self.style=""
                self.styleIcon=""
                self.styleColor=""

        def startElement(self, name, attrs):
                if self.inDocument==0: #we're entering document section looking 
for <name> element only
                        if name == "Document":
                                #print("Start Document")
                                self.inDocument = 1
                        elif self.inPlacemark==0: # we're not in a placemark, 
ignore other elements till we get a placemark
                                if name =="Placemark":
                                        self.inPlacemark = 1
                        else: # we're in a placemark
                                if name == "LineString":
                                        self.chars=""
                                        self.inLine=1
                                if name == "name":
                                        self.in_tag =1
                                        self.chars=""
                                elif name == "styleUrl":
                                        self.in_tag=1
                                        self.chars=""
                                elif name =="coordinates":
                                        self.in_tag =1
                                        self.chars=""
                                elif name =="description":
                                        self.in_tag = 1
                                        self.chars=""
                else: # we're in a docment looking only for <name> element
                        if name == "name":
                                self.in_tag =1
                                self.chars=""

        def characters(self, char):
                if self.in_tag:
                        self.chars += char
        def endElement(self, name):
                if self.inDocument==1: #we're in a document and looking only 
for the </name> element
                        if name == "name":
                                self.inDocument = 0
                                print("<metadata>")
                                print("\t<name>"+self.chars+"</name>")
                                print("</metadata>")
                                self.chars=""
                                self.in_tag = 0
                elif self.inPlacemark == 1:
                        if name == "Placemark":
                                if self.inLine==1: #we're doing a line/track 
placemark
                                        print("<trk>")
                                        print("\t<name>"+self.name+"</name>")
                                        if self.description != "":
                                                
print("\t<desc>"+self.description+"</desc>")
                                        print("\t<trkseg>")
                                        i=0
                                        while i < len(self.coordinates):
                                                print("\t\t<trkpt 
lat=\""+self.coordinates[i+1]+"\" lon=\""+self.coordinates[i]+"\"/>")
                                                i=i+3
                                        print("\t</trkseg>")
                                        print("\t<extensions>")
                                        style = self.style.split("-")
                                        self.styleWidth=float(style[2])/1000
                                        self.styleColor=style[1]
                                        
print("\t\t<color>#"+TrackTransparency+self.styleColor+"</color>")
                                        # 8/2020 it appears that OSMAnd ignores 
width and opacity tags
                                        
print("\t\t<width>"+str(self.styleWidth)+"</width>")
                                        print("\t\t<opacity>1</opacity>")
                                        print("\t</extensions>")
                                        print("</trk>")
                                else:   #it's waypoint
                                        print("<wpt 
lat=\""+self.coordinates[1]+"\" lon=\""+self.coordinates[0]+"\">")
                                        print("\t<name>"+self.name+"</name>")
                                        if self.description != "":
                                                
print("\t<desc>"+self.description+"</desc>")
                                        print("\t<extensions>")
                                        style = self.style.split("-")
                                        self.styleIcon=style[1]
                                        self.styleColor=style[2]
                                        #it's a KML icon that we haven't put in 
the dictionary so use a default one.
                                        if not self.styleIcon in iconDictionary:
                                                self.styleIcon="unknown"
                                        if iconDictionary[self.styleIcon][1] == 
KMLCOLOR:
                                                #use the icon color from the 
KML file
                                                
print("\t\t<color>#"+self.styleColor+"</color>")
                                        else:
                                                #use the icon color from the 
dictionary table
                                                
print("\t\t<color>#"+iconDictionary[self.styleIcon][1]+"</color>")
                                        
print("\t\t<icon>"+iconDictionary[self.styleIcon][0]+"</icon>")
                                        
print("\t\t<background>"+iconDictionary[self.styleIcon][2]+"</background>")
                                        print("\t</extensions>")
                                        print("</wpt>")
                                self.name =""
                                self.description=""
                                self.styleIcon=""
                                self.styleColor=""
                                self.style=""
                                self.inLine=0
                                self.inPlacemark = 0 #closing out a placemark
                        elif name == "name":
                                #ampersands and other special characters cause 
a problem in the XML/GPX file so replace them 
                                #with an XML escaped version.
                                self.name = html_escape(self.chars.strip())
                                self.chars=""
                                self.in_tag = 0
                        elif name == "styleUrl":
                                #style string is a different format for a 
track/line vs waypoint in KML
                                self.style=self.chars
                                self.chars=""
                                self.in_tag = 0
                        elif name == "coordinates":
                                #end up with a list of coordinates lon, lat, alt
                                self.coordinates=self.chars.strip().replace(" 
","").replace("\n",",").split(",")
                                self.chars=""
                                self.in_tag = 0
                        elif name == "description":
                                # Make sure to escape special characters and 
convert <br> in KML descriptions into carriage return/line feed
                                # characters that OSMAnd likes.
                                self.description = 
html_escape(self.chars.replace("<br>","\r\n"))
                                self.chars=""
                                self.in_tag = 0
                                
#GPX header
print ("""<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="OsmAnd+ 3.7.4" 
xmlns="http://www.topografix.com/GPX/1/1"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 
http://www.topografix.com/GPX/1/1/gpx.xsd";>
""")

parser = xml.sax.make_parser()
parser.setContentHandler(KmlParser())
parser.parse(open(input,"r"))

#GPX footer
print ("</gpx>")
#***end***

Reply via email to