Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
fiveham authored Dec 26, 2019
1 parent 64b4c30 commit e51a9ab
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 0 deletions.
128 changes: 128 additions & 0 deletions kml/io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from bs4.element import CData, NavigableString, Tag
from bs4 import BeautifulSoup

_OPEN = open

def open(filepath, encoding=None):
"""Read `filepath` and parse it as a KML document (bs4.BeautifulSoup).
:param filepath: the name of or relative path to a KML file
:param encoding: optional character encoding (rarely needed)
:returns: a formatted KML document
"""
return formatted(BeautifulSoup(_OPEN(filepath, encoding=encoding),
'xml'))

def parse(filetext):
"""Parse `filetext` as a KML document.
:param filetext: Either valid XML or a file-like object"""
return formatted(BeautifulSoup(filetext, 'xml'))

def save(soup, filepath):
"""Save `soup` to a file at `filepath`.
:param soup: a KML document (bs4.BeautifulSoup)
:param filepath: the name of the file to save
:returns: None
"""
_OPEN(filepath, 'w').write(str(soup))

def format(soup, no_empty=False):
"""Remove all leading and trailing whitespace on all strings in `soup`,
remove all empty or self-terminating tags, remove all kml: prefixes
from all tags, and ensure that all CDATA tags are properly wrapped in
CData objects.
This function modifies the `soup` object.
`soup` : a KML document (bs4.BeautifulSoup)
CDATA in KML gets parsed correctly when read from text, but when that
CDATA text is put into string representations of the tag it's
in, it is blindly given HTML entity substitution instead of being
wrapped in "<![CDATA[...]]>"
This function hunts down CDATA strings in `soup` and replaces them with
bs4.element.CData objects so that they print in the "<![CDATA[...]]>"
form.
A KML document when converted to a string will often "kml:" prefixes on
every tag. A KML file like that opens perfectly in Google Earth,
but the Google Maps Javascript API's KmlLayer class insists that those
make the file an "INVALID_DOCUMENT".
This function checks every single tag and removes the "kml" prefix if it
is present.
There is never any reason for whitespace padding at the front or end of
a string in a tag in a KML document. Similarly, pure-whitespace strings
have no meaning in a kml document.
This function checks every string in `soup`, replaces trimmable strings
with their trimmed counterparts, and outright removes pure-whitespace
strings.
Empty or self-terminating tags do nothing in a KML document. This
function checks every tag and removes the empty/self-terminating
ones.
:param soup: a KML document (bs4.BeautifulSoup)
:param no_empty: if True, remove empty tags. Default False.
:returns: None
"""

strip = []
destroy = []
for e in soup.descendants:
if isinstance(e, NavigableString):
if e.isspace():
destroy.append(e) #remove empty strings
elif e.strip() != e:
strip.append(e) #trim trimmable strings
elif isinstance(e, Tag):
if e.prefix == "kml":
e.prefix = None #remove kml: prefixes
if e.string and e.string.parent is e: #.string works indirectly
e.string = e.string.strip() #trim some trimmable strings
if any(c in e.string for c in REPLACE):
cdata = CData(e.string)
if len(str(cdata)) <= len(_as_html(e.string)):
e.string = cdata #use CDATA to wrap HTML
for d in destroy:
d.extract()
for s in strip:
s.replace_with(s.strip())
if no_empty:
for tag in soup(lambda thing : isinstance(thing,Tag) and
len(list(thing.contents)) == 0):
tag.decompose()

def formatted(soup, **kwargs):
"""Format `soup` and return it. Convenience function wrapping `format`.
:param soup: a KML document (bs4.BeautifulSoup)
:param no_empty: (optional, default False) remove empty tags if True
:returns: `soup`
"""

format(soup, **kwargs)
return soup

REPLACE = {'<': '&lt;',
'>': '&gt;',
'&': '&amp;'}

def _as_html(string):
"""Return a copy of `string` where all less-thans, greater-thans,
and ampersands are replaced by their HTML character entity equivalents.
:param string: a string
:returns: a string where certain chars are replaced by html entity codes
"""

for k,v in REPLACE.items():
string = string.replace(k,v)
return string
21 changes: 21 additions & 0 deletions kml/kmz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import zipfile
from zipfile import ZipFile

from .io import parse as parsekml

def open(filename):
"""Put in a KMZ file's name, get out a KML soup."""
with ZipFile(filename) as kmz:
return parsekml(kmz.open(kmz.namelist()[0]))

def save(soup, filename):
"""Save a KML soup as a KMZ file."""
with ZipFile(filename, mode='w', compression=zipfile.ZIP_DEFLATED) as kmz:
kmz.writestr('doc.kml', str(soup))

def compress(kmlfile, replace=False):
"""Compress an existing KML file to KMZ. Optionally remove the original."""
assert kmlfile.endswith('.kml')
kmzfile = kmlfile[:-3] + 'kmz'
with ZipFile(kmzfile, mode='w', compression=zipfile.ZIP_DEFLATED) as kmz:
kmz.write(kmlfile, 'doc.kml')
132 changes: 132 additions & 0 deletions kml/styles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from kml import add

_COLORS_5 = {1 : '7f000000',
2 : '7f007800',
3 : '7fff0000',
4 : '7f7f7fff',
5 : '7fffffff'}

_GREEN_ORANGE = {1 : '7fa8d7b6',
2 : '7f065fb4',
3 : '7f4fa86a',
4 : '7f6bb2f6'}
_COLORS_4 = _GREEN_ORANGE

_BLURPGRELLOW = {
1 : 'http://maps.google.com/mapfiles/kml/paddle/blu-circle.png',
2 : 'http://maps.google.com/mapfiles/kml/paddle/ylw-circle.png',
3 : 'http://maps.google.com/mapfiles/kml/paddle/grn-circle.png',
4 : 'http://maps.google.com/mapfiles/kml/paddle/purple-circle.png'}

def stylize(soup, coloring, pm2int,
d2=_GREEN_ORANGE, d1=None, d0=_BLURPGRELLOW):
keys = set()
for pm in soup('Placemark'):
key = pm2int(pm)
color = coloring[key]
(pm.styleUrl or add(pm, 'styleUrl')).string = '#color' + str(color)

for key in keys:
style_tag = soup.new_tag('Style', id='color'+str(key))
poly_color = d2 and d2[key]
line_color = (d1 and d1[key]) or (d2 and '0'*8)
point_href = d0 and d0[key]

if poly_color:
add(style_tag, ['PolyStyle', 'color']).string = poly_color
if line_color:
add(style_tag, ['LineStyle', 'color']).string = line_color
if point_href:
add(style_tag,
['IconStyle', 'Icon', 'href']).string = point_href
return

def _get_string(tag):
return tag.string

class BalloonStyle:
def __init__(self, tag):
assert tag.name == 'BalloonStyle'
for term in 'bgColor textColor text displayMode'.split():
thing = tag.find(term, recursive=False)
setattr(self, term, thing and thing.string)

class Icon:
def __init__(self, tag):
assert tag.name == 'Icon'
href = tag.href
self.href = href and href.string

class hotSpot:
def __init__(self, tag):
assert tag.name == 'hotSpot'
for key in 'x y xunits yunits'.split():
setattr(self, key, tag[key])

class IconStyle:
def __init__(self, tag):
assert tag.name == 'IconStyle'
wrapper = {'Icon': Icon, 'hotSpot': HotSpot}
for term in 'color colorMode scale heading Icon hotSpot'.split():
thing = tag.find(term, recursive=False)
setattr(self,
term,
thing and wrapper.get(thing, _get_string)(thing))

class LabelStyle:
def __init__(self, tag):
assert tag.name == 'LabelStyle'
for term in 'color colorMode scale'.split():
thing = tag.find(term, recursive=False)
setattr(self, term, thing and thing.string)

class LineStyle:
def __init__(self, tag):
assert tag.name == 'LineStyle'
for term in 'color colorMode width'.split():
thing = tag.find(term, recursive=False)
setattr(self, term, thing and thing.string)

gxs = 'outerColor outerWidth physicalWidth labelVisibility'
for term in gxs.split():
thing = tag.find((lambda t : t.name == term and t.prefix == 'gx'),
recursive=False)
setattr(self, 'gx_'+term, thing and thing.string)

class ItemIcon:
def __init__(self, tag):
assert tag.name == 'ItemIcon'
for term in 'state href'.split():
thing = tag.find(term, recursive=False)
setattr(self, term, thing and thing.string)

class ListStyle:
def __init__(self, tag):
assert tag.name == 'ListStyle'
wrapper = {'ItemIcon': ItemIcon}
for term in 'listItemType bgColor ItemIcon'.split():
thing = tag.find(term, recursive=False)
setattr(self,
term,
thing and wrapper.get(thing, _get_string)(thing))

class PolyStyle:
def __init__(self, tag):
assert tag.name == 'PolyStyle'
for term in 'color colorMode fill outline'.split():
thing = tag.find(term, recursive=False)
setattr(self, term, thing and thing.string)

class Style:

def __init__(self, style):
assert style.name == 'Style'
wrapper = {'BalloonStyle': BalloonStyle,
'ListStyle' : ListStyle,
'LineStyle' : LineStyle,
'PolyStyle' : PolyStyle,
'IconStyle' : IconStyle,
'LabelStyle' : LabelStyle}
for label, value in wrapper.items():
v = style.find(label)
setattr(self, label, v and value(v))

0 comments on commit e51a9ab

Please sign in to comment.