diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml
new file mode 100644
index 0000000..2047400
--- /dev/null
+++ b/.github/workflows/pypi-release.yml
@@ -0,0 +1,25 @@
+name: PyPI release
+on: [push]
+
+jobs:
+ pypi:
+ runs-on: ubuntu-18.04
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python 3.x
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.x
+ - name: Install dependencies
+ run: |
+ python -m pip install --user --upgrade setuptools wheel
+ - name: Build
+ run: |
+ python setup.py sdist bdist_wheel
+ python setup_meta.py sdist bdist_wheel
+ - name: Publish package
+ if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
+ uses: pypa/gh-action-pypi-publish@v1.1.0
+ with:
+ user: __token__
+ password: ${{ secrets.pypi_password }}
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
index d4f4ce6..4b3ec81 100644
--- a/.github/workflows/pythonapp.yml
+++ b/.github/workflows/pythonapp.yml
@@ -1,4 +1,4 @@
-name: Garden flower
+name: Tests
on: [push, pull_request]
jobs:
@@ -55,7 +55,7 @@ jobs:
draft: true
- name: Test with pytest
run: |
- python3 -m pytest --cov=kivy_garden.mapview --cov-report term --cov-branch tests/
+ python3 -m pytest tests/
- name: Coveralls
# use this custom action until Coveralls fixes uptream issues
# https://github.com/coverallsapp/github-action/issues/30
@@ -92,7 +92,7 @@ jobs:
run: python -m pip install -e .[dev,ci] --extra-index-url https://kivy-garden.github.io/simple/
- name: Test with pytest
run: |
- python -m pytest --cov=kivy_garden.mapview --cov-report term --cov-branch tests/
+ python -m pytest tests/
docs:
runs-on: ubuntu-18.04
diff --git a/.travis.yml b/.travis.yml
index 69b0f5e..d2b04cd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,14 +11,3 @@ install:
script:
- docker run -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix mapview-linux /bin/sh -c 'tox'
-
-# Deploy to PyPI using token set in `PYPI_PASSWORD` environment variable
-# https://pypi.org/manage/account/token/
-# https://travis-ci.com/github/kivy-garden/mapview/settings
-deploy:
- provider: pypi
- distributions: sdist bdist_wheel
- user: "__token__"
- on:
- tags: true
- repo: kivy-garden/mapview
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30159cc..d543f75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,12 @@
# Change Log
+## [Unreleased]
+
+ - Fix `Downloader` now checks for HTTP status code, refs #6
+
## [1.0.2]
- - Fix `AttributeError` on `GeoJsonMapLayer.canvas_line`
+ - Fix `AttributeError` on `GeoJsonMapLayer.canvas_line`, refs #12
## [1.0.1]
diff --git a/Dockerfile b/Dockerfile
index dda88e3..c34bf81 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,35 +5,21 @@
# docker run mapview-linux /bin/sh -c 'tox'
# Or for interactive shell:
# docker run -it --rm mapview-linux
+# For using the UI from Docker you may need to:
+# xhost +local:
FROM ubuntu:18.04
-# configure locale
-RUN apt -y update -qq > /dev/null && apt install --yes --no-install-recommends \
- locales \
- && locale-gen en_US.UTF-8 \
- && apt -y autoremove \
- && apt -y clean \
- && rm -rf /var/lib/apt/lists/*
-ENV LANG="en_US.UTF-8" \
- LANGUAGE="en_US.UTF-8" \
- LC_ALL="en_US.UTF-8"
-
# install system dependencies
RUN apt -y update -qq > /dev/null && apt install --yes --no-install-recommends \
build-essential \
- git \
- lsb-release \
libsdl2-dev \
libsdl2-image-dev \
libsdl2-mixer-dev \
libsdl2-ttf-dev \
- libssl-dev \
- make \
pkg-config \
python3-pip \
python3-setuptools \
tox \
- virtualenv \
&& python3 -m pip install --upgrade --no-cache setuptools \
&& apt -y autoremove \
&& apt -y clean \
diff --git a/README.md b/README.md
index 6141114..7655eaf 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,9 @@
# Mapview
-[](https://github.com/kivy-garden/mapview/actions)
+[](https://github.com/kivy-garden/mapview/actions?query=workflow%3ATests)
[](https://travis-ci.com/kivy-garden/mapview)
[](https://coveralls.io/github/kivy-garden/mapview?branch=develop)
+[](https://badge.fury.io/py/mapview)
Mapview is a Kivy widget for displaying interactive maps. It has been
designed with lot of inspirations of
@@ -31,13 +32,7 @@ the latests state-of-the-art Kivy's methods.
# Requirements
-It requires the `concurrent.futures` and `requests`. If you are on python 2.7,
-you can use `futures`:
-
-```
-pip install futures requests
-```
-
+It requires the `concurrent.futures` and `requests`.
If you use it on Android / iOS, don't forget to add `openssl` as a requirements,
otherwise you'll have an issue when importing `urllib3` from `requests`.
@@ -64,7 +59,9 @@ class MapViewApp(App):
MapViewApp().run()
```
-More extensive documentation will come soon.
+Find out more:
+- [examples/](https://github.com/kivy-garden/mapview/tree/master/examples)
+-
Contributing
diff --git a/examples/clustered_geojson.py b/examples/clustered_geojson.py
index 06ecbf1..8503a4a 100644
--- a/examples/clustered_geojson.py
+++ b/examples/clustered_geojson.py
@@ -1,14 +1,11 @@
-from kivy.base import runTouchApp
import sys
-if __name__ == '__main__' and __package__ is None:
- from os import path
- sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
+from kivy.base import runTouchApp
-from kivy_garden.mapview import MapView, MapMarker
-from kivy_garden.mapview.geojson import GeoJsonMapLayer
+from kivy_garden.mapview import MapMarker, MapView
from kivy_garden.mapview.clustered_marker_layer import ClusteredMarkerLayer
-from kivy_garden.mapview.utils import haversine, get_zoom_for_radius
+from kivy_garden.mapview.geojson import GeoJsonMapLayer
+from kivy_garden.mapview.utils import get_zoom_for_radius, haversine
source = sys.argv[1]
@@ -27,9 +24,7 @@
view = MapView(**options)
view.add_layer(layer)
-marker_layer = ClusteredMarkerLayer(
- cluster_radius=200
-)
+marker_layer = ClusteredMarkerLayer(cluster_radius=200)
view.add_layer(marker_layer)
# create marker if they exists
diff --git a/examples/map_browser.py b/examples/map_browser.py
index f8e5117..6c4e475 100644
--- a/examples/map_browser.py
+++ b/examples/map_browser.py
@@ -3,9 +3,11 @@
if __name__ == '__main__' and __package__ is None:
from os import sys, path
+
sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
-root = Builder.load_string("""
+root = Builder.load_string(
+ """
#:import MapSource kivy_garden.mapview.MapSource
:
@@ -69,6 +71,7 @@
text: "Longitude: {}".format(mapview.lon)
Label:
text: "Latitude: {}".format(mapview.lat)
- """)
+ """
+)
runTouchApp(root)
diff --git a/examples/map_with_marker_popup.py b/examples/map_with_marker_popup.py
index 642c87a..e26c906 100644
--- a/examples/map_with_marker_popup.py
+++ b/examples/map_with_marker_popup.py
@@ -1,13 +1,16 @@
import sys
+
from kivy.base import runTouchApp
from kivy.lang import Builder
if __name__ == '__main__' and __package__ is None:
from os import path
+
sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
-root = Builder.load_string("""
+root = Builder.load_string(
+ """
#:import sys sys
#:import MapSource kivy_garden.mapview.MapSource
MapView:
@@ -33,6 +36,7 @@
markup: True
halign: "center"
-""")
+"""
+)
runTouchApp(root)
diff --git a/examples/simple_geojson.py b/examples/simple_geojson.py
index 29b8a9f..b8e3931 100644
--- a/examples/simple_geojson.py
+++ b/examples/simple_geojson.py
@@ -1,13 +1,10 @@
-from kivy.base import runTouchApp
import sys
-if __name__ == '__main__' and __package__ is None:
- from os import path
- sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
+from kivy.base import runTouchApp
-from kivy_garden.mapview import MapView, MapMarker
+from kivy_garden.mapview import MapMarker, MapView
from kivy_garden.mapview.geojson import GeoJsonMapLayer
-from kivy_garden.mapview.utils import haversine, get_zoom_for_radius
+from kivy_garden.mapview.utils import get_zoom_for_radius, haversine
if len(sys.argv) > 1:
source = sys.argv[1]
diff --git a/examples/simple_map.py b/examples/simple_map.py
index 86256ea..3e279be 100644
--- a/examples/simple_map.py
+++ b/examples/simple_map.py
@@ -1,11 +1,8 @@
import sys
-from kivy.base import runTouchApp
-if __name__ == '__main__' and __package__ is None:
- from os import path
- sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
+from kivy.base import runTouchApp
-from kivy_garden.mapview import MapView, MapSource
+from kivy_garden.mapview import MapSource, MapView
kwargs = {}
if len(sys.argv) > 1:
diff --git a/examples/simple_mbtiles.py b/examples/simple_mbtiles.py
index be3e364..d94c8f8 100644
--- a/examples/simple_mbtiles.py
+++ b/examples/simple_mbtiles.py
@@ -9,18 +9,18 @@
"""
import sys
+
from kivy.base import runTouchApp
-if __name__ == '__main__' and __package__ is None:
- from os import path
- sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
from kivy_garden.mapview import MapView
from kivy_garden.mapview.mbtsource import MBTilesMapSource
-
source = MBTilesMapSource(sys.argv[1])
-runTouchApp(MapView(
- map_source=source,
- lat=source.default_lat,
- lon=source.default_lon,
- zoom=source.default_zoom))
+runTouchApp(
+ MapView(
+ map_source=source,
+ lat=source.default_lat,
+ lon=source.default_lon,
+ zoom=source.default_zoom,
+ )
+)
diff --git a/examples/test_kdbush.py b/examples/test_kdbush.py
index 89b3cc0..ecb66c5 100644
--- a/examples/test_kdbush.py
+++ b/examples/test_kdbush.py
@@ -5,11 +5,13 @@
the selected red dot.
"""
+from random import random
+
from kivy.app import App
+from kivy.graphics import Canvas, Color, Ellipse, Rectangle
from kivy.uix.widget import Widget
-from kivy.graphics import Color, Rectangle, Ellipse, Canvas
+
from kivy_garden.mapview.clustered_marker_layer import KDBush, Marker
-from random import random
# creating markers
points = []
@@ -36,8 +38,7 @@ def build(self):
with self.canvas_points:
Color(1, 0, 0)
for marker in points:
- Rectangle(
- pos=(marker.x * 600, marker.y * 600), size=(2, 2))
+ Rectangle(pos=(marker.x * 600, marker.y * 600), size=(2, 2))
self.canvas.before.clear()
with self.canvas.before:
@@ -66,7 +67,7 @@ def on_touch_move(self, touch):
def select(self, x, y):
self.selection_center = (x, y)
- self.selection = kdbush.within(x / 600., y / 600., self.radius)
+ self.selection = kdbush.within(x / 600.0, y / 600.0, self.radius)
self.build()
diff --git a/kivy_garden/mapview/__init__.py b/kivy_garden/mapview/__init__.py
index 19a6280..fd2301b 100644
--- a/kivy_garden/mapview/__init__.py
+++ b/kivy_garden/mapview/__init__.py
@@ -7,26 +7,23 @@
MapView is a Kivy widget that display maps.
"""
-
-__all__ = ["Coordinate", "Bbox", "MapView", "MapSource", "MapMarker",
- "MapLayer", "MarkerMapLayer", "MapMarkerPopup"]
-__version__ = "0.2"
-
-MIN_LATITUDE = -90.
-MAX_LATITUDE = 90.
-MIN_LONGITUDE = -180.
-MAX_LONGITUDE = 180.
-CACHE_DIR = "cache"
-
-try:
- # fix if used within garden
- import sys
- sys.modules['mapview'] = sys.modules['kivy.garden.mapview.mapview']
- del sys
-except KeyError:
- pass
-
-from kivy_garden.mapview.types import Coordinate, Bbox
from kivy_garden.mapview.source import MapSource
-from kivy_garden.mapview.view import MapView, MapMarker, MapLayer, MarkerMapLayer, \
- MapMarkerPopup
+from kivy_garden.mapview.types import Bbox, Coordinate
+from kivy_garden.mapview.view import (
+ MapLayer,
+ MapMarker,
+ MapMarkerPopup,
+ MapView,
+ MarkerMapLayer,
+)
+
+__all__ = [
+ "Coordinate",
+ "Bbox",
+ "MapView",
+ "MapSource",
+ "MapMarker",
+ "MapLayer",
+ "MarkerMapLayer",
+ "MapMarkerPopup",
+]
diff --git a/kivy_garden/mapview/_version.py b/kivy_garden/mapview/_version.py
index a6221b3..976498a 100644
--- a/kivy_garden/mapview/_version.py
+++ b/kivy_garden/mapview/_version.py
@@ -1 +1 @@
-__version__ = '1.0.2'
+__version__ = "1.0.3"
diff --git a/kivy_garden/mapview/clustered_marker_layer.py b/kivy_garden/mapview/clustered_marker_layer.py
index b9c7c2e..c2eaac0 100644
--- a/kivy_garden/mapview/clustered_marker_layer.py
+++ b/kivy_garden/mapview/clustered_marker_layer.py
@@ -4,15 +4,22 @@
===================================
"""
+from math import atan, exp, floor, log, pi, sin, sqrt
from os.path import dirname, join
-from math import sin, log, pi, atan, exp, floor, sqrt
-from kivy_garden.mapview.view import MapLayer, MapMarker
+
from kivy.lang import Builder
from kivy.metrics import dp
-from kivy.properties import (ObjectProperty, NumericProperty, StringProperty, ListProperty)
+from kivy.properties import (
+ ListProperty,
+ NumericProperty,
+ ObjectProperty,
+ StringProperty,
+)
+from kivy_garden.mapview.view import MapLayer, MapMarker
-Builder.load_string("""
+Builder.load_string(
+ """
:
size_hint: None, None
source: root.source
@@ -25,12 +32,13 @@
size: root.size
text: "{}".format(root.num_points)
font_size: dp(18)
-""")
+"""
+)
# longitude/latitude to spherical mercator in [0..1] range
def lngX(lng):
- return lng / 360. + 0.5
+ return lng / 360.0 + 0.5
def latY(lat):
@@ -38,8 +46,8 @@ def latY(lat):
return 0
if lat == -90:
return 1
- s = sin(lat * pi / 180.)
- y = (0.5 - 0.25 * log((1 + s) / (1 - s)) / pi)
+ s = sin(lat * pi / 180.0)
+ y = 0.5 - 0.25 * log((1 + s) / (1 - s)) / pi
return min(1, max(0, y))
@@ -53,11 +61,13 @@ def yLat(y):
return 360 * atan(exp(y2)) / pi - 90
-class KDBush(object):
- # kdbush implementation from https://github.com/mourner/kdbush/blob/master/src/kdbush.js
- #
+class KDBush:
+ """
+ kdbush implementation from:
+ https://github.com/mourner/kdbush/blob/master/src/kdbush.js
+ """
+
def __init__(self, points, node_size=64):
- super(KDBush, self).__init__()
self.points = points
self.node_size = node_size
@@ -71,8 +81,9 @@ def __init__(self, points, node_size=64):
self._sort(ids, coords, node_size, 0, len(ids) - 1, 0)
def range(self, min_x, min_y, max_x, max_y):
- return self._range(self.ids, self.coords, min_x, min_y, max_x, max_y,
- self.node_size)
+ return self._range(
+ self.ids, self.coords, min_x, min_y, max_x, max_y, self.node_size
+ )
def within(self, x, y, r):
return self._within(self.ids, self.coords, x, y, r, self.node_size)
@@ -80,7 +91,7 @@ def within(self, x, y, r):
def _sort(self, ids, coords, node_size, left, right, depth):
if right - left <= node_size:
return
- m = int(floor((left + right) / 2.))
+ m = int(floor((left + right) / 2.0))
self._select(ids, coords, m, left, right, depth % 2)
self._sort(ids, coords, node_size, left, m - 1, depth + 1)
self._sort(ids, coords, node_size, m + 1, right, depth + 1)
@@ -92,9 +103,8 @@ def _select(self, ids, coords, k, left, right, inc):
n = float(right - left + 1)
m = k - left + 1
z = log(n)
- s = 0.5 + exp(2 * z / 3.)
- sd = 0.5 * sqrt(z * s * (n - s) / n) * (-1
- if (m - n / 2.) < 0 else 1)
+ s = 0.5 + exp(2 * z / 3.0)
+ sd = 0.5 * sqrt(z * s * (n - s) / n) * (-1 if (m - n / 2.0) < 0 else 1)
new_left = max(left, int(floor(k - m * s / n + sd)))
new_right = min(right, int(floor(k + (n - m) * s / n + sd)))
self._select(ids, coords, k, new_left, new_right, inc)
@@ -152,26 +162,25 @@ def _range(self, ids, coords, min_x, min_y, max_x, max_y, node_size):
for i in range(left, right + 1):
x = coords[2 * i]
y = coords[2 * i + 1]
- if (x >= min_x and x <= max_x and y >= min_y and
- y <= max_y):
+ if x >= min_x and x <= max_x and y >= min_y and y <= max_y:
result.append(ids[i])
continue
- m = int(floor((left + right) / 2.))
+ m = int(floor((left + right) / 2.0))
x = coords[2 * m]
y = coords[2 * m + 1]
- if (x >= min_x and x <= max_x and y >= min_y and y <= max_y):
+ if x >= min_x and x <= max_x and y >= min_y and y <= max_y:
result.append(ids[m])
nextAxis = (axis + 1) % 2
- if (min_x <= x if axis == 0 else min_y <= y):
+ if min_x <= x if axis == 0 else min_y <= y:
stack.append(left)
stack.append(m - 1)
stack.append(nextAxis)
- if (max_x >= x if axis == 0 else max_y >= y):
+ if max_x >= x if axis == 0 else max_y >= y:
stack.append(m + 1)
stack.append(right)
stack.append(nextAxis)
@@ -195,7 +204,7 @@ def _within(self, ids, coords, qx, qy, r, node_size):
result.append(ids[i])
continue
- m = int(floor((left + right) / 2.))
+ m = int(floor((left + right) / 2.0))
x = coords[2 * m]
y = coords[2 * m + 1]
@@ -222,9 +231,8 @@ def _sq_dist(self, ax, ay, bx, by):
return dx * dx + dy * dy
-class Cluster(object):
+class Cluster:
def __init__(self, x, y, num_points, id, props):
- super(Cluster, self).__init__()
self.x = x
self.y = y
self.num_points = num_points
@@ -239,9 +247,8 @@ def __init__(self, x, y, num_points, id, props):
self.lat = yLat(y)
-class Marker(object):
+class Marker:
def __init__(self, lon, lat, cls=MapMarker, options=None):
- super(Marker, self).__init__()
self.lon = lon
self.lat = lat
self.cls = cls
@@ -258,21 +265,16 @@ def __init__(self, lon, lat, cls=MapMarker, options=None):
self.widget = None
def __repr__(self):
- return "".format(self.lon, self.lat,
- self.source)
+ return "".format(
+ self.lon, self.lat, self.source
+ )
-class SuperCluster(object):
+class SuperCluster:
"""Port of supercluster from mapbox in pure python
"""
- def __init__(self,
- min_zoom=0,
- max_zoom=16,
- radius=40,
- extent=512,
- node_size=64):
- super(SuperCluster, self).__init__()
+ def __init__(self, min_zoom=0, max_zoom=16, radius=40, extent=512, node_size=64):
self.min_zoom = min_zoom
self.max_zoom = max_zoom
self.radius = radius
@@ -284,6 +286,7 @@ def load(self, points):
Once loaded, the index is immutable.
"""
from time import time
+
self.trees = {}
self.points = points
@@ -365,7 +368,9 @@ def _cluster(self, points, zoom):
c_append(p)
else:
p.parent_id = i
- c_append(Cluster(wx / num_points, wy / num_points, num_points, i, props))
+ c_append(
+ Cluster(wx / num_points, wy / num_points, num_points, i, props)
+ )
return clusters
@@ -373,7 +378,7 @@ class ClusterMapMarker(MapMarker):
source = StringProperty(join(dirname(__file__), "icons", "cluster.png"))
cluster = ObjectProperty()
num_points = NumericProperty()
- text_color = ListProperty([.1, .1, .1, 1])
+ text_color = ListProperty([0.1, 0.1, 0.1, 1])
def on_cluster(self, instance, cluster):
self.num_points = cluster.num_points
@@ -393,7 +398,7 @@ class ClusteredMarkerLayer(MapLayer):
def __init__(self, **kwargs):
self.cluster = None
self.cluster_markers = []
- super(ClusteredMarkerLayer, self).__init__(**kwargs)
+ super().__init__(**kwargs)
def add_marker(self, lon, lat, cls=MapMarker, options=None):
if options is None:
@@ -427,7 +432,7 @@ def build_cluster(self):
max_zoom=self.cluster_max_zoom,
radius=self.cluster_radius,
extent=self.cluster_extent,
- node_size=self.cluster_node_size
+ node_size=self.cluster_node_size,
)
self.cluster.load(self.cluster_markers)
diff --git a/kivy_garden/mapview/constants.py b/kivy_garden/mapview/constants.py
new file mode 100644
index 0000000..b6998f8
--- /dev/null
+++ b/kivy_garden/mapview/constants.py
@@ -0,0 +1,5 @@
+MIN_LATITUDE = -90.0
+MAX_LATITUDE = 90.0
+MIN_LONGITUDE = -180.0
+MAX_LONGITUDE = 180.0
+CACHE_DIR = "cache"
diff --git a/kivy_garden/mapview/downloader.py b/kivy_garden/mapview/downloader.py
index e5f887e..2706ad8 100644
--- a/kivy_garden/mapview/downloader.py
+++ b/kivy_garden/mapview/downloader.py
@@ -2,30 +2,35 @@
__all__ = ["Downloader"]
-from kivy.clock import Clock
-from os.path import join, exists
-from os import makedirs, environ
+import logging
+import traceback
from concurrent.futures import ThreadPoolExecutor, TimeoutError, as_completed
+from os import environ, makedirs
+from os.path import exists, join
from random import choice
-import requests
-import traceback
from time import time
-from kivy_garden.mapview import CACHE_DIR
+import requests
+from kivy.clock import Clock
+from kivy.logger import LOG_LEVELS, Logger
+
+from kivy_garden.mapview.constants import CACHE_DIR
+
+if "MAPVIEW_DEBUG_DOWNLOADER" in environ:
+ Logger.setLevel(LOG_LEVELS['debug'])
-DEBUG = "MAPVIEW_DEBUG_DOWNLOADER" in environ
# user agent is needed because since may 2019 OSM gives me a 429 or 403 server error
# I tried it with a simpler one (just Mozilla/5.0) this also gets rejected
USER_AGENT = 'Kivy-garden.mapview'
-class Downloader(object):
+class Downloader:
_instance = None
MAX_WORKERS = 5
CAP_TIME = 0.064 # 15 FPS
@staticmethod
- def instance(cache_dir):
+ def instance(cache_dir=None):
if Downloader._instance is None:
if not cache_dir:
cache_dir = CACHE_DIR
@@ -38,12 +43,11 @@ def __init__(self, max_workers=None, cap_time=None, **kwargs):
max_workers = Downloader.MAX_WORKERS
if cap_time is None:
cap_time = Downloader.CAP_TIME
- super(Downloader, self).__init__()
self.is_paused = False
self.cap_time = cap_time
self.executor = ThreadPoolExecutor(max_workers=max_workers)
self._futures = []
- Clock.schedule_interval(self._check_executor, 1 / 60.)
+ Clock.schedule_interval(self._check_executor, 1 / 60.0)
if not exists(self.cache_dir):
makedirs(self.cache_dir)
@@ -52,47 +56,45 @@ def submit(self, f, *args, **kwargs):
self._futures.append(future)
def download_tile(self, tile):
- if DEBUG:
- print("Downloader: queue(tile) zoom={} x={} y={}".format(
- tile.zoom, tile.tile_x, tile.tile_y))
+ Logger.debug(
+ "Downloader: queue(tile) zoom={} x={} y={}".format(
+ tile.zoom, tile.tile_x, tile.tile_y
+ )
+ )
future = self.executor.submit(self._load_tile, tile)
self._futures.append(future)
def download(self, url, callback, **kwargs):
- if DEBUG:
- print("Downloader: queue(url) {}".format(url))
- future = self.executor.submit(
- self._download_url, url, callback, kwargs)
+ Logger.debug("Downloader: queue(url) {}".format(url))
+ future = self.executor.submit(self._download_url, url, callback, kwargs)
self._futures.append(future)
def _download_url(self, url, callback, kwargs):
- if DEBUG:
- print("Downloader: download(url) {}".format(url))
- r = requests.get(url, **kwargs)
- return callback, (url, r, )
+ Logger.debug("Downloader: download(url) {}".format(url))
+ response = requests.get(url, **kwargs)
+ response.raise_for_status()
+ return callback, (url, response)
def _load_tile(self, tile):
if tile.state == "done":
return
cache_fn = tile.cache_fn
if exists(cache_fn):
- if DEBUG:
- print("Downloader: use cache {}".format(cache_fn))
- return tile.set_source, (cache_fn, )
+ Logger.debug("Downloader: use cache {}".format(cache_fn))
+ return tile.set_source, (cache_fn,)
tile_y = tile.map_source.get_row_count(tile.zoom) - tile.tile_y - 1
- uri = tile.map_source.url.format(z=tile.zoom, x=tile.tile_x, y=tile_y,
- s=choice(tile.map_source.subdomains))
- if DEBUG:
- print("Downloader: download(tile) {}".format(uri))
- req = requests.get(uri, headers={'User-agent': USER_AGENT}, timeout=5)
+ uri = tile.map_source.url.format(
+ z=tile.zoom, x=tile.tile_x, y=tile_y, s=choice(tile.map_source.subdomains)
+ )
+ Logger.debug("Downloader: download(tile) {}".format(uri))
+ response = requests.get(uri, headers={'User-agent': USER_AGENT}, timeout=5)
try:
- req.raise_for_status()
- data = req.content
+ response.raise_for_status()
+ data = response.content
with open(cache_fn, "wb") as fd:
fd.write(data)
- if DEBUG:
- print("Downloaded {} bytes: {}".format(len(data), uri))
- return tile.set_source, (cache_fn, )
+ Logger.debug("Downloaded {} bytes: {}".format(len(data), uri))
+ return tile.set_source, (cache_fn,)
except Exception as e:
print("Downloader error: {!r}".format(e))
diff --git a/kivy_garden/mapview/geojson.py b/kivy_garden/mapview/geojson.py
index 7c0fccc..5fafe62 100644
--- a/kivy_garden/mapview/geojson.py
+++ b/kivy_garden/mapview/geojson.py
@@ -20,16 +20,26 @@
__all__ = ["GeoJsonMapLayer"]
import json
-from kivy.properties import StringProperty, ObjectProperty
-from kivy.graphics import (Canvas, PushMatrix, PopMatrix, MatrixInstruction,
- Translate, Scale)
-from kivy.graphics import Mesh, Line, Color
-from kivy.graphics.tesselator import Tesselator, WINDING_ODD, TYPE_POLYGONS
-from kivy.utils import get_color_from_hex
+
+from kivy.graphics import (
+ Canvas,
+ Color,
+ Line,
+ MatrixInstruction,
+ Mesh,
+ PopMatrix,
+ PushMatrix,
+ Scale,
+ Translate,
+)
+from kivy.graphics.tesselator import TYPE_POLYGONS, WINDING_ODD, Tesselator
from kivy.metrics import dp
-from kivy_garden.mapview import CACHE_DIR
-from kivy_garden.mapview.view import MapLayer
+from kivy.properties import ObjectProperty, StringProperty
+from kivy.utils import get_color_from_hex
+
+from kivy_garden.mapview.constants import CACHE_DIR
from kivy_garden.mapview.downloader import Downloader
+from kivy_garden.mapview.view import MapLayer
COLORS = {
'aliceblue': '#f0f8ff',
@@ -178,7 +188,7 @@
'white': '#ffffff',
'whitesmoke': '#f5f5f5',
'yellow': '#ffff00',
- 'yellowgreen': '#9acd32'
+ 'yellowgreen': '#9acd32',
}
@@ -195,7 +205,7 @@ class GeoJsonMapLayer(MapLayer):
def __init__(self, **kwargs):
self.first_time = True
self.initial_zoom = None
- super(GeoJsonMapLayer, self).__init__(**kwargs)
+ super().__init__(**kwargs)
with self.canvas:
self.canvas_polygon = Canvas()
self.canvas_line = Canvas()
@@ -216,12 +226,12 @@ def reposition(self):
if zoom is None:
self.initial_zoom = zoom = pzoom
if zoom != pzoom:
- diff = 2**(pzoom - zoom)
+ diff = 2 ** (pzoom - zoom)
vx /= diff
vy /= diff
self.g_scale.x = self.g_scale.y = diff
else:
- self.g_scale.x = self.g_scale.y = 1.
+ self.g_scale.x = self.g_scale.y = 1.0
self.g_translate.xy = vx, vy
self.g_matrix.matrix = self.parent._scatter.transform
@@ -269,39 +279,38 @@ def _get_bounds(feature):
for polygon in geometry["coordinates"]:
for coordinate in polygon[0]:
_submit_coordinate(coordinate)
+
self.traverse_feature(_get_bounds)
return bounds
@property
def center(self):
min_lon, max_lon, min_lat, max_lat = self.bounds
- cx = (max_lon - min_lon) / 2.
- cy = (max_lat - min_lat) / 2.
+ cx = (max_lon - min_lon) / 2.0
+ cy = (max_lat - min_lat) / 2.0
return min_lon + cx, min_lat + cy
def on_geojson(self, instance, geojson, update=False):
if self.parent is None:
return
if not update:
- # print "Reload geojson (polygon)"
self.g_canvas_polygon.clear()
self._geojson_part(geojson, geotype="Polygon")
- # print "Reload geojson (LineString)"
self.canvas_line.clear()
self._geojson_part(geojson, geotype="LineString")
def on_source(self, instance, value):
- if value.startswith("http://") or value.startswith("https://"):
- Downloader.instance(
- cache_dir=self.cache_dir
- ).download(value, self._load_geojson_url)
+ if value.startswith(("http://", "https://")):
+ Downloader.instance(cache_dir=self.cache_dir).download(
+ value, self._load_geojson_url
+ )
else:
with open(value, "rb") as fd:
geojson = json.load(fd)
self.geojson = geojson
- def _load_geojson_url(self, url, r):
- self.geojson = r.json()
+ def _load_geojson_url(self, url, response):
+ self.geojson = response.json()
def _geojson_part(self, part, geotype=None):
tp = part["type"]
@@ -344,10 +353,8 @@ def _geojson_part_geometry(self, geometry, properties):
graphics.append(Color(*color))
for vertices, indices in tess.meshes:
graphics.append(
- Mesh(
- vertices=vertices,
- indices=indices,
- mode="triangle_fan"))
+ Mesh(vertices=vertices, indices=indices, mode="triangle_fan")
+ )
elif tp == "LineString":
stroke = get_color_from_hex(properties.get("stroke", "#ffffff"))
diff --git a/kivy_garden/mapview/mbtsource.py b/kivy_garden/mapview/mbtsource.py
index 4d8eabb..e90f87a 100644
--- a/kivy_garden/mapview/mbtsource.py
+++ b/kivy_garden/mapview/mbtsource.py
@@ -10,17 +10,20 @@
__all__ = ["MBTilesMapSource"]
-from kivy_garden.mapview.source import MapSource
-from kivy_garden.mapview.downloader import Downloader
-from kivy.core.image import Image as CoreImage, ImageLoader
-import threading
-import sqlite3
import io
+import sqlite3
+import threading
+
+from kivy.core.image import Image as CoreImage
+from kivy.core.image import ImageLoader
+
+from kivy_garden.mapview.downloader import Downloader
+from kivy_garden.mapview.source import MapSource
class MBTilesMapSource(MapSource):
def __init__(self, filename, **kwargs):
- super(MBTilesMapSource, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.filename = filename
self.db = sqlite3.connect(filename)
@@ -33,21 +36,21 @@ def __init__(self, filename, **kwargs):
self.max_zoom = int(metadata["maxzoom"])
self.attribution = metadata.get("attribution", "")
self.bounds = bounds = None
- cx = cy = 0.
+ cx = cy = 0.0
cz = 5
if "bounds" in metadata:
self.bounds = bounds = map(float, metadata["bounds"].split(","))
if "center" in metadata:
cx, cy, cz = map(float, metadata["center"].split(","))
elif self.bounds:
- cx = (bounds[2] + bounds[0]) / 2.
- cy = (bounds[3] + bounds[1]) / 2.
+ cx = (bounds[2] + bounds[0]) / 2.0
+ cy = (bounds[3] + bounds[1]) / 2.0
cz = self.min_zoom
self.default_lon = cx
self.default_lat = cy
self.default_zoom = int(cz)
self.projection = metadata.get("projection", "")
- self.is_xy = (self.projection == "xy")
+ self.is_xy = self.projection == "xy"
def fill_tile(self, tile):
if tile.state == "done":
@@ -63,10 +66,12 @@ def _load_tile(self, tile):
# get the right tile
c = ctx.db.cursor()
c.execute(
- ("SELECT tile_data FROM tiles WHERE "
- "zoom_level=? AND tile_column=? AND tile_row=?"),
- (tile.zoom, tile.tile_x, tile.tile_y))
- # print "fetch", tile.zoom, tile.tile_x, tile.tile_y
+ (
+ "SELECT tile_data FROM tiles WHERE "
+ "zoom_level=? AND tile_column=? AND tile_row=?"
+ ),
+ (tile.zoom, tile.tile_x, tile.tile_y),
+ )
row = c.fetchone()
if not row:
tile.state = "done"
@@ -79,15 +84,17 @@ def _load_tile(self, tile):
# android issue, "buffer" does not have the buffer interface
# ie row[0] buffer is not compatible with BytesIO on Android??
data = io.BytesIO(bytes(row[0]))
- im = CoreImage(data, ext='png',
- filename="{}.{}.{}.png".format(tile.zoom, tile.tile_x,
- tile.tile_y))
+ im = CoreImage(
+ data,
+ ext='png',
+ filename="{}.{}.{}.png".format(tile.zoom, tile.tile_x, tile.tile_y),
+ )
if im is None:
tile.state = "done"
return
- return self._load_tile_done, (tile, im, )
+ return self._load_tile_done, (tile, im,)
def _load_tile_done(self, tile, im):
tile.texture = im.texture
@@ -96,19 +103,19 @@ def _load_tile_done(self, tile, im):
def get_x(self, zoom, lon):
if self.is_xy:
return lon
- return super(MBTilesMapSource, self).get_x(zoom, lon)
+ return super().get_x(zoom, lon)
def get_y(self, zoom, lat):
if self.is_xy:
return lat
- return super(MBTilesMapSource, self).get_y(zoom, lat)
+ return super().get_y(zoom, lat)
def get_lon(self, zoom, x):
if self.is_xy:
return x
- return super(MBTilesMapSource, self).get_lon(zoom, x)
+ return super().get_lon(zoom, x)
def get_lat(self, zoom, y):
if self.is_xy:
return y
- return super(MBTilesMapSource, self).get_lat(zoom, y)
+ return super().get_lat(zoom, y)
diff --git a/kivy_garden/mapview/source.py b/kivy_garden/mapview/source.py
index 7e6c346..61722fd 100644
--- a/kivy_garden/mapview/source.py
+++ b/kivy_garden/mapview/source.py
@@ -2,16 +2,23 @@
__all__ = ["MapSource"]
+import hashlib
+from math import atan, ceil, cos, exp, log, pi, tan
+
from kivy.metrics import dp
-from math import cos, ceil, log, tan, pi, atan, exp
-from kivy_garden.mapview import MIN_LONGITUDE, MAX_LONGITUDE, MIN_LATITUDE, MAX_LATITUDE, \
- CACHE_DIR
+
+from kivy_garden.mapview.constants import (
+ CACHE_DIR,
+ MAX_LATITUDE,
+ MAX_LONGITUDE,
+ MIN_LATITUDE,
+ MIN_LONGITUDE,
+)
from kivy_garden.mapview.downloader import Downloader
from kivy_garden.mapview.utils import clamp
-import hashlib
-class MapSource(object):
+class MapSource:
"""Base class for implementing a map source / provider
"""
@@ -21,34 +28,91 @@ class MapSource(object):
# list of available providers
# cache_key: (is_overlay, minzoom, maxzoom, url, attribution)
providers = {
- "osm": (0, 0, 19, "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", attribution_osm),
- "osm-hot": (0, 0, 19, "http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", ""),
- "osm-de": (0, 0, 18, "http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png", "Tiles @ OSM DE"),
- "osm-fr": (0, 0, 20, "http://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png", "Tiles @ OSM France"),
- "cyclemap": (0, 0, 17, "http://{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png", "Tiles @ Andy Allan"),
- "thunderforest-cycle": (0, 0, 19, "http://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png", attribution_thunderforest),
- "thunderforest-transport": (0, 0, 19, "http://{s}.tile.thunderforest.com/transport/{z}/{x}/{y}.png", attribution_thunderforest),
- "thunderforest-landscape": (0, 0, 19, "http://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}.png", attribution_thunderforest),
- "thunderforest-outdoors": (0, 0, 19, "http://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png", attribution_thunderforest),
-
+ "osm": (
+ 0,
+ 0,
+ 19,
+ "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
+ attribution_osm,
+ ),
+ "osm-hot": (
+ 0,
+ 0,
+ 19,
+ "http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
+ "",
+ ),
+ "osm-de": (
+ 0,
+ 0,
+ 18,
+ "http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png",
+ "Tiles @ OSM DE",
+ ),
+ "osm-fr": (
+ 0,
+ 0,
+ 20,
+ "http://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png",
+ "Tiles @ OSM France",
+ ),
+ "cyclemap": (
+ 0,
+ 0,
+ 17,
+ "http://{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png",
+ "Tiles @ Andy Allan",
+ ),
+ "thunderforest-cycle": (
+ 0,
+ 0,
+ 19,
+ "http://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png",
+ attribution_thunderforest,
+ ),
+ "thunderforest-transport": (
+ 0,
+ 0,
+ 19,
+ "http://{s}.tile.thunderforest.com/transport/{z}/{x}/{y}.png",
+ attribution_thunderforest,
+ ),
+ "thunderforest-landscape": (
+ 0,
+ 0,
+ 19,
+ "http://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}.png",
+ attribution_thunderforest,
+ ),
+ "thunderforest-outdoors": (
+ 0,
+ 0,
+ 19,
+ "http://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png",
+ attribution_thunderforest,
+ ),
# no longer available
- #"mapquest-osm": (0, 0, 19, "http://otile{s}.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpeg", "Tiles Courtesy of Mapquest", {"subdomains": "1234", "image_ext": "jpeg"}),
- #"mapquest-aerial": (0, 0, 19, "http://oatile{s}.mqcdn.com/tiles/1.0.0/sat/{z}/{x}/{y}.jpeg", "Tiles Courtesy of Mapquest", {"subdomains": "1234", "image_ext": "jpeg"}),
-
+ # "mapquest-osm": (0, 0, 19, "http://otile{s}.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpeg", "Tiles Courtesy of Mapquest", {"subdomains": "1234", "image_ext": "jpeg"}),
+ # "mapquest-aerial": (0, 0, 19, "http://oatile{s}.mqcdn.com/tiles/1.0.0/sat/{z}/{x}/{y}.jpeg", "Tiles Courtesy of Mapquest", {"subdomains": "1234", "image_ext": "jpeg"}),
# more to add with
# https://github.com/leaflet-extras/leaflet-providers/blob/master/leaflet-providers.js
# not working ?
- #"openseamap": (0, 0, 19, "http://tiles.openseamap.org/seamark/{z}/{x}/{y}.png",
+ # "openseamap": (0, 0, 19, "http://tiles.openseamap.org/seamark/{z}/{x}/{y}.png",
# "Map data @ OpenSeaMap contributors"),
}
- def __init__(self,
- url="http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
- cache_key=None, min_zoom=0, max_zoom=19, tile_size=256,
- image_ext="png",
- attribution="© OpenStreetMap contributors",
- subdomains="abc", **kwargs):
- super(MapSource, self).__init__()
+ def __init__(
+ self,
+ url="http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
+ cache_key=None,
+ min_zoom=0,
+ max_zoom=19,
+ tile_size=256,
+ image_ext="png",
+ attribution="© OpenStreetMap contributors",
+ subdomains="abc",
+ **kwargs
+ ):
if cache_key is None:
# possible cache hit, but very unlikely
cache_key = hashlib.sha224(url.encode("utf8")).hexdigest()[:10]
@@ -74,39 +138,46 @@ def from_provider(key, **kwargs):
is_overlay, min_zoom, max_zoom, url, attribution = provider[:5]
if len(provider) > 5:
options = provider[5]
- return MapSource(cache_key=key, min_zoom=min_zoom,
- max_zoom=max_zoom, url=url, cache_dir=cache_dir,
- attribution=attribution, **options)
+ return MapSource(
+ cache_key=key,
+ min_zoom=min_zoom,
+ max_zoom=max_zoom,
+ url=url,
+ cache_dir=cache_dir,
+ attribution=attribution,
+ **options
+ )
def get_x(self, zoom, lon):
"""Get the x position on the map using this map source's projection
(0, 0) is located at the top left.
"""
lon = clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE)
- return ((lon + 180.) / 360. * pow(2., zoom)) * self.dp_tile_size
+ return ((lon + 180.0) / 360.0 * pow(2.0, zoom)) * self.dp_tile_size
def get_y(self, zoom, lat):
"""Get the y position on the map using this map source's projection
(0, 0) is located at the top left.
"""
lat = clamp(-lat, MIN_LATITUDE, MAX_LATITUDE)
- lat = lat * pi / 180.
- return ((1.0 - log(tan(lat) + 1.0 / cos(lat)) / pi) /
- 2. * pow(2., zoom)) * self.dp_tile_size
+ lat = lat * pi / 180.0
+ return (
+ (1.0 - log(tan(lat) + 1.0 / cos(lat)) / pi) / 2.0 * pow(2.0, zoom)
+ ) * self.dp_tile_size
def get_lon(self, zoom, x):
"""Get the longitude to the x position in the map source's projection
"""
dx = x / float(self.dp_tile_size)
- lon = dx / pow(2., zoom) * 360. - 180.
+ lon = dx / pow(2.0, zoom) * 360.0 - 180.0
return clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE)
def get_lat(self, zoom, y):
"""Get the latitude to the y position in the map source's projection
"""
dy = y / float(self.dp_tile_size)
- n = pi - 2 * pi * dy / pow(2., zoom)
- lat = -180. / pi * atan(.5 * (exp(n) - exp(-n)))
+ n = pi - 2 * pi * dy / pow(2.0, zoom)
+ lat = -180.0 / pi * atan(0.5 * (exp(n) - exp(-n)))
return clamp(lat, MIN_LATITUDE, MAX_LATITUDE)
def get_row_count(self, zoom):
diff --git a/kivy_garden/mapview/utils.py b/kivy_garden/mapview/utils.py
index 775ed23..1ecc84f 100644
--- a/kivy_garden/mapview/utils.py
+++ b/kivy_garden/mapview/utils.py
@@ -2,7 +2,7 @@
__all__ = ["clamp", "haversine", "get_zoom_for_radius"]
-from math import radians, cos, sin, asin, sqrt, pi
+from math import asin, cos, pi, radians, sin, sqrt
from kivy.core.window import Window
from kivy.metrics import dp
@@ -24,21 +24,21 @@ def haversine(lon1, lat1, lon2, lat2):
# haversine formula
dlon = lon2 - lon1
dlat = lat2 - lat1
- a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
+ a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
c = 2 * asin(sqrt(a))
km = 6367 * c
return km
-def get_zoom_for_radius(radius_km, lat=None, tile_size=256.):
+def get_zoom_for_radius(radius_km, lat=None, tile_size=256.0):
"""See: https://wiki.openstreetmap.org/wiki/Zoom_levels"""
- radius = radius_km * 1000.
+ radius = radius_km * 1000.0
if lat is None:
- lat = 0. # Do not compensate for the latitude
+ lat = 0.0 # Do not compensate for the latitude
# Calculate the equatorial circumference based on the WGS-84 radius
- earth_circumference = 2. * pi * 6378137. * cos(lat * pi / 180.)
+ earth_circumference = 2.0 * pi * 6378137.0 * cos(lat * pi / 180.0)
# Check how many tiles that are currently in view
nr_tiles_shown = min(Window.size) / dp(tile_size)
diff --git a/kivy_garden/mapview/view.py b/kivy_garden/mapview/view.py
index b5b3d5c..48b5457 100644
--- a/kivy_garden/mapview/view.py
+++ b/kivy_garden/mapview/view.py
@@ -1,32 +1,45 @@
# coding=utf-8
-__all__ = ["MapView", "MapMarker", "MapMarkerPopup", "MapLayer",
- "MarkerMapLayer"]
+__all__ = ["MapView", "MapMarker", "MapMarkerPopup", "MapLayer", "MarkerMapLayer"]
+
+import webbrowser
+from itertools import takewhile
+from math import ceil
+from os.path import dirname, join
-from os.path import join, dirname
from kivy.clock import Clock
-from kivy.metrics import dp
-from kivy.uix.widget import Widget
-from kivy.uix.label import Label
-from kivy.uix.image import Image
-from kivy.uix.scatter import Scatter
-from kivy.uix.behaviors import ButtonBehavior
-from kivy.properties import NumericProperty, ObjectProperty, ListProperty, \
- AliasProperty, BooleanProperty, StringProperty
+from kivy.compat import string_types
from kivy.graphics import Canvas, Color, Rectangle
from kivy.graphics.transformation import Matrix
from kivy.lang import Builder
-from kivy.compat import string_types
-from math import ceil
-from kivy_garden.mapview import MIN_LONGITUDE, MAX_LONGITUDE, MIN_LATITUDE, MAX_LATITUDE, \
- CACHE_DIR, Coordinate, Bbox
+from kivy.metrics import dp
+from kivy.properties import (
+ AliasProperty,
+ BooleanProperty,
+ ListProperty,
+ NumericProperty,
+ ObjectProperty,
+ StringProperty,
+)
+from kivy.uix.behaviors import ButtonBehavior
+from kivy.uix.image import Image
+from kivy.uix.label import Label
+from kivy.uix.scatter import Scatter
+from kivy.uix.widget import Widget
+
+from kivy_garden.mapview import Bbox, Coordinate
+from kivy_garden.mapview.constants import (
+ CACHE_DIR,
+ MAX_LATITUDE,
+ MAX_LONGITUDE,
+ MIN_LATITUDE,
+ MIN_LONGITUDE,
+)
from kivy_garden.mapview.source import MapSource
from kivy_garden.mapview.utils import clamp
-from itertools import takewhile
-
-import webbrowser
-Builder.load_string("""
+Builder.load_string(
+ """
:
size_hint: None, None
source: root.source
@@ -81,7 +94,8 @@
center_x: root.center_x
size: root.popup_size
-""")
+"""
+)
class ClickableLabel(Label):
@@ -91,7 +105,7 @@ def on_ref_press(self, *args):
class Tile(Rectangle):
def __init__(self, *args, **kwargs):
- super(Tile, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
self.cache_dir = kwargs.get('cache_dir', CACHE_DIR)
@property
@@ -100,7 +114,8 @@ def cache_fn(self):
fn = map_source.cache_fmt.format(
image_ext=map_source.image_ext,
cache_key=map_source.cache_key,
- **self.__dict__)
+ **self.__dict__
+ )
return join(self.cache_dir, fn)
def set_source(self, cache_fn):
@@ -152,7 +167,7 @@ def add_widget(self, widget):
if not self.placeholder:
self.placeholder = widget
if self.is_open:
- super(MapMarkerPopup, self).add_widget(self.placeholder)
+ super().add_widget(self.placeholder)
else:
self.placeholder.add_widget(widget)
@@ -160,7 +175,7 @@ def remove_widget(self, widget):
if widget is not self.placeholder:
self.placeholder.remove_widget(widget)
else:
- super(MapMarkerPopup, self).remove_widget(widget)
+ super().remove_widget(widget)
def on_is_open(self, *args):
self.refresh_open_status()
@@ -170,15 +185,16 @@ def on_release(self, *args):
def refresh_open_status(self):
if not self.is_open and self.placeholder.parent:
- super(MapMarkerPopup, self).remove_widget(self.placeholder)
+ super().remove_widget(self.placeholder)
elif self.is_open and not self.placeholder.parent:
- super(MapMarkerPopup, self).add_widget(self.placeholder)
+ super().add_widget(self.placeholder)
class MapLayer(Widget):
"""A map layer, that is repositionned everytime the :class:`MapView` is
moved.
"""
+
viewport_x = NumericProperty(0)
viewport_y = NumericProperty(0)
@@ -197,22 +213,22 @@ def unload(self):
class MarkerMapLayer(MapLayer):
"""A map layer for :class:`MapMarker`
"""
+
order_marker_by_latitude = BooleanProperty(True)
def __init__(self, **kwargs):
self.markers = []
- super(MarkerMapLayer, self).__init__(**kwargs)
+ super().__init__(**kwargs)
def insert_marker(self, marker, **kwargs):
if self.order_marker_by_latitude:
- before = list(takewhile(
- lambda i_m: i_m[1].lat < marker.lat,
- enumerate(self.children)
- ))
+ before = list(
+ takewhile(lambda i_m: i_m[1].lat < marker.lat, enumerate(self.children))
+ )
if before:
kwargs['index'] = before[-1][0] + 1
- super(MarkerMapLayer, self).add_widget(marker, **kwargs)
+ super().add_widget(marker, **kwargs)
def add_widget(self, marker):
marker._layer = self
@@ -223,7 +239,7 @@ def remove_widget(self, marker):
marker._layer = None
if marker in self.markers:
self.markers.remove(marker)
- super(MarkerMapLayer, self).remove_widget(marker)
+ super().remove_widget(marker)
def reposition(self):
if not self.markers:
@@ -241,7 +257,7 @@ def reposition(self):
if not marker.parent:
self.insert_marker(marker)
else:
- super(MarkerMapLayer, self).remove_widget(marker)
+ super().remove_widget(marker)
def set_marker_position(self, mapview, marker):
x, y = mapview.get_window_xy_from(marker.lat, marker.lon, mapview.zoom)
@@ -256,11 +272,10 @@ def unload(self):
class MapViewScatter(Scatter):
# internal
def on_transform(self, *args):
- super(MapViewScatter, self).on_transform(*args)
+ super().on_transform(*args)
self.parent.on_transform(self.transform)
def collide_point(self, x, y):
- # print "collide_point", x, y
return True
@@ -309,11 +324,11 @@ class MapView(Widget):
delta_x = NumericProperty(0)
delta_y = NumericProperty(0)
- background_color = ListProperty([181 / 255., 208 / 255., 208 / 255., 1])
+ background_color = ListProperty([181 / 255.0, 208 / 255.0, 208 / 255.0, 1])
cache_dir = StringProperty(CACHE_DIR)
_zoom = NumericProperty(0)
_pause = BooleanProperty(False)
- _scale = 1.
+ _scale = 1.0
_disabled_count = 0
__events__ = ["on_map_relocated"]
@@ -337,8 +352,7 @@ def get_bbox(self, margin=0):
top/right (lat2, lon2).
"""
x1, y1 = self.to_local(0 - margin, 0 - margin)
- x2, y2 = self.to_local((self.width + margin),
- (self.height + margin))
+ x2, y2 = self.to_local((self.width + margin), (self.height + margin))
c1 = self.get_latlon_at(x1, y1)
c2 = self.get_latlon_at(x2, y2)
return Bbox((c1.lat, c1.lon, c2.lat, c2.lon))
@@ -395,23 +409,25 @@ def set_zoom_at(self, zoom, x, y, scale=None):
"""Sets the zoom level, leaving the (x, y) at the exact same point
in the view.
"""
- zoom = clamp(zoom,
- self.map_source.get_min_zoom(),
- self.map_source.get_max_zoom())
+ zoom = clamp(
+ zoom, self.map_source.get_min_zoom(), self.map_source.get_max_zoom()
+ )
if int(zoom) == int(self._zoom):
if scale is None:
return
elif scale == self.scale:
return
- scale = scale or 1.
+ scale = scale or 1.0
# first, rescale the scatter
scatter = self._scatter
scale = clamp(scale, scatter.scale_min, scatter.scale_max)
rescale = scale * 1.0 / scatter.scale
- scatter.apply_transform(Matrix().scale(rescale, rescale, rescale),
- post_multiply=True,
- anchor=scatter.to_local(x, y))
+ scatter.apply_transform(
+ Matrix().scale(rescale, rescale, rescale),
+ post_multiply=True,
+ anchor=scatter.to_local(x, y),
+ )
# adjust position if the zoom changed
c1 = self.map_source.get_col_count(self._zoom)
@@ -421,9 +437,9 @@ def set_zoom_at(self, zoom, x, y, scale=None):
self.delta_x = scatter.x + self.delta_x * f
self.delta_y = scatter.y + self.delta_y * f
# back to 0 every time
- scatter.apply_transform(Matrix().translate(
- -scatter.x, -scatter.y, 0
- ), post_multiply=True)
+ scatter.apply_transform(
+ Matrix().translate(-scatter.x, -scatter.y, 0), post_multiply=True
+ )
# avoid triggering zoom changes.
self._zoom = zoom
@@ -447,7 +463,8 @@ def get_latlon_at(self, x, y, zoom=None):
scale = self._scale
return Coordinate(
lat=self.map_source.get_lat(zoom, y / scale + vy),
- lon=self.map_source.get_lon(zoom, x / scale + vx))
+ lon=self.map_source.get_lon(zoom, x / scale + vx),
+ )
def add_marker(self, marker, layer=None):
"""Add a marker into the layer. If layer is None, it will be added in
@@ -477,9 +494,8 @@ def add_layer(self, layer, mode="window"):
widget yourself: think as Z-sprite / billboard.
Defaults to "window".
"""
- assert (mode in ("scatter", "window"))
- if self._default_marker_layer is None and \
- isinstance(layer, MarkerMapLayer):
+ assert mode in ("scatter", "window")
+ if self._default_marker_layer is None and isinstance(layer, MarkerMapLayer):
self._default_marker_layer = layer
self._layers.append(layer)
c = self.canvas
@@ -488,7 +504,7 @@ def add_layer(self, layer, mode="window"):
else:
self.canvas = self.canvas_layers_out
layer.canvas_parent = self.canvas
- super(MapView, self).add_widget(layer)
+ super().add_widget(layer)
self.canvas = c
def remove_layer(self, layer):
@@ -497,7 +513,7 @@ def remove_layer(self, layer):
c = self.canvas
self._layers.remove(layer)
self.canvas = layer.canvas_parent
- super(MapView, self).remove_widget(layer)
+ super().remove_widget(layer)
self.canvas = c
def sync_to(self, other):
@@ -511,6 +527,7 @@ def sync_to(self, other):
def __init__(self, **kwargs):
from kivy.base import EventLoop
+
EventLoop.ensure_window()
self._invalid_scale = True
self._tiles = []
@@ -530,13 +547,13 @@ def __init__(self, **kwargs):
with self.canvas:
self.canvas_layers_out = Canvas()
self._scale_target_anim = False
- self._scale_target = 1.
+ self._scale_target = 1.0
self._touch_count = 0
self.map_source.cache_dir = self.cache_dir
- Clock.schedule_interval(self._animate_color, 1 / 60.)
+ Clock.schedule_interval(self._animate_color, 1 / 60.0)
self.lat = kwargs.get("lat", self.lat)
self.lon = kwargs.get("lon", self.lon)
- super(MapView, self).__init__(**kwargs)
+ super().__init__(**kwargs)
def _animate_color(self, dt):
# fast path
@@ -544,14 +561,14 @@ def _animate_color(self, dt):
if d == 0:
for tile in self._tiles:
if tile.state == "need-animation":
- tile.g_color.a = 1.
+ tile.g_color.a = 1.0
tile.state = "animated"
for tile in self._tiles_bg:
if tile.state == "need-animation":
- tile.g_color.a = 1.
+ tile.g_color.a = 1.0
tile.state = "animated"
else:
- d = d / 1000.
+ d = d / 1000.0
for tile in self._tiles:
if tile.state != "need-animation":
continue
@@ -571,7 +588,7 @@ def add_widget(self, widget):
elif isinstance(widget, MapLayer):
self.add_layer(widget)
else:
- super(MapView, self).add_widget(widget)
+ super().add_widget(widget)
def remove_widget(self, widget):
if isinstance(widget, MapMarker):
@@ -579,13 +596,13 @@ def remove_widget(self, widget):
elif isinstance(widget, MapLayer):
self.remove_layer(widget)
else:
- super(MapView, self).remove_widget(widget)
+ super().remove_widget(widget)
def on_map_relocated(self, zoom, coord):
pass
def animated_diff_scale_at(self, d, x, y):
- self._scale_target_time = 1.
+ self._scale_target_time = 1.0
self._scale_target_pos = x, y
if self._scale_target_anim is False:
self._scale_target_anim = True
@@ -593,10 +610,10 @@ def animated_diff_scale_at(self, d, x, y):
else:
self._scale_target += d
Clock.unschedule(self._animate_scale)
- Clock.schedule_interval(self._animate_scale, 1 / 60.)
+ Clock.schedule_interval(self._animate_scale, 1 / 60.0)
def _animate_scale(self, dt):
- diff = self._scale_target / 3.
+ diff = self._scale_target / 3.0
if abs(diff) < 0.01:
diff = self._scale_target
self._scale_target = 0
@@ -618,17 +635,18 @@ def scale_at(self, scale, x, y):
scatter = self._scatter
scale = clamp(scale, scatter.scale_min, scatter.scale_max)
rescale = scale * 1.0 / scatter.scale
- scatter.apply_transform(Matrix().scale(rescale, rescale, rescale),
- post_multiply=True,
- anchor=scatter.to_local(x, y))
+ scatter.apply_transform(
+ Matrix().scale(rescale, rescale, rescale),
+ post_multiply=True,
+ anchor=scatter.to_local(x, y),
+ )
def on_touch_down(self, touch):
if not self.collide_point(*touch.pos):
return
if self.pause_on_action:
self._pause = True
- if "button" in touch.profile and touch.button in (
- "scrolldown", "scrollup"):
+ if "button" in touch.profile and touch.button in ("scrolldown", "scrollup"):
d = 1 if touch.button == "scrollup" else -1
self.animated_diff_scale_at(d, *touch.pos)
return True
@@ -639,7 +657,7 @@ def on_touch_down(self, touch):
self._touch_count += 1
if self._touch_count == 1:
self._touch_zoom = (self.zoom, self._scale)
- return super(MapView, self).on_touch_down(touch)
+ return super().on_touch_down(touch)
def on_touch_up(self, touch):
if touch.grab_current == self:
@@ -651,12 +669,12 @@ def on_touch_up(self, touch):
cur_zoom = self.zoom
cur_scale = self._scale
if cur_zoom < zoom or cur_scale < scale:
- self.animated_diff_scale_at(1. - cur_scale, *touch.pos)
+ self.animated_diff_scale_at(1.0 - cur_scale, *touch.pos)
elif cur_zoom > zoom or cur_scale > scale:
- self.animated_diff_scale_at(2. - cur_scale, *touch.pos)
+ self.animated_diff_scale_at(2.0 - cur_scale, *touch.pos)
self._pause = False
return True
- return super(MapView, self).on_touch_up(touch)
+ return super().on_touch_up(touch)
def on_transform(self, *args):
self._invalid_scale = True
@@ -668,19 +686,19 @@ def on_transform(self, *args):
zoom = self._zoom
scatter = self._scatter
scale = scatter.scale
- if scale >= 2.:
+ if scale >= 2.0:
zoom += 1
- scale /= 2.
+ scale /= 2.0
elif scale < 1:
zoom -= 1
- scale *= 2.
+ scale *= 2.0
zoom = clamp(zoom, map_source.min_zoom, map_source.max_zoom)
if zoom != self._zoom:
self.set_zoom_at(zoom, scatter.x, scatter.y, scale=scale)
self.trigger_update(True)
else:
- if zoom == map_source.min_zoom and scatter.scale < 1.:
- scatter.scale = 1.
+ if zoom == map_source.min_zoom and scatter.scale < 1.0:
+ scatter.scale = 1.0
self.trigger_update(True)
else:
self.trigger_update(False)
@@ -705,16 +723,16 @@ def _apply_bounds(self):
oxmin, oymin = self._scatter.to_local(self.x, self.y)
oxmax, oymax = self._scatter.to_local(self.right, self.top)
s = self._scale
- cxmin = (oxmin - dx)
+ cxmin = oxmin - dx
if cxmin < xmin:
self._scatter.x += (cxmin - xmin) * s
- cymin = (oymin - dy)
+ cymin = oymin - dy
if cymin < ymin:
self._scatter.y += (cymin - ymin) * s
- cxmax = (oxmax - dx)
+ cxmax = oxmax - dx
if cxmax > xmax:
self._scatter.x -= (xmax - cxmax) * s
- cymax = (oymax - dy)
+ cymax = oymax - dy
if cymax > ymax:
self._scatter.y -= (ymax - cymax) * s
@@ -730,12 +748,12 @@ def trigger_update(self, full):
def do_update(self, dt):
zoom = self._zoom
scale = self._scale
- self.lon = self.map_source.get_lon(zoom,
- (
- self.center_x - self._scatter.x) / scale - self.delta_x)
- self.lat = self.map_source.get_lat(zoom,
- (
- self.center_y - self._scatter.y) / scale - self.delta_y)
+ self.lon = self.map_source.get_lon(
+ zoom, (self.center_x - self._scatter.x) / scale - self.delta_x
+ )
+ self.lat = self.map_source.get_lat(
+ zoom, (self.center_y - self._scatter.y) / scale - self.delta_y
+ )
self.dispatch("on_map_relocated", zoom, Coordinate(self.lon, self.lat))
for layer in self._layers:
layer.reposition()
@@ -768,8 +786,7 @@ def bbox_for_zoom(self, vx, vy, w, h, zoom):
x_count = tile_x_last - tile_x_first
y_count = tile_y_last - tile_y_first
- return (tile_x_first, tile_y_first, tile_x_last, tile_y_last,
- x_count, y_count)
+ return (tile_x_first, tile_y_first, tile_x_last, tile_y_last, x_count, y_count)
def load_visible_tiles(self):
map_source = self.map_source
@@ -779,12 +796,14 @@ def load_visible_tiles(self):
bbox_for_zoom = self.bbox_for_zoom
size = map_source.dp_tile_size
- tile_x_first, tile_y_first, tile_x_last, tile_y_last, \
- x_count, y_count = bbox_for_zoom(vx, vy, self.width, self.height, zoom)
-
- # print "Range {},{} to {},{}".format(
- # tile_x_first, tile_y_first,
- # tile_x_last, tile_y_last)
+ (
+ tile_x_first,
+ tile_y_first,
+ tile_x_last,
+ tile_y_last,
+ x_count,
+ y_count,
+ ) = bbox_for_zoom(vx, vy, self.width, self.height, zoom)
# Adjust tiles behind us
for tile in self._tiles_bg[:]:
@@ -794,11 +813,21 @@ def load_visible_tiles(self):
f = 2 ** (zoom - tile.zoom)
w = self.width / f
h = self.height / f
- btile_x_first, btile_y_first, btile_x_last, btile_y_last, \
- _, _ = bbox_for_zoom(vx / f, vy / f, w, h, tile.zoom)
-
- if tile_x < btile_x_first or tile_x >= btile_x_last or \
- tile_y < btile_y_first or tile_y >= btile_y_last:
+ (
+ btile_x_first,
+ btile_y_first,
+ btile_x_last,
+ btile_y_last,
+ _,
+ _,
+ ) = bbox_for_zoom(vx / f, vy / f, w, h, tile.zoom)
+
+ if (
+ tile_x < btile_x_first
+ or tile_x >= btile_x_last
+ or tile_y < btile_y_first
+ or tile_y >= btile_y_last
+ ):
tile.state = "done"
self._tiles_bg.remove(tile)
self.canvas_map.before.remove(tile.g_color)
@@ -807,17 +836,19 @@ def load_visible_tiles(self):
tsize = size * f
tile.size = tsize, tsize
- tile.pos = (
- tile_x * tsize + self.delta_x,
- tile_y * tsize + self.delta_y)
+ tile.pos = (tile_x * tsize + self.delta_x, tile_y * tsize + self.delta_y)
# Get rid of old tiles first
for tile in self._tiles[:]:
tile_x = tile.tile_x
tile_y = tile.tile_y
- if tile_x < tile_x_first or tile_x >= tile_x_last or \
- tile_y < tile_y_first or tile_y >= tile_y_last:
+ if (
+ tile_x < tile_x_first
+ or tile_x >= tile_x_last
+ or tile_y < tile_y_first
+ or tile_y >= tile_y_last
+ ):
tile.state = "done"
self.tile_map_set(tile_x, tile_y, False)
self._tiles.remove(tile)
@@ -825,8 +856,7 @@ def load_visible_tiles(self):
self.canvas_map.remove(tile.g_color)
else:
tile.size = (size, size)
- tile.pos = (
- tile_x * size + self.delta_x, tile_y * size + self.delta_y)
+ tile.pos = (tile_x * size + self.delta_x, tile_y * size + self.delta_y)
# Load new tiles if needed
x = tile_x_first + x_count // 2 - 1
@@ -836,9 +866,13 @@ def load_visible_tiles(self):
turn = 0
while arm_size < arm_max:
for i in range(arm_size):
- if not self.tile_in_tile_map(x, y) and \
- y >= tile_y_first and y < tile_y_last and \
- x >= tile_x_first and x < tile_x_last:
+ if (
+ not self.tile_in_tile_map(x, y)
+ and y >= tile_y_first
+ and y < tile_y_last
+ and x >= tile_x_first
+ and x < tile_x_last
+ ):
self.load_tile(x, y, size, zoom)
x += dirs[turn % 4 + 1]
@@ -852,7 +886,7 @@ def load_visible_tiles(self):
def load_tile(self, x, y, size, zoom):
if self.tile_in_tile_map(x, y) or zoom != self._zoom:
return
- self.load_tile_for_source(self.map_source, 1., size, x, y, zoom)
+ self.load_tile_for_source(self.map_source, 1.0, size, x, y, zoom)
# XXX do overlay support
self.tile_map_set(x, y, True)
@@ -947,15 +981,19 @@ def on_map_source(self, instance, source):
self.map_source = MapSource.from_provider(source)
elif isinstance(source, (tuple, list)):
cache_key, min_zoom, max_zoom, url, attribution, options = source
- self.map_source = MapSource(url=url, cache_key=cache_key,
- min_zoom=min_zoom, max_zoom=max_zoom,
- attribution=attribution,
- cache_dir=self.cache_dir, **options)
+ self.map_source = MapSource(
+ url=url,
+ cache_key=cache_key,
+ min_zoom=min_zoom,
+ max_zoom=max_zoom,
+ attribution=attribution,
+ cache_dir=self.cache_dir,
+ **options
+ )
elif isinstance(source, MapSource):
self.map_source = source
else:
raise Exception("Invalid map source provider")
- self.zoom = clamp(self.zoom,
- self.map_source.min_zoom, self.map_source.max_zoom)
+ self.zoom = clamp(self.zoom, self.map_source.min_zoom, self.map_source.max_zoom)
self.remove_all_tiles()
self.trigger_update(True)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..9acad64
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[tool.black]
+line-length = 88
+skip-string-normalization = true
diff --git a/setup.cfg b/setup.cfg
index a343008..1bfde65 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,14 +1,18 @@
[flake8]
max-line-length = 88
extend-ignore =
- E122,
- E128,
- E203,
- E265,
F401,
- E402,
- E501,
- W504
+ E501
+
+[isort]
+multi_line_output=3
+include_trailing_comma=True
+force_grid_wrap=0
+use_parentheses=True
+line_length=88
[coverage:run]
relative_files = True
+
+[tool:pytest]
+addopts = --cov=kivy_garden.mapview --cov-report term --cov-branch
diff --git a/tests/maps-devrel-google.json b/tests/maps-devrel-google.json
new file mode 100644
index 0000000..be2f35f
--- /dev/null
+++ b/tests/maps-devrel-google.json
@@ -0,0 +1,146 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {
+ "letter": "G",
+ "color": "blue",
+ "rank": "7",
+ "ascii": "71"
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [123.61, -22.14], [122.38, -21.73], [121.06, -21.69], [119.66, -22.22], [119.00, -23.40],
+ [118.65, -24.76], [118.43, -26.07], [118.78, -27.56], [119.22, -28.57], [120.23, -29.49],
+ [121.77, -29.87], [123.57, -29.64], [124.45, -29.03], [124.71, -27.95], [124.80, -26.70],
+ [124.80, -25.60], [123.61, -25.64], [122.56, -25.64], [121.72, -25.72], [121.81, -26.62],
+ [121.86, -26.98], [122.60, -26.90], [123.57, -27.05], [123.57, -27.68], [123.35, -28.18],
+ [122.51, -28.38], [121.77, -28.26], [121.02, -27.91], [120.49, -27.21], [120.14, -26.50],
+ [120.10, -25.64], [120.27, -24.52], [120.67, -23.68], [121.72, -23.32], [122.43, -23.48],
+ [123.04, -24.04], [124.54, -24.28], [124.58, -23.20], [123.61, -22.14]
+ ]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {
+ "letter": "o",
+ "color": "red",
+ "rank": "15",
+ "ascii": "111"
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [128.84, -25.76], [128.18, -25.60], [127.96, -25.52], [127.88, -25.52], [127.70, -25.60],
+ [127.26, -25.79], [126.60, -26.11], [126.16, -26.78], [126.12, -27.68], [126.21, -28.42],
+ [126.69, -29.49], [127.74, -29.80], [128.80, -29.72], [129.41, -29.03], [129.72, -27.95],
+ [129.68, -27.21], [129.33, -26.23], [128.84, -25.76]
+ ],
+ [
+ [128.45, -27.44], [128.32, -26.94], [127.70, -26.82], [127.35, -27.05], [127.17, -27.80],
+ [127.57, -28.22], [128.10, -28.42], [128.49, -27.80], [128.45, -27.44]
+ ]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {
+ "letter": "o",
+ "color": "yellow",
+ "rank": "15",
+ "ascii": "111"
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [131.87, -25.76], [131.35, -26.07], [130.95, -26.78], [130.82, -27.64], [130.86, -28.53],
+ [131.26, -29.22], [131.92, -29.76], [132.45, -29.87], [133.06, -29.76], [133.72, -29.34],
+ [134.07, -28.80], [134.20, -27.91], [134.07, -27.21], [133.81, -26.31], [133.37, -25.83],
+ [132.71, -25.64], [131.87, -25.76]
+ ],
+ [
+ [133.15, -27.17], [132.71, -26.86], [132.09, -26.90], [131.74, -27.56], [131.79, -28.26],
+ [132.36, -28.45], [132.93, -28.34], [133.15, -27.76], [133.15, -27.17]
+ ]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {
+ "letter": "g",
+ "color": "blue",
+ "rank": "7",
+ "ascii": "103"
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [138.12, -25.04], [136.84, -25.16], [135.96, -25.36], [135.26, -25.99], [135, -26.90],
+ [135.04, -27.91], [135.26, -28.88], [136.05, -29.45], [137.02, -29.49], [137.81, -29.49],
+ [137.94, -29.99], [137.90, -31.20], [137.85, -32.24], [136.88, -32.69], [136.45, -32.36],
+ [136.27, -31.80], [134.95, -31.84], [135.17, -32.99], [135.52, -33.43], [136.14, -33.76],
+ [137.06, -33.83], [138.12, -33.65], [138.86, -33.21], [139.30, -32.28], [139.30, -31.24],
+ [139.30, -30.14], [139.21, -28.96], [139.17, -28.22], [139.08, -27.41], [139.08, -26.47],
+ [138.99, -25.40], [138.73, -25.00 ], [138.12, -25.04]
+ ],
+ [
+ [137.50, -26.54], [136.97, -26.47], [136.49, -26.58], [136.31, -27.13], [136.31, -27.72],
+ [136.58, -27.99], [137.50, -28.03], [137.68, -27.68], [137.59, -26.78], [137.50, -26.54]
+ ]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {
+ "letter": "l",
+ "color": "green",
+ "rank": "12",
+ "ascii": "108"
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [140.14,-21.04], [140.31,-29.42], [141.67,-29.49], [141.59,-20.92], [140.14,-21.04]
+ ]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {
+ "letter": "e",
+ "color": "red",
+ "rank": "5",
+ "ascii": "101"
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [144.14, -27.41], [145.67, -27.52], [146.86, -27.09], [146.82, -25.64], [146.25, -25.04],
+ [145.45, -24.68], [144.66, -24.60], [144.09, -24.76], [143.43, -25.08], [142.99, -25.40],
+ [142.64, -26.03], [142.64, -27.05], [142.64, -28.26], [143.30, -29.11], [144.18, -29.57],
+ [145.41, -29.64], [146.46, -29.19], [146.64, -28.72], [146.82, -28.14], [144.84, -28.42],
+ [144.31, -28.26], [144.14, -27.41]
+ ],
+ [
+ [144.18, -26.39], [144.53, -26.58], [145.19, -26.62], [145.72, -26.35], [145.81, -25.91],
+ [145.41, -25.68], [144.97, -25.68], [144.49, -25.64], [144, -25.99], [144.18, -26.39]
+ ]
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/test_downloader.py b/tests/test_downloader.py
new file mode 100644
index 0000000..9cb44a9
--- /dev/null
+++ b/tests/test_downloader.py
@@ -0,0 +1,59 @@
+from unittest import mock
+
+from kivy.clock import Clock
+
+from kivy_garden.mapview.constants import CACHE_DIR
+from kivy_garden.mapview.downloader import Downloader
+from tests.utils import patch_requests_get
+
+
+class TestDownloader:
+ def teardown_method(self):
+ Downloader._instance = None
+
+ def test_instance(self):
+ """Makes sure instance is a singleton."""
+ assert Downloader._instance is None
+ downloader = Downloader.instance()
+ assert downloader == Downloader._instance
+ assert type(downloader) == Downloader
+ assert downloader.cache_dir == CACHE_DIR
+ Downloader._instance = None
+ new_cache_dir = "new_cache_dir"
+ downloader = Downloader.instance(new_cache_dir)
+ assert downloader.cache_dir == new_cache_dir
+
+ def test_download(self):
+ """Checks download() callback."""
+ callback = mock.Mock()
+ url = "https://ifconfig.me/"
+ downloader = Downloader.instance()
+ assert len(downloader._futures) == 0
+ with patch_requests_get() as m_get:
+ downloader.download(url, callback)
+ assert m_get.call_args_list == [mock.call(url)]
+ assert callback.call_args_list == []
+ assert len(downloader._futures) == 1
+ Clock.tick()
+ assert callback.call_args_list == [mock.call(url, mock.ANY)]
+ assert len(downloader._futures) == 0
+
+ def test_download_status_error(self):
+ """
+ Error status code should be checked.
+ Callback function will not be invoked on error.
+ """
+ callback = mock.Mock()
+ url = "https://httpstat.us/404"
+ status_code = 404
+ downloader = Downloader.instance()
+ assert len(downloader._futures) == 0
+ with patch_requests_get(status_code=status_code) as m_get:
+ downloader.download(url, callback)
+ assert m_get.call_args_list == [mock.call(url)]
+ assert len(downloader._futures) == 1
+ assert callback.call_args_list == []
+ while len(downloader._futures) > 0:
+ Clock.tick()
+ assert callback.call_args_list == []
+ assert len(downloader._futures) == 0
diff --git a/tests/test_geojson.py b/tests/test_geojson.py
new file mode 100644
index 0000000..f4eabf7
--- /dev/null
+++ b/tests/test_geojson.py
@@ -0,0 +1,72 @@
+import json
+import os
+from unittest import mock
+
+from kivy.clock import Clock
+
+from kivy_garden.mapview import MapView
+from kivy_garden.mapview.geojson import GeoJsonMapLayer
+from tests.utils import patch_requests_get
+
+
+def load_json(filename):
+ test_dir = os.path.dirname(__file__)
+ json_file_path = os.path.join(test_dir, filename)
+ with open(json_file_path) as f:
+ json_file_content = f.read()
+ return json.loads(json_file_content)
+
+
+class TestGeoJsonMapLayer:
+ def test_init_simple(self):
+ """Makes sure we can initialize a simple GeoJsonMapLayer object."""
+ kwargs = {}
+ maplayer = GeoJsonMapLayer(**kwargs)
+ assert maplayer.source == ""
+ assert maplayer.geojson is None
+ assert maplayer.cache_dir == "cache"
+
+ def test_init_source(self):
+ """
+ Providing the source from http(s).
+ The json object should get downloaded using the requests library.
+ """
+ source = "https://storage.googleapis.com/maps-devrel/google.json"
+ kwargs = {"source": source}
+ response_json = {
+ "type": "FeatureCollection",
+ }
+ with patch_requests_get(response_json) as m_get:
+ maplayer = GeoJsonMapLayer(**kwargs)
+ assert maplayer.source == source
+ assert maplayer.geojson is None
+ while maplayer.geojson is None:
+ Clock.tick()
+ assert maplayer.geojson == response_json
+ assert m_get.call_args_list == [mock.call(source)]
+
+ def test_init_geojson(self):
+ """Providing the geojson directly, polygons should get added."""
+ options = {}
+ mapview = MapView(**options)
+ geojson = {}
+ kwargs = {"geojson": geojson}
+ maplayer = GeoJsonMapLayer(**kwargs)
+ mapview.add_layer(maplayer)
+ Clock.tick()
+ assert maplayer.source == ""
+ assert maplayer.geojson == geojson
+ assert len(maplayer.canvas_line.children) == 0
+ assert len(maplayer.canvas_polygon.children) == 3
+ assert len(maplayer.g_canvas_polygon.children) == 0
+ geojson = load_json("maps-devrel-google.json")
+ kwargs = {"geojson": geojson}
+ maplayer = GeoJsonMapLayer(**kwargs)
+ mapview = MapView(**options)
+ mapview.add_layer(maplayer)
+ Clock.tick()
+ assert maplayer.source == ""
+ assert maplayer.geojson == geojson
+ assert len(maplayer.canvas_line.children) == 0
+ assert len(maplayer.canvas_polygon.children) == 3
+ assert len(maplayer.g_canvas_polygon.children) == 132
diff --git a/tests/test_mapview.py b/tests/test_mapview.py
index 4023003..4020a12 100644
--- a/tests/test_mapview.py
+++ b/tests/test_mapview.py
@@ -1,18 +1,9 @@
-import unittest
from kivy_garden.mapview import MapView
-class TextInputTest(unittest.TestCase):
-
+class TestMapView:
def test_init_simple_map(self):
- """
- Makes sure we can initialize a simple MapView object.
- """
+ """Makes sure we can initialize a simple MapView object."""
kwargs = {}
mapview = MapView(**kwargs)
- self.assertEqual(len(mapview.children), 2)
-
-
-if __name__ == '__main__':
- import unittest
- unittest.main()
+ assert len(mapview.children) == 2
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 0000000..8e43c44
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,11 @@
+from unittest import mock
+
+from requests.models import Response
+
+
+def patch_requests_get(response_json=None, status_code=200):
+ response = Response()
+ response.json = lambda: response_json
+ response.status_code = status_code
+ m_get = mock.Mock(return_value=response)
+ return mock.patch("requests.get", m_get)
diff --git a/tox.ini b/tox.ini
index 6a0775d..a846486 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,4 +10,4 @@ commands =
[testenv:pep8]
deps = flake8
-commands = flake8 kivy_garden/ examples/
+commands = flake8 tests/ kivy_garden/ examples/