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`.
|
# Colors for the visualizations ins `folium`.
|
||||||
RESTAURANT_COLOR = 'red'
|
RESTAURANT_COLOR = 'red'
|
||||||
CUSTOMER_COLOR = 'blue'
|
CUSTOMER_COLOR = 'blue'
|
||||||
|
NEUTRAL_COLOR = 'black'
|
||||||
|
|
||||||
# Implementation-specific settings
|
# Implementation-specific settings
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
|
|
|
@ -11,6 +11,7 @@ from sqlalchemy import orm
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
from sqlalchemy.ext import hybrid
|
from sqlalchemy.ext import hybrid
|
||||||
|
|
||||||
|
from urban_meal_delivery import config
|
||||||
from urban_meal_delivery.db import meta
|
from urban_meal_delivery.db import meta
|
||||||
from urban_meal_delivery.db import utils
|
from urban_meal_delivery.db import utils
|
||||||
|
|
||||||
|
@ -151,7 +152,7 @@ class Address(meta.Base):
|
||||||
`.city.map` for convenience in interactive usage
|
`.city.map` for convenience in interactive usage
|
||||||
"""
|
"""
|
||||||
defaults = {
|
defaults = {
|
||||||
'color': 'black',
|
'color': f'{config.NEUTRAL_COLOR}',
|
||||||
'popup': f'{self.street}, {self.zip_code} {self.city_name}',
|
'popup': f'{self.street}, {self.zip_code} {self.city_name}',
|
||||||
}
|
}
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import itertools
|
||||||
import json
|
import json
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
import folium
|
||||||
import googlemaps as gm
|
import googlemaps as gm
|
||||||
import ordered_set
|
import ordered_set
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
@ -227,6 +228,11 @@ class Path(meta.Base):
|
||||||
db.session.add(self)
|
db.session.add(self)
|
||||||
db.session.commit()
|
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
|
@functools.cached_property
|
||||||
def waypoints(self) -> List[utils.Location]:
|
def waypoints(self) -> List[utils.Location]:
|
||||||
"""The couriers' route from `.first_address` to `.second_address`.
|
"""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)
|
point.relate_to(self.first_address.city.southwest)
|
||||||
|
|
||||||
return points
|
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 datetime
|
||||||
|
|
||||||
|
import folium
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
from urban_meal_delivery import config
|
||||||
|
from urban_meal_delivery import db
|
||||||
from urban_meal_delivery.db import meta
|
from urban_meal_delivery.db import meta
|
||||||
|
|
||||||
|
|
||||||
|
@ -524,3 +527,36 @@ class Order(meta.Base): # noqa:WPS214
|
||||||
return '<{cls}(#{order_id})>'.format(
|
return '<{cls}(#{order_id})>'.format(
|
||||||
cls=self.__class__.__name__, order_id=self.id,
|
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