Open kilbee opened 8 years ago
I implemented this here. https://github.com/kivy-garden/garden.mapview/compare/master...frmdstryr:master . Works with zooming, moving, etc...
@frmdstryr I've came up with similar implementation, but what I'd really like is mode="scatter" on my layer where I draw lines, because recalculating too many points on every MapView transform takes too long. And while for simple translation "scatter" works ok, it doesn't work for zooming. Unless I'm doing something wrong... here's example code I'm using: line from Dover (England) to Calais (France).
Yeah I have the same problem on mobile... tried cythonizing and still is slow. Will see if I can figure something out...
I discovered that commenting out this line https://github.com/kivy-garden/garden.mapview/blob/master/mapview/view.py#L647 fixes the scaling issue. However doing this prevents the map from loading the new tiles when zooming. Still looking for an actual fix...
Got it working with a scatter now. The map resets the scatter transform the zoom level changes, thus the scatter layer must be redrawn in this case. All other movements and scaling within the same zoom level will be taken care of by the scatter.
from kivy.graphics.context_instructions import Translate, Scale
class LineMapLayer(MapLayer):
def __init__(self, **kwargs):
super(LineMapLayer, self).__init__(**kwargs)
self.zoom = 0
def reposition(self):
mapview = self.parent
#: Must redraw when the zoom changes
#: as the scatter transform resets for the new tiles
if (self.zoom != mapview.zoom):
self.draw_line()
def draw_line(self, *args):
mapview = self.parent
self.zoom = mapview.zoom
geo_dover = [51.126251, 1.327067]
geo_calais = [50.959086, 1.827652]
screen_dover = mapview.get_window_xy_from(geo_dover[0], geo_dover[1], mapview.zoom)
screen_calais = mapview.get_window_xy_from(geo_calais[0], geo_calais[1], mapview.zoom)
# When zooming we must undo the current scatter transform
# or the animation makes the line misplaced
scatter = mapview._scatter
x,y,s = scatter.x, scatter.y, scatter.scale
point_list = [ screen_dover[0], screen_dover[1],
screen_calais[0], screen_calais[1] ]
with self.canvas:
self.canvas.clear()
Scale(1/s,1/s,1)
Translate(-x,-y)
Color(0, 0, 0, .6)
Line(points=point_list, width=3, joint="bevel")
Looks great, very needed addition. I will test it when i pick up my project with mapview. One more thought: do you think it would be faster if instead get_window_xy_from (coordinates..) we could translate geopoints based on transform on zoom level change? I actually had working scatter too, but without resetting it (I scaled line width instead - very dirty solution), it was super fast, but adding new points on zoom/transformation other than default was way over my head (I'd need to reverse track all transformations etc. - it kinda worked, but only on zooming in, on zooming out it was buggy and I couldn't narrow it down - probably applied transformations in wrong order or something and since it was very dirty anyway I gave up on it).
Actually I didn't even think about it but yes you should be able to just apply the same transformations that get_window_xy_from does. Will try it.
I was able to get about a 2x speedup by doing this, however it is breaking the line width drawing algorithm.
import os
import random
from math import *
from mapview.utils import clamp
import time
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
from kivy.clock import Clock
from kivy.graphics import Color, Line
from kivy.graphics.transformation import Matrix
from kivy.graphics.context_instructions import Translate, Scale
from mapview import MapView, MapLayer, MIN_LONGITUDE, MIN_LATITUDE, MAX_LATITUDE, MAX_LONGITUDE
class MapViewApp(App):
mapview = None
def __init__(self, **kwargs):
super(MapViewApp, self).__init__(**kwargs)
Clock.schedule_once(self.post, 0)
def build(self):
layout = BoxLayout(orientation='vertical')
return layout
def post(self, *args):
layout = FloatLayout()
self.mapview = MapView(zoom=9, lat=51.046284, lon=1.541179)
line = LineMapLayer()
self.mapview.add_layer(line, mode="scatter") # window scatter
layout.add_widget(self.mapview)
self.root.add_widget(layout)
b = BoxLayout(orientation='horizontal',height='32dp',size_hint_y=None)
b.add_widget(Button(text="Zoom in",on_press=lambda a: setattr(self.mapview,'zoom',self.mapview.zoom+1)))
b.add_widget(Button(text="Zoom out",on_press=lambda a: setattr(self.mapview,'zoom',self.mapview.zoom-1)))
b.add_widget(Button(text="AddPoint",on_press=lambda a: line.add_point()))
self.root.add_widget(b)
class LineMapLayer(MapLayer):
def __init__(self, **kwargs):
super(LineMapLayer, self).__init__(**kwargs)
self.zoom = 0
geo_dover = [51.126251, 1.327067]
geo_calais = [50.959086, 1.827652]
# NOTE: Points must be valid as they're no longer clamped
self.coordinates = [geo_dover, geo_calais]
for i in range(25000-2):
self.coordinates.append(self.gen_point())
def reposition(self):
mapview = self.parent
#: Must redraw when the zoom changes
#: as the scatter transform resets for the new tiles
if (self.zoom != mapview.zoom):
self.draw_line()
def gen_point(self):
n = len(self.coordinates)
dx,dy = random.randint(-100,100)/10000.0,random.randint(0,100)/10000.0
c = (self.coordinates[-1][0]+dx,
self.coordinates[-1][1]+dy)
return c
def add_point(self):
#: Add a random point close to the previous one
for i in range(len(self.coordinates)):
self.coordinates.append(self.gen_point())
self.draw_line()
def get_x(self, lon):
"""Get the x position on the map using this map source's projection
(0, 0) is located at the top left.
"""
return clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE)
def get_y(self, 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))
def draw_line(self, *args):
mapview = self.parent
self.zoom = mapview.zoom
# When zooming we must undo the current scatter transform
# or the animation distorts it
scatter = mapview._scatter
map_source = mapview.map_source
sx,sy,ss = scatter.x, scatter.y, scatter.scale
vx,vy,vs = mapview.viewport_pos[0], mapview.viewport_pos[1], mapview.scale
# Account for map source tile size and mapview zoom
ms = pow(2.0,mapview.zoom) * map_source.dp_tile_size
#: Since lat is not a linear transform we must compute manually
line_points = []
for lat,lon in self.coordinates:
line_points.extend((self.get_x(lon),self.get_y(lat)))
#line_points.extend(mapview.get_window_xy_from(lat,lon,mapview.zoom))
with self.canvas:
# Clear old line
self.canvas.clear()
# Undo the scatter animation transform
Scale(1/ss,1/ss,1)
Translate(-sx,-sy)
# Apply the get window xy from transforms
Scale(vs,vs,1)
Translate(-vx,-vy)
# Apply the what we can factor out
# of the mapsource long,lat to x,y conversion
Scale(ms/360.0,ms/2.0,1)
Translate(180,0)
# Draw new
Color(0, 0, 0, .6)
Line(points=line_points, width=1)#4/ms)#, joint="round",joint_precision=100)
MapViewApp().run()
Edit: This doesn't eliminate redrawing on zoom
I'm also interested in this, since I'm implementing in-dash navigation for my car using Kivy. I'm planning on using the Google Maps Directions API to get directions, and display them on the map as vector lines and markers.
Is there anything I can do to help move this feature along aside from testing what's been pasted above?
It seems that Kivy's Line()
graphics instruction doesn't do line width very well; it only expands in the y
direction, not in x
.
(the darker line is with width=1
, the lighter one is width=0.00002, joint='round', close=False, cap='none'
)
I was able to get the line width working much better by multiplying the result of get_y()
by 180, and changing the last Scale()
call to:
Scale(ms/360.0, ms/360.0, 1)
I was able to get things working faster by precalculating line_points
whenever coordinates
changes, instead of recalculating it every time we draw.
Also, line width works better scaling so we do ms/2.0, ms/2.0
instead of ms/360.0, ms/360.0
import random
from math import *
from kivy.graphics import Color, Line, SmoothLine
from kivy.graphics.context_instructions import Translate, Scale
from kivy.garden.mapview.mapview.utils import clamp
from kivy.garden.mapview.mapview import MapLayer, MIN_LONGITUDE, MIN_LATITUDE, MAX_LATITUDE, MAX_LONGITUDE
class LineMapLayer(MapLayer):
def __init__(self, **kwargs):
super(LineMapLayer, self).__init__(**kwargs)
self._coordinates = []
self.zoom = 0
geo_dover = [51.126251, 1.327067]
geo_calais = [50.959086, 1.827652]
# NOTE: Points must be valid as they're no longer clamped
coordinates = [geo_dover, geo_calais]
for i in range(500 - 2):
coordinates.append(self.gen_point(coordinates[-1]))
self.coordinates = coordinates
@property
def coordinates(self):
return self._coordinates
@coordinates.setter
def coordinates(self, coordinates):
self._coordinates = coordinates
#: Since lat is not a linear transform we must compute manually
self.line_points = [(self.get_x(lon), self.get_y(lat)) for lat, lon in coordinates]
#self.line_points = [mapview.get_window_xy_from(lat, lon, mapview.zoom) for lat, lon in coordinates]
def reposition(self):
mapview = self.parent
#: Must redraw when the zoom changes
#: as the scatter transform resets for the new tiles
if (self.zoom != mapview.zoom):
self.draw_line()
def gen_point(self, lastPoint):
dx, dy = random.randint(-100, 100) / 20000.0, random.randint(0, 100) / 20000.0
c = (lastPoint[0] + dx, lastPoint[1] + dy)
return c
def add_point(self):
#: Add a random point close to the previous one
coordinates = self.coordinates
for i in range(len(self.coordinates)):
coordinates.append(self.gen_point(coordinates[-1]))
self.coordinates = coordinates
self.draw_line()
def get_x(self, lon):
'''Get the x position on the map using this map source's projection
(0, 0) is located at the top left.
'''
return clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) / 180.
def get_y(self, 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))
def draw_line(self, *args):
mapview = self.parent
self.zoom = mapview.zoom
# When zooming we must undo the current scatter transform
# or the animation distorts it
scatter = mapview._scatter
map_source = mapview.map_source
sx, sy, ss = scatter.x, scatter.y, scatter.scale
vx, vy, vs = mapview.viewport_pos[0], mapview.viewport_pos[1], mapview.scale
# Account for map source tile size and mapview zoom
ms = pow(2.0, mapview.zoom) * map_source.dp_tile_size
with self.canvas:
# Clear old line
self.canvas.clear()
# Undo the scatter animation transform
Scale(1 / ss, 1 / ss, 1)
Translate(-sx, -sy)
# Apply the get window xy from transforms
Scale(vs, vs, 1)
Translate(-vx, -vy)
# Apply the what we can factor out
# of the mapsource long, lat to x, y conversion
Scale(ms / 2.0, ms / 2.0, 1)
Translate(1, 0)
# Draw new
Color(0, 0.2, 0.7, 0.25)
Line(points=self.line_points, width=6.5 / ms)
Color(0, 0.2, 0.7, 1)
Line(points=self.line_points, width=6 / ms)
Color(0, 0.3, 1, 1)
Line(points=self.line_points, width=4 / ms)
#Line(points=self.line_points, width=1)#4 / ms)#, joint='round', joint_precision=100)
#Line(points=self.line_points, width=4 / ms, joint='round', close=False, cap='none')
#Line(points=self.line_points, width=1)
I'm running into some precision issues with the current code:
~Changing the scale of the map back to Scale(ms/360.0, ms/360.0, 1)
instead of Scale(ms / 2.0, ms / 2.0, 1)
helps a bit with the glitches when zoomed in, but they still appear. (it seems like we'll need an even bigger scale in order to fix those)~ It seems that the imprecision issues show up regardless of what scale I use for the numbers here, which led me to believe it's due to including ms
in the Scale()
call; however, I experimented with pre-scaling line_points
each time the zoom changes, but it's still not giving me good results. I'm not sure why the imprecision issue constantly shows up at the highest zooms.
Not sure what to do about the offset... still trying to diagnose that.
The offset seems to be because the MapView
is not positioned at (0, 0)
in the window; I have it in a PageLayout
. So, the drawing routine for the lines should apparently take the MapView
's position into account when rendering.
This is working much better:
from math import *
from kivy.graphics import Color, Line, SmoothLine, MatrixInstruction
from kivy.graphics.context_instructions import Translate, Scale
from kivy.clock import Clock
from kivy.garden.mapview.mapview.utils import clamp
from kivy.garden.mapview.mapview import MapLayer, MIN_LONGITUDE, MIN_LATITUDE, MAX_LATITUDE, MAX_LONGITUDE
class LineMapLayer(MapLayer):
def __init__(self, **kwargs):
super(LineMapLayer, self).__init__(**kwargs)
self._coordinates = []
self._line_points = None
self._line_points_offset = (0, 0)
self.zoom = 0
@property
def coordinates(self):
return self._coordinates
@coordinates.setter
def coordinates(self, coordinates):
self._coordinates = coordinates
self.invalidate_line_points()
self.clear_and_redraw()
@property
def line_points(self):
if self._line_points is None:
self.calc_line_points()
return self._line_points
@property
def line_points_offset(self):
if self._line_points is None:
self.calc_line_points()
return self._line_points_offset
def calc_line_points(self):
# Offset all points by the coordinates of the first point, to keep coordinates closer to zero.
# (and therefore avoid some float precision issues when drawing lines)
self._line_points_offset = (self.get_x(self.coordinates[0][1]), self.get_y(self.coordinates[0][0]))
# Since lat is not a linear transform we must compute manually
self._line_points = [(self.get_x(lon) - self._line_points_offset[0], self.get_y(lat) - self._line_points_offset[1]) for lat, lon in self.coordinates]
def invalidate_line_points(self):
self._line_points = None
self._line_points_offset = (0, 0)
def get_x(self, lon):
'''Get the x position on the map using this map source's projection
(0, 0) is located at the top left.
'''
return clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) * self.ms / 360.0
def get_y(self, lat):
'''Get the y position on the map using this map source's projection
(0, 0) is located at the top left.
'''
lat = radians(clamp(-lat, MIN_LATITUDE, MAX_LATITUDE))
return ((1.0 - log(tan(lat) + 1.0 / cos(lat)) / pi)) * self.ms / 2.0
def reposition(self):
mapview = self.parent
# Must redraw when the zoom changes
# as the scatter transform resets for the new tiles
if (self.zoom != mapview.zoom):
map_source = mapview.map_source
self.ms = pow(2.0, mapview.zoom) * map_source.dp_tile_size
self.invalidate_line_points()
self.clear_and_redraw()
def clear_and_redraw(self, *args):
with self.canvas:
# Clear old line
self.canvas.clear()
# FIXME: Why is 0.05 a good value here? Why does 0 leave us with weird offsets?
Clock.schedule_once(self._draw_line, 0.05)
def _draw_line(self, *args):
mapview = self.parent
self.zoom = mapview.zoom
# When zooming we must undo the current scatter transform
# or the animation distorts it
scatter = mapview._scatter
sx, sy, ss = scatter.x, scatter.y, scatter.scale
# Account for map source tile size and mapview zoom
vx, vy, vs = mapview.viewport_pos[0], mapview.viewport_pos[1], mapview.scale
with self.canvas:
# Clear old line
self.canvas.clear()
# Offset by the MapView's position in the window
Translate(*mapview.pos)
# Undo the scatter animation transform
Scale(1 / ss, 1 / ss, 1)
Translate(-sx, -sy)
# Apply the get window xy from transforms
Scale(vs, vs, 1)
Translate(-vx, -vy)
# Apply the what we can factor out of the mapsource long, lat to x, y conversion
Translate(self.ms / 2, 0)
# Translate by the offset of the line points (this keeps the points closer to the origin)
Translate(*self.line_points_offset)
# Draw line
Color(0, 0.2, 0.7, 0.25)
Line(points=self.line_points, width=6.5 / 2)
Color(0, 0.2, 0.7, 1)
Line(points=self.line_points, width=6 / 2)
Color(0, 0.3, 1, 1)
Line(points=self.line_points, width=4 / 2)
This makes several changes from the last version:
line.coordinates
to any array of coordinates you want.Translate()
while drawing. This prevents float imprecision issues while rendering. (provided your line isn't too large; it might be better to offset relative to the center of the viewport, but that would require re-calculating line_points
every frame, or having some sort of distance threshold which triggers a re-calculation)mapview.pos
to translate the drawing, so it actually lines up with the map most of the time.This performs rather well (although there's some visible lag when zooming in or out before it redraws, but I think that's unavoidable as long as there's a Scatter animation present) and it looks equally good zoomed in or out.
There's only one big problem here:
MapView
handles its Scatter
.sx, sy, ss = scatter.x - mapview.x, scatter.y - mapview.y, scatter.scale
instead of
Translate(*mapview.pos)
also works
This is working much better:
from math import * from kivy.graphics import Color, Line, SmoothLine, MatrixInstruction from kivy.graphics.context_instructions import Translate, Scale from kivy.clock import Clock from kivy.garden.mapview.mapview.utils import clamp from kivy.garden.mapview.mapview import MapLayer, MIN_LONGITUDE, MIN_LATITUDE, MAX_LATITUDE, MAX_LONGITUDE class LineMapLayer(MapLayer): def __init__(self, **kwargs): super(LineMapLayer, self).__init__(**kwargs) self._coordinates = [] self._line_points = None self._line_points_offset = (0, 0) self.zoom = 0 @property def coordinates(self): return self._coordinates @coordinates.setter def coordinates(self, coordinates): self._coordinates = coordinates self.invalidate_line_points() self.clear_and_redraw() @property def line_points(self): if self._line_points is None: self.calc_line_points() return self._line_points @property def line_points_offset(self): if self._line_points is None: self.calc_line_points() return self._line_points_offset def calc_line_points(self): # Offset all points by the coordinates of the first point, to keep coordinates closer to zero. # (and therefore avoid some float precision issues when drawing lines) self._line_points_offset = (self.get_x(self.coordinates[0][1]), self.get_y(self.coordinates[0][0])) # Since lat is not a linear transform we must compute manually self._line_points = [(self.get_x(lon) - self._line_points_offset[0], self.get_y(lat) - self._line_points_offset[1]) for lat, lon in self.coordinates] def invalidate_line_points(self): self._line_points = None self._line_points_offset = (0, 0) def get_x(self, lon): '''Get the x position on the map using this map source's projection (0, 0) is located at the top left. ''' return clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) * self.ms / 360.0 def get_y(self, lat): '''Get the y position on the map using this map source's projection (0, 0) is located at the top left. ''' lat = radians(clamp(-lat, MIN_LATITUDE, MAX_LATITUDE)) return ((1.0 - log(tan(lat) + 1.0 / cos(lat)) / pi)) * self.ms / 2.0 def reposition(self): mapview = self.parent # Must redraw when the zoom changes # as the scatter transform resets for the new tiles if (self.zoom != mapview.zoom): map_source = mapview.map_source self.ms = pow(2.0, mapview.zoom) * map_source.dp_tile_size self.invalidate_line_points() self.clear_and_redraw() def clear_and_redraw(self, *args): with self.canvas: # Clear old line self.canvas.clear() # FIXME: Why is 0.05 a good value here? Why does 0 leave us with weird offsets? Clock.schedule_once(self._draw_line, 0.05) def _draw_line(self, *args): mapview = self.parent self.zoom = mapview.zoom # When zooming we must undo the current scatter transform # or the animation distorts it scatter = mapview._scatter sx, sy, ss = scatter.x, scatter.y, scatter.scale # Account for map source tile size and mapview zoom vx, vy, vs = mapview.viewport_pos[0], mapview.viewport_pos[1], mapview.scale with self.canvas: # Clear old line self.canvas.clear() # Offset by the MapView's position in the window Translate(*mapview.pos) # Undo the scatter animation transform Scale(1 / ss, 1 / ss, 1) Translate(-sx, -sy) # Apply the get window xy from transforms Scale(vs, vs, 1) Translate(-vx, -vy) # Apply the what we can factor out of the mapsource long, lat to x, y conversion Translate(self.ms / 2, 0) # Translate by the offset of the line points (this keeps the points closer to the origin) Translate(*self.line_points_offset) # Draw line Color(0, 0.2, 0.7, 0.25) Line(points=self.line_points, width=6.5 / 2) Color(0, 0.2, 0.7, 1) Line(points=self.line_points, width=6 / 2) Color(0, 0.3, 1, 1) Line(points=self.line_points, width=4 / 2)
This makes several changes from the last version:
- I removed the test points, since now you can set
line.coordinates
to any array of coordinates you want.- All coordinates are offset to be relative to the first coordinate, and the first coordinate is used in a
Translate()
while drawing. This prevents float imprecision issues while rendering. (provided your line isn't too large; it might be better to offset relative to the center of the viewport, but that would require re-calculatingline_points
every frame, or having some sort of distance threshold which triggers a re-calculation)- We use
mapview.pos
to translate the drawing, so it actually lines up with the map most of the time.- We delay drawing by 0.05 seconds. Apparently this gives the other transforms a chance to settle so the line doesn't end up being weirdly offset. (still don't understand the mechanics here, though)
This performs rather well (although there's some visible lag when zooming in or out before it redraws, but I think that's unavoidable as long as there's a Scatter animation present) and it looks equally good zoomed in or out.
There's only one big problem here:
- After zooming too far in or too far out, the line becomes permanently offset to the east. I'm not sure, but I think it might have something to do with how
MapView
handles itsScatter
.
Can you please attach MapViewApp Class that run the map?
Great work! It helped me to draw a route on MapView
. The only part where I had to make a change is that a redraw is also wanted when the position of MapView
changes, not only when the zoom changes.
from math import *
from kivy.graphics import Color, Line
from kivy.graphics.context_instructions import Translate, Scale
from kivy.clock import Clock
from kivy.garden.mapview.mapview.utils import clamp
from kivy.garden.mapview.mapview import MapLayer, MIN_LONGITUDE, MIN_LATITUDE, MAX_LATITUDE, MAX_LONGITUDE
class LineMapLayer(MapLayer):
def __init__(self, **kwargs):
super(LineMapLayer, self).__init__(**kwargs)
self._coordinates = list()
self._line_points = None
self._line_points_offset = (0, 0)
self.zoom = 0
self.lon = 0
self.lat = 0
self.ms = 0
@property
def coordinates(self):
return self._coordinates
@coordinates.setter
def coordinates(self, coordinates):
self._coordinates = coordinates
self.invalidate_line_points()
self.clear_and_redraw()
@property
def line_points(self):
if self._line_points is None:
self.calc_line_points()
return self._line_points
@property
def line_points_offset(self):
if self._line_points is None:
self.calc_line_points()
return self._line_points_offset
def calc_line_points(self):
# Offset all points by the coordinates of the first point, to keep coordinates closer to zero.
# (and therefore avoid some float precision issues when drawing lines)
self._line_points_offset = (self.get_x(self.coordinates[0][1]), self.get_y(self.coordinates[0][0]))
# Since lat is not a linear transform we must compute manually
self._line_points = [(self.get_x(lon) - self._line_points_offset[0], self.get_y(lat) -
self._line_points_offset[1]) for lat, lon in self.coordinates]
def invalidate_line_points(self):
self._line_points = None
self._line_points_offset = (0, 0)
def get_x(self, lon):
"""Get the x position on the map using this map source's projection
(0, 0) is located at the top left.
"""
return clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) * self.ms / 360.0
def get_y(self, lat):
"""Get the y position on the map using this map source's projection
(0, 0) is located at the top left.
"""
lat = radians(clamp(-lat, MIN_LATITUDE, MAX_LATITUDE))
return (1.0 - log(tan(lat) + 1.0 / cos(lat)) / pi) * self.ms / 2.0
def reposition(self):
map_view = self.parent
# Must redraw when the zoom changes
# as the scatter transform resets for the new tiles
if self.zoom != map_view.zoom or \
self.lon != round(map_view.lon, 7) or \
self.lat != round(map_view.lat, 7):
map_source = map_view.map_source
self.ms = pow(2.0, map_view.zoom) * map_source.dp_tile_size
self.invalidate_line_points()
self.clear_and_redraw()
def clear_and_redraw(self, *args):
with self.canvas:
# Clear old line
self.canvas.clear()
# FIXME: Why is 0.05 a good value here? Why does 0 leave us with weird offsets?
Clock.schedule_once(self._draw_line, 0.05)
def _draw_line(self, *args):
map_view = self.parent
self.zoom = map_view.zoom
self.lon = map_view.lon
self.lat = map_view.lat
# When zooming we must undo the current scatter transform
# or the animation distorts it
scatter = map_view._scatter
sx, sy, ss = scatter.x, scatter.y, scatter.scale
# Account for map source tile size and map view zoom
vx, vy, vs = map_view.viewport_pos[0], map_view.viewport_pos[1], map_view.scale
with self.canvas:
# Clear old line
self.canvas.clear()
# Offset by the MapView's position in the window
Translate(*map_view.pos)
# Undo the scatter animation transform
Scale(1 / ss, 1 / ss, 1)
Translate(-sx, -sy)
# Apply the get window xy from transforms
Scale(vs, vs, 1)
Translate(-vx, -vy)
# Apply the what we can factor out of the mapsource long, lat to x, y conversion
Translate(self.ms / 2, 0)
# Translate by the offset of the line points (this keeps the points closer to the origin)
Translate(*self.line_points_offset)
Color(0, 0.3, 1, 1)
Line(points=self.line_points, width=2)
Sounds like someone should make a PR out of that code :) Nice collaboration here!
Hello. I have a problem. I'm working on a project and would like to use class LineMapLayer in it. However, the drawing of the track does not work correctly for me. When you shift the mapview and zoom in, the track is drawn in the wrong place. But when you zoom out or resize the program window, the track is drawn perfectly. I use Kivy 2.0.0, Kivymd - master branch, mapview-1.0.5. Tell me where my error is. Program Listing:
import sys
from kivy.base import runTouchApp from kivy.lang import Builder from linemaplayer import *
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( """
MapView: lat: 51.543 lon: 46.059 zoom: 12 map_source: MapSource(sys.argv[1], attribution="") if len(sys.argv) > 1 else "osm"
LineMapLayer:
_coordinates: [[51.51203, 45.95524], [51.512066, 45.955235], [51.512066, 45.955235], [51.512066, 45.955235], [51.512066, 45.955235], [51.512066, 45.955235], [51.512066, 45.955235], [51.51207, 45.95524], [51.512066, 45.955235], [51.51208, 45.95522], [51.51208, 45.9552], [51.512085, 45.9552], [51.512085, 45.955196], [51.51209, 45.955196], [51.51209, 45.955196], [51.512085, 45.955196], [51.51209, 45.955196], [51.51209, 45.955193], [51.51209, 45.955193], [51.51209, 45.955193], [51.51209, 45.955193], [51.51209, 45.955193], [51.51209, 45.95519], [51.512173, 45.95524], [51.51217, 45.955242], [51.51214, 45.955265], [51.51211, 45.955288], [51.512096, 45.955307], [51.512043, 45.955364], [51.512024, 45.95538], [51.51187, 45.95544], [51.51182, 45.955437], [51.511784, 45.955444], [51.5117, 45.95546], [51.51172, 45.955437], [51.51172, 45.955437], [51.51172, 45.955437], [51.51172, 45.955437], [51.51172, 45.955437], [51.51175, 45.95542], [51.51174, 45.955425], [51.51174, 45.955425], [51.51174, 45.955425], [51.51174, 45.955425], [51.511745, 45.955425], [51.51175, 45.955425], [51.51175, 45.955425], [51.511745, 45.955425], [51.511745, 45.955425], [51.51175, 45.955425], [51.511753, 45.955425], [51.511753, 45.955425], [51.51175, 45.955425], [51.51175, 45.955425], [51.51175, 45.955425], [51.51174, 45.95543], [51.51174, 45.95543], [51.511726, 45.95545], [51.511696, 45.955494], [51.511684, 45.9555], [51.511658, 45.95552], [51.511642, 45.955532], [51.511597, 45.955536], [51.511585, 45.955544], [51.511566, 45.95558], [51.511505, 45.955566], [51.5115, 45.955532], [51.511555, 45.955585], [51.511574, 45.95561], [51.511467, 45.955616], [51.511574, 45.955616], [51.511524, 45.955566], [51.511677, 45.955517], [51.51164, 45.95561], [51.511593, 45.95557], [51.51158, 45.955574], [51.511585, 45.95567], [51.51164, 45.955658], [51.51159, 45.955624], [51.511566, 45.955624], [51.511562, 45.95559], [51.51149, 45.955627], [51.511444, 45.95569], [51.511517, 45.95567], [51.511497, 45.95567], [51.51153, 45.955704], [51.511597, 45.95569], [51.511513, 45.95571], [51.511505, 45.955692], [51.511482, 45.955757], [51.51154, 45.955692], [51.511505, 45.955612], [51.51148, 45.955593], [51.511536, 45.955563], [51.511646, 45.95563], [51.5117, 45.955723], [51.511547, 45.955658], [51.511566, 45.95558], [51.51142, 45.955593], [51.51153, 45.95558], [51.511543, 45.95556], [51.511486, 45.955597], [51.511597, 45.955563], [51.51159, 45.955574], [51.511578, 45.955666], [51.511562, 45.955654], [51.511635, 45.95567], [51.511623, 45.95561], [51.511604, 45.955593], [51.511543, 45.95562], [51.511456, 45.95571], [51.51143, 45.955723], [51.511234, 45.955776], [51.511196, 45.955757], [51.511173, 45.955753], [51.511093, 45.955616], [51.510975, 45.955235], [51.51095, 45.954994], [51.51093, 45.954926], [51.510906, 45.95479], [51.510895, 45.954784], [51.510883, 45.954773], [51.510876, 45.95475], [51.51092, 45.954685], [51.51095, 45.95465], [51.510933, 45.954597], [51.510902, 45.9546], [51.51087, 45.9546], [51.510815, 45.95462], [51.510326, 45.954826], [51.5103, 45.954838], [51.51029, 45.95487], [51.51003, 45.95508], [51.50994, 45.95515], [51.50946, 45.955414], [51.509445, 45.95542], [51.50942, 45.955395], [51.509422, 45.9554], [51.509277, 45.95543], [51.509014, 45.954926], [51.508488, 45.953297], [51.507915, 45.951374], [51.507874, 45.95123], [51.507866, 45.951233], [51.507736, 45.95083], [51.507317, 45.949284], [51.507065, 45.948357], [51.507053, 45.948273], [51.50705, 45.948193], [51.507057, 45.948124], [51.507076, 45.948063], [51.50711, 45.94802], [51.507156, 45.948], [51.5072, 45.947998], [51.507244, 45.948013], [51.50736, 45.94806], [51.507793, 45.94836], [51.507847, 45.948414], [51.507893, 45.948463], [51.50794, 45.948517], [51.507954, 45.948574], [51.507954, 45.94863], [51.507935, 45.94868], [51.507877, 45.948765], [51.507725, 45.94887], [51.507526, 45.948982], [51.50625, 45.9499], [51.50601, 45.950085], [51.50592, 45.950138], [51.505825, 45.95027], [51.5058, 45.950302], [51.505756, 45.950314], [51.50571, 45.9503], [51.505554, 45.95021], [51.505505, 45.950207], [51.505444, 45.95022], [51.5054, 45.950237], [51.505306, 45.950268], [51.505287, 45.95027], [51.505257, 45.95031], [51.50522, 45.950363], [51.505203, 45.95043], [51.505177, 45.950607], [51.50518, 45.950817], [51.505188, 45.952324], [51.505177, 45.9532], [51.505096, 45.953903], [51.50502, 45.954056], [51.50466, 45.955162], [51.50445, 45.95554], [51.504356, 45.955643], [51.504173, 45.95584], [51.5038, 45.95617], [51.50289, 45.956696], [51.502804, 45.956776], [51.502773, 45.956783], [51.502747, 45.95679], [51.502743, 45.956814], [51.50273, 45.95679], [51.50268, 45.956696], [51.502514, 45.956917], [51.502476, 45.95695]]
""" )
runTouchApp(root)
is it possible to use google maps as a provider for kivy garden mapview? i used the google maps directions api to get the distance, time, and a bunch of other information including polylines points but i'm stuck. i don't know the possibility of plotting the polyline points on my kivygarden mapview
Hello. I have a problem. I'm working on a project and would like to use class LineMapLayer in it. However, the drawing of the track does not work correctly for me. When you shift the mapview and zoom in, the track is drawn in the wrong place. But when you zoom out or resize the program window, the track is drawn perfectly. I use Kivy 2.0.0, Kivymd - master branch, mapview-1.0.5. Tell me where my error is. Program Listing:
import sys
from kivy.base import runTouchApp from kivy.lang import Builder from linemaplayer import *
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( """
:import sys sys
:import MapSource kivy_garden.mapview.MapSource
MapView: lat: 51.543 lon: 46.059 zoom: 12 map_source: MapSource(sys.argv[1], attribution="") if len(sys.argv) > 1 else "osm"
LineMapLayer: _coordinates: [[51.51203, 45.95524], [51.512066, 45.955235], [51.512066, 45.955235], [51.512066, 45.955235], [51.512066, 45.955235], [51.512066, 45.955235], [51.512066, 45.955235], [51.51207, 45.95524], [51.512066, 45.955235], [51.51208, 45.95522], [51.51208, 45.9552], [51.512085, 45.9552], [51.512085, 45.955196], [51.51209, 45.955196], [51.51209, 45.955196], [51.512085, 45.955196], [51.51209, 45.955196], [51.51209, 45.955193], [51.51209, 45.955193], [51.51209, 45.955193], [51.51209, 45.955193], [51.51209, 45.955193], [51.51209, 45.95519], [51.512173, 45.95524], [51.51217, 45.955242], [51.51214, 45.955265], [51.51211, 45.955288], [51.512096, 45.955307], [51.512043, 45.955364], [51.512024, 45.95538], [51.51187, 45.95544], [51.51182, 45.955437], [51.511784, 45.955444], [51.5117, 45.95546], [51.51172, 45.955437], [51.51172, 45.955437], [51.51172, 45.955437], [51.51172, 45.955437], [51.51172, 45.955437], [51.51175, 45.95542], [51.51174, 45.955425], [51.51174, 45.955425], [51.51174, 45.955425], [51.51174, 45.955425], [51.511745, 45.955425], [51.51175, 45.955425], [51.51175, 45.955425], [51.511745, 45.955425], [51.511745, 45.955425], [51.51175, 45.955425], [51.511753, 45.955425], [51.511753, 45.955425], [51.51175, 45.955425], [51.51175, 45.955425], [51.51175, 45.955425], [51.51174, 45.95543], [51.51174, 45.95543], [51.511726, 45.95545], [51.511696, 45.955494], [51.511684, 45.9555], [51.511658, 45.95552], [51.511642, 45.955532], [51.511597, 45.955536], [51.511585, 45.955544], [51.511566, 45.95558], [51.511505, 45.955566], [51.5115, 45.955532], [51.511555, 45.955585], [51.511574, 45.95561], [51.511467, 45.955616], [51.511574, 45.955616], [51.511524, 45.955566], [51.511677, 45.955517], [51.51164, 45.95561], [51.511593, 45.95557], [51.51158, 45.955574], [51.511585, 45.95567], [51.51164, 45.955658], [51.51159, 45.955624], [51.511566, 45.955624], [51.511562, 45.95559], [51.51149, 45.955627], [51.511444, 45.95569], [51.511517, 45.95567], [51.511497, 45.95567], [51.51153, 45.955704], [51.511597, 45.95569], [51.511513, 45.95571], [51.511505, 45.955692], [51.511482, 45.955757], [51.51154, 45.955692], [51.511505, 45.955612], [51.51148, 45.955593], [51.511536, 45.955563], [51.511646, 45.95563], [51.5117, 45.955723], [51.511547, 45.955658], [51.511566, 45.95558], [51.51142, 45.955593], [51.51153, 45.95558], [51.511543, 45.95556], [51.511486, 45.955597], [51.511597, 45.955563], [51.51159, 45.955574], [51.511578, 45.955666], [51.511562, 45.955654], [51.511635, 45.95567], [51.511623, 45.95561], [51.511604, 45.955593], [51.511543, 45.95562], [51.511456, 45.95571], [51.51143, 45.955723], [51.511234, 45.955776], [51.511196, 45.955757], [51.511173, 45.955753], [51.511093, 45.955616], [51.510975, 45.955235], [51.51095, 45.954994], [51.51093, 45.954926], [51.510906, 45.95479], [51.510895, 45.954784], [51.510883, 45.954773], [51.510876, 45.95475], [51.51092, 45.954685], [51.51095, 45.95465], [51.510933, 45.954597], [51.510902, 45.9546], [51.51087, 45.9546], [51.510815, 45.95462], [51.510326, 45.954826], [51.5103, 45.954838], [51.51029, 45.95487], [51.51003, 45.95508], [51.50994, 45.95515], [51.50946, 45.955414], [51.509445, 45.95542], [51.50942, 45.955395], [51.509422, 45.9554], [51.509277, 45.95543], [51.509014, 45.954926], [51.508488, 45.953297], [51.507915, 45.951374], [51.507874, 45.95123], [51.507866, 45.951233], [51.507736, 45.95083], [51.507317, 45.949284], [51.507065, 45.948357], [51.507053, 45.948273], [51.50705, 45.948193], [51.507057, 45.948124], [51.507076, 45.948063], [51.50711, 45.94802], [51.507156, 45.948], [51.5072, 45.947998], [51.507244, 45.948013], [51.50736, 45.94806], [51.507793, 45.94836], [51.507847, 45.948414], [51.507893, 45.948463], [51.50794, 45.948517], [51.507954, 45.948574], [51.507954, 45.94863], [51.507935, 45.94868], [51.507877, 45.948765], [51.507725, 45.94887], [51.507526, 45.948982], [51.50625, 45.9499], [51.50601, 45.950085], [51.50592, 45.950138], [51.505825, 45.95027], [51.5058, 45.950302], [51.505756, 45.950314], [51.50571, 45.9503], [51.505554, 45.95021], [51.505505, 45.950207], [51.505444, 45.95022], [51.5054, 45.950237], [51.505306, 45.950268], [51.505287, 45.95027], [51.505257, 45.95031], [51.50522, 45.950363], [51.505203, 45.95043], [51.505177, 45.950607], [51.50518, 45.950817], [51.505188, 45.952324], [51.505177, 45.9532], [51.505096, 45.953903], [51.50502, 45.954056], [51.50466, 45.955162], [51.50445, 45.95554], [51.504356, 45.955643], [51.504173, 45.95584], [51.5038, 45.95617], [51.50289, 45.956696], [51.502804, 45.956776], [51.502773, 45.956783], [51.502747, 45.95679], [51.502743, 45.956814], [51.50273, 45.95679], [51.50268, 45.956696], [51.502514, 45.956917], [51.502476, 45.95695]]
""" )
runTouchApp(root)
I found a solution to this problem. Everything turned out to be quite simple. When adding a maplayer, you must specify mode = "scatter". add_layer (mylayer, mode = "scatter")
Has anyone managed to use this to draw multiple lines on the map? I have tried calling multiple instances of the LineMapLayer to no avail
Now calling multiple instances of the LineMapLayer works!
main.py:
from kivymd.app import MDApp
from kivy.uix.screenmanager import Screen
from kivy_garden.mapview import MapLayer, MapMarker
from kivy.graphics import Color, Line
from kivy.graphics.context_instructions import Translate, Scale, PushMatrix, PopMatrix
from kivy_garden.mapview.utils import clamp
from kivy_garden.mapview.constants import \
(MIN_LONGITUDE, MAX_LONGITUDE, MIN_LATITUDE, MAX_LATITUDE)
from math import radians, log, tan, cos, pi
import random
class LineMapLayer(MapLayer):
def __init__(self, coordinates=[[0, 0], [0, 0]], color=[0, 0, 1, 1], **kwargs):
super().__init__(**kwargs)
self._coordinates = coordinates
self.color = color
self._line_points = None
self._line_points_offset = (0, 0)
self.zoom = 0
self.lon = 0
self.lat = 0
self.ms = 0
@property
def coordinates(self):
return self._coordinates
@coordinates.setter
def coordinates(self, coordinates):
self._coordinates = coordinates
self.invalidate_line_points()
self.clear_and_redraw()
@property
def line_points(self):
if self._line_points is None:
self.calc_line_points()
return self._line_points
@property
def line_points_offset(self):
if self._line_points is None:
self.calc_line_points()
return self._line_points_offset
def calc_line_points(self):
# Offset all points by the coordinates of the first point,
# to keep coordinates closer to zero.
# (and therefore avoid some float precision issues when drawing lines)
self._line_points_offset = (self.get_x(self.coordinates[0][1]),
self.get_y(self.coordinates[0][0]))
# Since lat is not a linear transform we must compute manually
self._line_points = [(self.get_x(lon) - self._line_points_offset[0],
self.get_y(lat) - self._line_points_offset[1])
for lat, lon in self.coordinates]
def invalidate_line_points(self):
self._line_points = None
self._line_points_offset = (0, 0)
def get_x(self, lon):
"""Get the x position on the map using this map source's projection
(0, 0) is located at the top left.
"""
return clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) * self.ms / 360.0
def get_y(self, lat):
"""Get the y position on the map using this map source's projection
(0, 0) is located at the top left.
"""
lat = radians(clamp(-lat, MIN_LATITUDE, MAX_LATITUDE))
return (1.0 - log(tan(lat) + 1.0 / cos(lat)) / pi) * self.ms / 2.0
# Function called when the MapView is moved
def reposition(self):
map_view = self.parent
# Must redraw when the zoom changes
# as the scatter transform resets for the new tiles
if self.zoom != map_view.zoom or \
self.lon != round(map_view.lon, 7) or \
self.lat != round(map_view.lat, 7):
map_source = map_view.map_source
self.ms = pow(2.0, map_view.zoom) * map_source.dp_tile_size
self.invalidate_line_points()
self.clear_and_redraw()
def clear_and_redraw(self, *args):
with self.canvas:
# Clear old line
self.canvas.clear()
self._draw_line()
def _draw_line(self, *args):
map_view = self.parent
self.zoom = map_view.zoom
self.lon = map_view.lon
self.lat = map_view.lat
# When zooming we must undo the current scatter transform
# or the animation distorts it
scatter = map_view._scatter
sx, sy, ss = scatter.x, scatter.y, scatter.scale
# Account for map source tile size and map view zoom
vx, vy, vs = map_view.viewport_pos[0], map_view.viewport_pos[1], map_view.scale
with self.canvas:
# Save the current coordinate space context
PushMatrix()
# Offset by the MapView's position in the window (always 0,0 ?)
Translate(*map_view.pos)
# Undo the scatter animation transform
Scale(1 / ss, 1 / ss, 1)
Translate(-sx, -sy)
# Apply the get window xy from transforms
Scale(vs, vs, 1)
Translate(-vx, -vy)
# Apply what we can factor out of the mapsource long, lat to x, y conversion
Translate(self.ms / 2, 0)
# Translate by the offset of the line points
# (this keeps the points closer to the origin)
Translate(*self.line_points_offset)
Color(*self.color)
Line(points=self.line_points, width=2)
# Retrieve the last saved coordinate space context
PopMatrix()
class MapLayout(Screen):
pass
class MapViewApp(MDApp):
def on_start(self):
mapview = self.root.mapview
mapview.lat = 51.046284
mapview.lon = 1.541179
mapview.zoom = 7 # zoom values: 0 - 19
# You can import JSON data here or:
my_coordinates = [[51.505807, -0.128513], [51.126251, 1.327067],
[50.959086, 1.827652], [48.85519, 2.35021]]
# Add routes
lml1 = LineMapLayer(coordinates=my_coordinates, color=[1, 0, 0, 1])
mapview.add_layer(lml1, mode="scatter")
my_coordinates = [my_coordinates[-1]]
for i in range(4600):
my_coordinates.append(gen_rand_point(my_coordinates[-1]))
lml2 = LineMapLayer(coordinates=my_coordinates, color=[0.5, 0, 1, 1])
mapview.add_layer(lml2, mode="scatter")
my_coordinates = [[51.505807, -0.128513], [48.85519, 2.35021]]
lml3 = LineMapLayer(coordinates=my_coordinates, color=[0, 0, 1, 1])
mapview.add_layer(lml3, mode="scatter")
# Add markers
marker = MapMarker(lat=51.126251, lon=1.327067, source='images/marker.png')
mapview.add_marker(marker)
marker = MapMarker(lat=50.959086, lon=1.827652, source='images/marker.png')
mapview.add_marker(marker)
def build(self):
return MapLayout()
def gen_rand_point(last_coordinate):
dx, dy = random.randint(-100, 100) / 10000.0, random.randint(0, 100) / 10000.0
c = (last_coordinate[0] + dx,
last_coordinate[1] + dy)
return c
if __name__ == '__main__':
MapViewApp().run()
mapview.kv:
<MapLayout>:
mapview: _mapview
MapView:
id: _mapview
MapMarker: # London
source: 'images/marker.png' # png image 32x32
lat: 51.505807
lon: -0.128513
MapMarker: # Paris
source: 'images/marker.png' # png image 32x32
lat: 48.85519
lon: 2.35021
MDFillRoundFlatButton:
text: 'Home'
pos_hint: {'right': 0.95, 'y': 0.05}
on_release:
root.mapview.center_on(51.505807, -0.128513) # London
Note: Do not forget to add 'images/marker.png' to your project.
The result of the above code:
Hi, I'm Elias. I'm Brazilian and I'm sorry for my bad English. I'm developing an application in kivy, about bus routes. I wanted help in adding bus routes to my map. my code is like this: if you can help me i would appreciate it
from kivy.base import runTouchApp from kivy.lang import Builder
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("""
Toolbar@BoxLayout: size_hint_y: None height: '48dp' padding: '4dp' spacing: '4dp'
canvas:
Color:
rgba: .5, .2, .2, .6
Rectangle:
pos: self.pos
size: self.size
ShadedLabel@Label: size: self.texture_size canvas.before: Color: rgba: .2, .2, .2, .6 Rectangle: pos: self.pos size: self.size
RelativeLayout:
MapView:
id: mapview
lat: -3.065254715651993
lon: -60.017043914794925
zoom: 15
#size_hint: .5, .5
#pos_hint: {"x": .25, "y": .25}
#on_map_relocated: mapview2.sync_to(self)
#on_map_relocated: mapview3.sync_to(self)
Toolbar:
top: root.top
Button:
text: "ir para o centro"
on_release: mapview.center_on(-3.127648227145917, -60.0188892759717)
Button:
text: "ir para cidade nova"
on_release: mapview.center_on(-3.0295139313693595, -59.9921953048975)
Spinner:
text: "onibus"
values: '842','850'
on_text:
Toolbar:
Label:
text: "Longitude: {}".format(mapview.lon)
Label:
text: "Latitude: {}".format(mapview.lat)
""")
runTouchApp(root)
Thanks for your email,
I am currently on annual leave until Monday 3rd October 2022. This inbox will not be monitored in my absence; for urgent communications, please contact @.***
Kind regards, Harrison
hi, if you have time, you can see my code and help me. please.
Hi! I'd like to draw .gpx routes on mapview, is there generic class for drawing lines (simple line between two geocoordinates) or how would I go about it?
Actually worked out temporary solution with canvas lines drawn from MapMarkers, however I think marker positioning is off when map is zoomed.
Checked with examples from this repo and it seems marks are actually off when map is zoomed (kivy 1.9.2 dev)