-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
281 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = {'<': '<', | ||
'>': '>', | ||
'&': '&'} | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |