Add Order.draw()
and Path.draw()
- `Order.draw()` plots a `Courier`'s path from the `Order.pickup_address` to the `Order.delivery_address` - `Path.draw()` plots a `Courier`'s path between any two `Address` objects
This commit is contained in:
parent
2449492aba
commit
d83ff2e273
4 changed files with 112 additions and 1 deletions
|
@ -59,6 +59,7 @@ class Config:
|
|||
# Colors for the visualizations ins `folium`.
|
||||
RESTAURANT_COLOR = 'red'
|
||||
CUSTOMER_COLOR = 'blue'
|
||||
NEUTRAL_COLOR = 'black'
|
||||
|
||||
# Implementation-specific settings
|
||||
# --------------------------------
|
||||
|
|
|
@ -11,6 +11,7 @@ from sqlalchemy import orm
|
|||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.ext import hybrid
|
||||
|
||||
from urban_meal_delivery import config
|
||||
from urban_meal_delivery.db import meta
|
||||
from urban_meal_delivery.db import utils
|
||||
|
||||
|
@ -151,7 +152,7 @@ class Address(meta.Base):
|
|||
`.city.map` for convenience in interactive usage
|
||||
"""
|
||||
defaults = {
|
||||
'color': 'black',
|
||||
'color': f'{config.NEUTRAL_COLOR}',
|
||||
'popup': f'{self.street}, {self.zip_code} {self.city_name}',
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
|
|
@ -7,6 +7,7 @@ import itertools
|
|||
import json
|
||||
from typing import List
|
||||
|
||||
import folium
|
||||
import googlemaps as gm
|
||||
import ordered_set
|
||||
import sqlalchemy as sa
|
||||
|
@ -227,6 +228,11 @@ class Path(meta.Base):
|
|||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
@property # pragma: no cover
|
||||
def map(self) -> folium.Map: # noqa:WPS125
|
||||
"""Convenience property to obtain the underlying `City.map`."""
|
||||
return self.first_address.city.map
|
||||
|
||||
@functools.cached_property
|
||||
def waypoints(self) -> List[utils.Location]:
|
||||
"""The couriers' route from `.first_address` to `.second_address`.
|
||||
|
@ -241,3 +247,70 @@ class Path(meta.Base):
|
|||
point.relate_to(self.first_address.city.southwest)
|
||||
|
||||
return points
|
||||
|
||||
def draw( # noqa:WPS211
|
||||
self,
|
||||
*,
|
||||
reverse: bool = False,
|
||||
start_tooltip: str = 'Start',
|
||||
end_tooltip: str = 'End',
|
||||
start_color: str = 'green',
|
||||
end_color: str = 'red',
|
||||
path_color: str = 'black',
|
||||
) -> folium.Map: # pragma: no cover
|
||||
"""Draw the `.waypoints` from `.first_address` to `.second_address`.
|
||||
|
||||
Args:
|
||||
reverse: by default, `.first_address` is used as the start;
|
||||
set to `False` to make `.second_address` the start
|
||||
start_tooltip: text shown on marker at the path's start
|
||||
end_tooltip: text shown on marker at the path's end
|
||||
start_color: `folium` color for the path's start
|
||||
end_color: `folium` color for the path's end
|
||||
path_color: `folium` color along the path, which
|
||||
is the line between the `.waypoints`
|
||||
|
||||
Returns:
|
||||
`.map` for convenience in interactive usage
|
||||
"""
|
||||
# Without `self._directions` synced from Google Maps,
|
||||
# the `.waypoints` are not available.
|
||||
self.sync_with_google_maps()
|
||||
|
||||
# First, plot the couriers' path between the start and
|
||||
# end locations, so that it is below the `folium.Circle`s.
|
||||
line = folium.PolyLine(
|
||||
locations=(
|
||||
self.first_address.location.lat_lng,
|
||||
*(point.lat_lng for point in self.waypoints),
|
||||
self.second_address.location.lat_lng,
|
||||
),
|
||||
color=path_color,
|
||||
weight=2,
|
||||
)
|
||||
line.add_to(self.map)
|
||||
|
||||
# Draw the path's start and end locations, possibly reversed,
|
||||
# on top of the couriers' path.
|
||||
|
||||
if reverse:
|
||||
start, end = self.second_address, self.first_address
|
||||
else:
|
||||
start, end = self.first_address, self.second_address
|
||||
|
||||
start.draw(
|
||||
radius=5,
|
||||
color=start_color,
|
||||
fill_color=start_color,
|
||||
fill_opacity=1,
|
||||
tooltip=start_tooltip,
|
||||
)
|
||||
end.draw(
|
||||
radius=5,
|
||||
color=end_color,
|
||||
fill_color=end_color,
|
||||
fill_opacity=1,
|
||||
tooltip=end_tooltip,
|
||||
)
|
||||
|
||||
return self.map
|
||||
|
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
import datetime
|
||||
|
||||
import folium
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from urban_meal_delivery import config
|
||||
from urban_meal_delivery import db
|
||||
from urban_meal_delivery.db import meta
|
||||
|
||||
|
||||
|
@ -524,3 +527,36 @@ class Order(meta.Base): # noqa:WPS214
|
|||
return '<{cls}(#{order_id})>'.format(
|
||||
cls=self.__class__.__name__, order_id=self.id,
|
||||
)
|
||||
|
||||
def draw(self) -> folium.Map: # pragma: no cover
|
||||
"""Draw the `.waypoints` from `.pickup_address` to `.delivery_address`.
|
||||
|
||||
Important: Do not put this in an automated script as a method call
|
||||
triggers an API call to the Google Maps API and may result in costs.
|
||||
|
||||
Returns:
|
||||
`...city.map` for convenience in interactive usage
|
||||
"""
|
||||
path = db.Path.from_order(self)
|
||||
|
||||
restaurant_tooltip = f'{self.restaurant.name} (#{self.restaurant.id})'
|
||||
customer_tooltip = f'Customer #{self.customer.id}'
|
||||
|
||||
# Because the underlying distance matrix is symmetric (i.e., a DB constraint),
|
||||
# we must check if the `.pickup_address` is the couriers' `Path`'s start.
|
||||
if path.first_address is self.pickup_address:
|
||||
reverse = False
|
||||
start_tooltip, end_tooltip = restaurant_tooltip, customer_tooltip
|
||||
else:
|
||||
reverse = True
|
||||
start_tooltip, end_tooltip = customer_tooltip, restaurant_tooltip
|
||||
|
||||
# This triggers `Path.sync_with_google_maps()` behind the scenes.
|
||||
return path.draw(
|
||||
reverse=reverse,
|
||||
start_tooltip=start_tooltip,
|
||||
end_tooltip=end_tooltip,
|
||||
start_color=config.RESTAURANT_COLOR,
|
||||
end_color=config.CUSTOMER_COLOR,
|
||||
path_color=config.NEUTRAL_COLOR,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue