Add functionality for drawing folium.Maps
- this code is not unit-tested due to the complexity involving interactive `folium.Map`s => visual checks give high confidence
This commit is contained in:
parent
605ade4078
commit
4b6d92958d
12 changed files with 714 additions and 13 deletions
|
|
@ -1,9 +1,14 @@
|
|||
"""Provide the ORM's `City` model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
from urban_meal_delivery.db import utils
|
||||
|
||||
|
|
@ -94,3 +99,152 @@ class City(meta.Base):
|
|||
The city borders refer to the Google Maps viewport.
|
||||
"""
|
||||
return self.northeast.northing - self.southwest.northing
|
||||
|
||||
def clear_map(self) -> City: # pragma: no cover
|
||||
"""Create a new `folium.Map` object aligned with the city's viewport.
|
||||
|
||||
The map is available via the `.map` property. Note that it is a
|
||||
mutable objects that is changed from various locations in the code base.
|
||||
|
||||
Returns:
|
||||
self: enabling method chaining
|
||||
""" # noqa:DAR203
|
||||
self._map = folium.Map(
|
||||
location=[self.center_latitude, self.center_longitude],
|
||||
zoom_start=self.initial_zoom,
|
||||
)
|
||||
return self
|
||||
|
||||
@property # pragma: no cover
|
||||
def map(self) -> folium.Map: # noqa:WPS125
|
||||
"""A `folium.Map` object aligned with the city's viewport.
|
||||
|
||||
See docstring for `.clear_map()` for further info.
|
||||
"""
|
||||
if not hasattr(self, '_map'): # noqa:WPS421 note:d334120e
|
||||
self.clear_map()
|
||||
|
||||
return self._map
|
||||
|
||||
def draw_restaurants( # noqa:WPS231
|
||||
self, order_counts: bool = False, # pragma: no cover
|
||||
) -> folium.Map:
|
||||
"""Draw all restaurants on the`.map`.
|
||||
|
||||
Args:
|
||||
order_counts: show the number of orders
|
||||
|
||||
Returns:
|
||||
`.map` for convenience in interactive usage
|
||||
"""
|
||||
# Obtain all primary `Address`es in the city that host `Restaurant`s.
|
||||
addresses = ( # noqa:ECE001
|
||||
db.session.query(db.Address)
|
||||
.filter(
|
||||
db.Address.id.in_(
|
||||
db.session.query(db.Address.primary_id) # noqa:WPS221
|
||||
.join(db.Restaurant, db.Address.id == db.Restaurant.address_id)
|
||||
.filter(db.Address.city == self)
|
||||
.distinct()
|
||||
.all(),
|
||||
),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
for address in addresses:
|
||||
# Show the restaurant's name if there is only one.
|
||||
# Otherwise, list all the restaurants' ID's.
|
||||
restaurants = ( # noqa:ECE001
|
||||
db.session.query(db.Restaurant)
|
||||
.join(db.Address, db.Restaurant.address_id == db.Address.id)
|
||||
.filter(db.Address.primary_id == address.id)
|
||||
.all()
|
||||
)
|
||||
if len(restaurants) == 1:
|
||||
tooltip = f'{restaurants[0].name} (#{restaurants[0].id})' # noqa:WPS221
|
||||
else:
|
||||
tooltip = 'Restaurants ' + ', '.join( # noqa:WPS336
|
||||
f'#{restaurant.id}' for restaurant in restaurants
|
||||
)
|
||||
|
||||
if order_counts:
|
||||
# Calculate the number of orders for ALL restaurants ...
|
||||
n_orders = ( # noqa:ECE001
|
||||
db.session.query(db.Order.id)
|
||||
.join(db.Address, db.Order.pickup_address_id == db.Address.id)
|
||||
.filter(db.Address.primary_id == address.id)
|
||||
.count()
|
||||
)
|
||||
# ... and adjust the size of the red dot on the `.map`.
|
||||
if n_orders >= 1000:
|
||||
radius = 20 # noqa:WPS220
|
||||
elif n_orders >= 500:
|
||||
radius = 15 # noqa:WPS220
|
||||
elif n_orders >= 100:
|
||||
radius = 10 # noqa:WPS220
|
||||
elif n_orders >= 10:
|
||||
radius = 5 # noqa:WPS220
|
||||
else:
|
||||
radius = 1 # noqa:WPS220
|
||||
|
||||
tooltip += f' | n_orders={n_orders}' # noqa:WPS336
|
||||
|
||||
address.draw(
|
||||
radius=radius,
|
||||
color=config.RESTAURANT_COLOR,
|
||||
fill_color=config.RESTAURANT_COLOR,
|
||||
fill_opacity=0.3,
|
||||
tooltip=tooltip,
|
||||
)
|
||||
|
||||
else:
|
||||
address.draw(
|
||||
radius=1, color=config.RESTAURANT_COLOR, tooltip=tooltip,
|
||||
)
|
||||
|
||||
return self.map
|
||||
|
||||
def draw_zip_codes(self) -> folium.Map: # pragma: no cover
|
||||
"""Draw all addresses on the `.map`, colorized by their `.zip_code`.
|
||||
|
||||
This does not make a distinction between restaurant and customer addresses.
|
||||
Also, due to the high memory usage, the number of orders is not calculated.
|
||||
|
||||
Returns:
|
||||
`.map` for convenience in interactive usage
|
||||
"""
|
||||
# First, create a color map with distinct colors for each zip code.
|
||||
all_zip_codes = sorted(
|
||||
row[0]
|
||||
for row in db.session.execute(
|
||||
f""" -- # noqa:S608
|
||||
SELECT DISTINCT
|
||||
zip_code
|
||||
FROM
|
||||
{config.CLEAN_SCHEMA}.addresses
|
||||
WHERE
|
||||
city_id = {self.id};
|
||||
""",
|
||||
)
|
||||
)
|
||||
cmap = utils.make_random_cmap(len(all_zip_codes), bright=False)
|
||||
colors = {
|
||||
code: utils.rgb_to_hex(*cmap(index))
|
||||
for index, code in enumerate(all_zip_codes)
|
||||
}
|
||||
|
||||
# Second, draw every address on the `.map.
|
||||
for address in self.addresses:
|
||||
# Non-primary addresses are covered by primary ones anyway.
|
||||
if not address.is_primary:
|
||||
continue
|
||||
|
||||
marker = folium.Circle( # noqa:WPS317
|
||||
(address.latitude, address.longitude),
|
||||
color=colors[address.zip_code],
|
||||
radius=1,
|
||||
)
|
||||
marker.add_to(self.map)
|
||||
|
||||
return self.map
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue