Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .uvversion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.7.12
1 change: 0 additions & 1 deletion MANIFEST.in

This file was deleted.

46 changes: 41 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ motionless

motionless is a Python library that takes the pain out of generating [Google Static Map](http://code.google.com/apis/maps/documentation/staticmaps/) URLs. Three map types are supported. Each is illustrated below. For fully worked code see the examples directory for code that parses and visualizes both GeoRSS feeds and GPX files.

motionless is tested with Python versions 3.9 to 3.11.
motionless requires Python 3.9 or later and is tested with Python versions 3.9 to 3.13.

Code is licensed under Apache 2.0

Expand All @@ -26,16 +26,52 @@ generate and use a personal API key.
Installation instructions
=========================

Motionless is a pure python package. Install it with conda (or mamba):
Motionless is a pure Python package and can be installed using various package managers:

### Using pip (traditional)

```bash
pip install motionless
```
$ conda install -c conda-forge motionless

### Using uv (recommended, fastest)

```bash
# Install uv if you haven't already
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install motionless
uv pip install motionless
```

or pip:
### Using conda/mamba

```bash
conda install -c conda-forge motionless
# or
mamba install -c conda-forge motionless
```
$ pip install motionless

### For development

If you want to contribute to motionless or run the latest development version:

```bash
# Clone the repository
git clone https://github.com/ryancox/motionless.git
cd motionless

# Install uv if you haven't already
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install poetry via uv
uv tool install poetry

# Install dependencies and the package in development mode
poetry install

# Run tests
poetry run pytest
```


Expand Down
19 changes: 7 additions & 12 deletions examples/demo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Examples of the various maps that can be created with motionless."""
from __future__ import print_function
from motionless import AddressMarker, DecoratedMap, CenterMap, VisibleMap
from motionless import AddressMarker, CenterMap, DecoratedMap, VisibleMap

cmap = CenterMap(address='151 third st, san francisco, ca')

Expand All @@ -16,24 +15,20 @@
label='G'))


htmlPage = """
htmlPage = f"""
<html>
<body>
<h2>SFMOMA</h2>
<img src="%s"/>
<img src="{cmap.generate_url()}"/>
<h2>La Tour Eiffel</h2>
<img src="%s"/>
<img src="{cmap_sat.generate_url()}"/>
<h2>Tahoe City and Sugarbowl</h2>
<img src="%s"/>
<img src="{vmap.generate_url()}"/>
<h2>Google and Apple</h2>
<img src="%s"/>
<img src="{dmap.generate_url()}"/>
</body>
</html>
""" % (
cmap.generate_url(),
cmap_sat.generate_url(),
vmap.generate_url(),
dmap.generate_url())
"""

with open("demo.html", "w") as html:
html.write(htmlPage)
Expand Down
3 changes: 1 addition & 2 deletions examples/earthquakes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Get the current USGS earthquake feed and add it to a DecoratedMap."""
from __future__ import print_function
from motionless import LatLonMarker, DecoratedMap
from motionless import DecoratedMap, LatLonMarker

try:
from urllib import request
Expand Down
9 changes: 4 additions & 5 deletions examples/munich.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
"""Parse a GPS track and add it to a DecoratedMap."""
from __future__ import print_function
import xml.sax
import os
from motionless import LatLonMarker, DecoratedMap
import xml.sax

from motionless import DecoratedMap, LatLonMarker

current_dir = os.path.dirname(os.path.abspath(__file__))

class GPXHandler(xml.sax.handler.ContentHandler):
"""GPS track parser"""
def __init__(self, gmap):
self.gmap = gmap
self.first = True
self.first = True
self.prev = None

def startElement(self, name, attrs):
if name == 'trkpt':
if name == 'trkpt':
self.gmap.add_path_latlon(attrs['lat'], attrs['lon'])
self.prev = (attrs['lat'], attrs['lon'])
if self.first:
Expand Down
65 changes: 31 additions & 34 deletions motionless/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import base64
import hmac
import hashlib
import hmac
import re

try:
from urllib import quote

from urlparse import urlparse
except ImportError:
from urllib.parse import quote, urlparse
Expand All @@ -18,7 +20,7 @@
For details about the GoogleStatic Map API see:
http://code.google.com/apis/maps/documentation/staticmaps/

If you encounter problems, log an issue on github.
If you encounter problems, log an issue on github.

Copyright 2010 Ryan A Cox - [email protected]

Expand All @@ -38,10 +40,10 @@


__author__ = "Ryan Cox <[email protected]>"
__version__ = "1.4.dev"
__version__ = "1.4.0"


class Color(object):
class Color:
COLORS = ['black', 'brown', 'green', 'purple',
'yellow', 'blue', 'gray', 'orange', 'red', 'white']
pat = re.compile("0x[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8}")
Expand All @@ -50,22 +52,20 @@ class Color(object):
def is_valid_color(color):
return Color.pat.match(color) or color in Color.COLORS

class Marker(object):
class Marker:
SIZES = ['tiny', 'mid', 'small']
LABELS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

def __init__(self, size, color, label, icon_url):
if size and size not in Marker.SIZES:
raise ValueError(
"[%s] is not a valid marker size. Valid sizes include %s" %
(size, Marker.SIZES))
if label and (len(label) != 1 or not label in Marker.LABELS):
f"[{size}] is not a valid marker size. Valid sizes include {Marker.SIZES}")
if label and (len(label) != 1 or label not in Marker.LABELS):
raise ValueError(
"[%s] is not a valid label. Valid labels are a single character 'A'..'Z' or '0'..'9'" % label)
if color and not Color.is_valid_color(color):
raise ValueError(
"[%s] is not a valid color. Valid colors include %s" %
(color, Color.COLORS))
f"[{color}] is not a valid color. Valid colors include {Color.COLORS}")
if icon_url and not self.check_icon_url(icon_url):
raise ValueError(
"[%s] is not a valid url." % icon_url
Expand Down Expand Up @@ -95,7 +95,7 @@ def __init__(self, lat, lon, size=None, color=None, label=None, icon_url=None):
self.longitude = lon


class Map(object):
class Map:
MAX_URL_LEN = 8192 # https://developers.google.com/maps/documentation/static-maps/intro#url-size-restriction

def __init__(self, size_x, size_y, maptype, zoom=None, scale=1, key=None, language='en', style=None, clientid=None, secret=None, channel=None):
Expand Down Expand Up @@ -150,8 +150,7 @@ def _sign(self, url):
def _check_url(self, url):
if len(url) > Map.MAX_URL_LEN:
raise ValueError(
"Generated URL is %s characters in length. Maximum is %s" %
(len(url), Map.MAX_URL_LEN))
f"Generated URL is {len(url)} characters in length. Maximum is {Map.MAX_URL_LEN}")


class CenterMap(Map):
Expand All @@ -163,12 +162,12 @@ def __init__(self, address=None, lat=None, lon=None, zoom=17, size_x=400,
if address:
self.center = quote(address)
elif lat and lon:
self.center = "%s,%s" % (lat, lon)
self.center = f"{lat},{lon}"
else:
self.center = "1600 Amphitheatre Parkway Mountain View, CA"

def generate_url(self):
query = "%smaptype=%s&format=%s&scale=%s&center=%s&zoom=%s&size=%sx%s&sensor=%s&language=%s" % (
query = "{}maptype={}&format={}&scale={}&center={}&zoom={}&size={}x{}&sensor={}&language={}".format(
self._get_key(),
self.maptype,
self.format,
Expand Down Expand Up @@ -198,10 +197,10 @@ def add_address(self, address):
self.locations.append(quote(address))

def add_latlon(self, lat, lon):
self.locations.append("%s,%s" % (quote(lat), quote(lon)))
self.locations.append(f"{quote(lat)},{quote(lon)}")

def generate_url(self):
query = "%smaptype=%s&format=%s&scale=%s&size=%sx%s&sensor=%s&visible=%s&language=%s" % (
query = "{}maptype={}&format={}&scale={}&size={}x{}&sensor={}&visible={}&language={}".format(
self._get_key(),
self.maptype,
self.format,
Expand Down Expand Up @@ -243,7 +242,7 @@ def __init__(self, lat=None, lon=None, zoom=None, size_x=400, size_y=400,
else:
self.simplify_threshold = simplify_threshold_meters / DecoratedMap.METERS_PER_DEGREE
if lat and lon:
self.center = "%s,%s" % (lat, lon)
self.center = f"{lat},{lon}"
else:
self.center = None

Expand All @@ -261,13 +260,11 @@ def check_parameters(self):

if not Color.is_valid_color(self.fillcolor):
raise ValueError(
"%s is not a valid fill color. Must be 24 or 32 bit value or one of %s" %
(self.fillcolor, Color.COLORS))
f"{self.fillcolor} is not a valid fill color. Must be 24 or 32 bit value or one of {Color.COLORS}")

if self.pathcolor and not Color.is_valid_color(self.pathcolor):
raise ValueError(
"%s is not a valid path color. Must be 24 or 32 bit value or one of %s" %
(self.pathcolor, Color.COLORS))
f"{self.pathcolor} is not a valid path color. Must be 24 or 32 bit value or one of {Color.COLORS}")

def _generate_markers(self):
styles = set()
Expand All @@ -285,7 +282,7 @@ def _generate_markers(self):
data[(marker.size, marker.color, marker.label, marker.icon_url)
].append(quote(marker.address))
if isinstance(marker, LatLonMarker):
location = "%s,%s" % (marker.latitude, marker.longitude)
location = f"{marker.latitude},{marker.longitude}"
data[(marker.size, marker.color, marker.label, marker.icon_url)
].append(location)
# build markers entries for URL
Expand Down Expand Up @@ -324,11 +321,11 @@ def add_path_address(self, address):
self.path.append(quote(address))

def add_path_latlon(self, lat, lon):
self.path.append("%s,%s" % (quote(str(lat)), quote(str(lon))))
self.path.append(f"{quote(str(lat))},{quote(str(lon))}")

def generate_url(self):
self.check_parameters()
query = "%smaptype=%s&format=%s&scale=%s&size=%sx%s&sensor=%s&language=%s" % (
query = "{}maptype={}&format={}&scale={}&size={}x{}&sensor={}&language={}".format(
self._get_key(),
self.maptype,
self.format,
Expand All @@ -339,36 +336,36 @@ def generate_url(self):
self.language)

if self.center:
query = "%s&center=%s" % (query, self.center)
query = f"{query}&center={self.center}"

if self.zoom:
query = "%s&zoom=%s" % (query, self.zoom)
query = f"{query}&zoom={self.zoom}"

if len(self.markers) > 0:
query = "%s&%s" % (query, self._generate_markers())
query = f"{query}&{self._generate_markers()}"

if len(self.path) > 0:
query = "%s&path=" % query

if self.pathcolor:
query = "%scolor:%s|" % (query, self.pathcolor)
query = f"{query}color:{self.pathcolor}|"

if self.pathweight:
query = "%sweight:%s|" % (query, self.pathweight)
query = f"{query}weight:{self.pathweight}|"

if self.region:
query = "%sfillcolor:%s|" % (query, self.fillcolor)
query = f"{query}fillcolor:{self.fillcolor}|"

query = "%senc:%s" % (query, quote(self._polyencode()))
query = f"{query}enc:{quote(self._polyencode())}"

if self.style:
for style_map in self.style:
query = "%s&style=feature:%s|element:%s|" % (
query = "{}&style=feature:{}|element:{}|".format(
query,
(style_map['feature'] if 'feature' in style_map else 'all'),
(style_map['element'] if 'element' in style_map else 'all'))
for prop, rule in style_map['rules'].items():
query = "%s%s:%s|" % (query, prop, str(rule).replace('#', '0x'))
query = "{}{}:{}|".format(query, prop, str(rule).replace('#', '0x'))

if self.channel:
query += '&channel=%s' % (self.channel)
Expand Down
3 changes: 2 additions & 1 deletion motionless/gpolyencode.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@
POSSIBILITY OF SUCH DAMAGE.
"""
import math

try:
from StringIO import StringIO
except ImportError:
from io import StringIO


class GPolyEncoder(object):
class GPolyEncoder:

def __init__(self, num_levels=18, zoom_factor=2, threshold=0.00001,
force_endpoints=True):
Expand Down
Loading