Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ÖV-Güteklassen und Station count more flexible #1572

Closed
EPajares opened this issue Sep 22, 2022 · 3 comments
Closed

Make ÖV-Güteklassen und Station count more flexible #1572

EPajares opened this issue Sep 22, 2022 · 3 comments
Assignees

Comments

@EPajares
Copy link
Collaborator

EPajares commented Sep 22, 2022

Goal of this issue

Currently, the user can only compute these indicators for one study area.

  1. Allow superuser to compute ÖV Güteklassen and station count for more than one study area. If possible we make also this optional. I believe currently we are just using the active study area and the client does not pass anything.
  2. For ÖV Güteklasse add option to pass the hardcoded station_config dynamically. However, to avoid breaking changes, this should only be optional. The user should still be able to pass no configuration.

Resources

count_pt_service_stations()

# == PUBLIC TRANSPORT INDICATORS ==#
async def count_pt_service_stations(
self, db: AsyncSession, start_time, end_time, weekday, study_area_id, return_type
) -> Any:
"""Get count of public transport stations for every service."""
template_sql = SQLReturnTypes[return_type.value].value
stations_count = await db.execute(
text(
template_sql
% f"""
SELECT * FROM basic.count_public_transport_services_station(:study_area_id, :start_time, :end_time, :weekday)
"""
),
{
"study_area_id": study_area_id,
"start_time": timedelta(seconds=start_time),
"end_time": timedelta(seconds=end_time),
"weekday": weekday,
},
)
stations_count = stations_count.fetchall()[0][0]
return stations_count

compute_oev_gueteklassen()

async def compute_oev_gueteklassen(
self,
db: AsyncSession,
start_time,
end_time,
weekday,
study_area_id,
) -> FeatureCollection:
"""
Calculate the OEV-Gueteklassen for a given time period and weekday.
"""
# TODO: Use isochrone calculation instead of buffer
station_config = {
"groups": {
"0": "B", # route_type: category of public transport route
"1": "A",
"2": "A",
"3": "C",
"4": "B",
"5": "B",
"6": "B",
"7": "B",
"11": "B",
"12": "B",
},
"time_frequency": [0, 4, 10, 19, 39, 60, 119],
"categories": [
{
"A": 1, # i.e. types of transports in category A are in transport stop category I
"B": 1,
"C": 2,
},
{"A": 1, "B": 2, "C": 3},
{"A": 2, "B": 3, "C": 4},
{"A": 3, "B": 4, "C": 5},
{"A": 4, "B": 5, "C": 6},
{"A": 5, "B": 6, "C": 6},
],
"classification": {
"1": {300: "A", 500: "A", 750: "B", 1000: "C"},
"2": {300: "A", 500: "B", 750: "C", 1000: "D"},
"3": {300: "B", 500: "C", 750: "D", 1000: "E"},
"4": {300: "C", 500: "D", 750: "E", 1000: "F"},
"5": {300: "D", 500: "E", 750: "F"},
"6": {300: "E", 500: "F"},
"7": {300: "F"},
},
}
time_window = (end_time - start_time) / 60
stations = await db.execute(
text(
"""
SELECT trip_cnt, ST_TRANSFORM(geom, 3857) as geom
FROM basic.count_public_transport_services_station(:study_area_id, :start_time, :end_time, :weekday)
"""
),
{
"study_area_id": study_area_id,
"start_time": timedelta(seconds=start_time),
"end_time": timedelta(seconds=end_time),
"weekday": weekday,
},
)
stations = stations.fetchall()
project = pyproj.Transformer.from_crs(
pyproj.CRS("EPSG:3857"), pyproj.CRS("EPSG:4326"), always_xy=True
).transform
classificiation_buffers = {}
for station in stations:
station_geom = wkb.loads(station.geom, hex=True)
trip_cnt = station["trip_cnt"]
# - find station group
station_groups = [] # list of station groups e.g [A, B, C]
acc_trips = {} # accumulated trips per station group
for route_type, trip_count in trip_cnt.items():
station_group = station_config["groups"].get(str(route_type))
if station_group:
station_groups.append(station_group)
acc_trips[station_group] = acc_trips.get(station_group, 0) + trip_count
station_group = min(station_groups) # the highest priority (e.g A )
station_group_trip_count = acc_trips[station_group]
if station_group_trip_count == 0:
continue
station_group_trip_time_frequency = time_window / (station_group_trip_count / 2)
# - find station category based on time frequency and group
time_interval = bisect.bisect_left(
station_config["time_frequency"], station_group_trip_time_frequency
)
if time_interval == len(station_config["time_frequency"]):
continue # no category found
station_category = station_config["categories"][time_interval - 1].get(station_group)
if not station_category:
continue
# - find station classification based on category
station_classification = station_config["classification"][str(station_category)]
for buffer_dist, classification in station_classification.items():
buffer_geom = station_geom.buffer(buffer_dist)
# add geom in classfication_shapes
if classification not in classificiation_buffers:
classificiation_buffers[classification] = [buffer_geom]
else:
classificiation_buffers[classification].append(buffer_geom)
features = []
agg_union = None
for classification, shapes in dict(sorted(classificiation_buffers.items())).items():
union_geom = unary_union(shapes)
difference_geom = union_geom
if agg_union:
difference_geom = union_geom.difference(agg_union)
agg_union = agg_union.union(union_geom)
else:
agg_union = union_geom
feature = Feature(
geometry=transform(project, difference_geom),
properties={"class": classification},
)
if feature.geometry is not None:
features.append(feature)
return FeatureCollection(features)

Deliverables

Add options to endpoints.

Branch to derive

Dev

Branch to push feature/oev-guetklasse-flexible

@EPajares EPajares added the API label Sep 22, 2022
@EPajares EPajares added this to the v1.3 milestone Sep 22, 2022
@metemaddar
Copy link
Contributor

This should affect the following endpoints:

@router.get("/pt-station-count")
async def count_pt_service_stations(
*,
db: AsyncSession = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user),
start_time: Optional[int] = Query(
description="Start time in seconds since midnight (Default: 07:00)",
default=25200,
ge=0,
le=86400,
),
end_time: Optional[int] = Query(
description="End time in seconds since midnight (Default: 09:00)",
default=32400,
ge=0,
le=86400,
),
weekday: Optional[int] = Query(
description="Weekday (1 = Monday, 7 = Sunday) (Default: Monday)", default=1, ge=1, le=7
),
study_area_id: Optional[int] = Query(
default=None, description="Study area id (Default: User active study area)"
),
return_type: ReturnType = Query(
default=ReturnType.geojson, description="Return type of the response"
),
):
"""
Return the number of trips for every route type on every station given a time period and weekday.
"""
if start_time >= end_time:
raise HTTPException(status_code=422, detail="Start time must be before end time")
is_superuser = crud.user.is_superuser(current_user)
if study_area_id is not None and not is_superuser:
owns_study_area = await CRUDBase(models.UserStudyArea).get_by_multi_keys(
db, keys={"user_id": current_user.id, "study_area_id": study_area_id}
)
if owns_study_area == []:
raise HTTPException(
status_code=400,
detail="The user doesn't own the study area or user doesn't have enough privileges",
)
else:
study_area_id = study_area_id or current_user.active_study_area_id
stations_count = await crud.indicator.count_pt_service_stations(
db=db,
start_time=start_time,
end_time=end_time,
weekday=weekday,
study_area_id=study_area_id,
return_type=return_type,
)
return return_geojson_or_geobuf(stations_count, return_type.value)

@router.get("/pt-oev-gueteklassen")
async def calculate_oev_gueteklassen(
*,
db: AsyncSession = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user),
start_time: Optional[int] = Query(
description="Start time in seconds since midnight (Default: 07:00)",
default=25200,
ge=0,
le=86400,
),
end_time: Optional[int] = Query(
description="End time in seconds since midnight (Default: 09:00)",
default=32400,
ge=0,
le=86400,
),
weekday: Optional[int] = Query(
description="Weekday (1 = Monday, 7 = Sunday) (Default: Monday)", default=1, ge=1, le=7
),
study_area_id: Optional[int] = Query(
default=None, description="Study area id (Default: User active study area)"
),
return_type: ReturnType = Query(
default=ReturnType.geojson, description="Return type of the response"
),
):
"""
ÖV-Güteklassen (The public transport quality classes) is an indicator for access to public transport.
The indicator makes it possible to identify locations which, thanks to their good access to public transport, have great potential as focal points for development.
The calculation in an automated process from the data in the electronic timetable (GTFS).
"""
if start_time >= end_time:
raise HTTPException(status_code=422, detail="Start time must be before end time")
is_superuser = crud.user.is_superuser(current_user)
if study_area_id is not None and not is_superuser:
owns_study_area = await CRUDBase(models.UserStudyArea).get_by_multi_keys(
db, keys={"user_id": current_user.id, "study_area_id": study_area_id}
)
if owns_study_area == []:
raise HTTPException(
status_code=400,
detail="The user doesn't own the study area or user doesn't have enough privileges",
)
else:
study_area_id = study_area_id or current_user.active_study_area_id
oev_gueteklassen_features = await crud.indicator.compute_oev_gueteklassen(
db=db,
start_time=start_time,
end_time=end_time,
weekday=weekday,
study_area_id=study_area_id,
)
if return_type.value == ReturnType.geojson.value:
oev_gueteklassen_features = jsonable_encoder(oev_gueteklassen_features)
return return_geojson_or_geobuf(oev_gueteklassen_features, return_type.value)

And for the first one, if I get it well, we need to get study_area_id as a list, so, if we get study_area_id as a list, then it should be return calculation as a list as well.
Of course I think it would be good to always return as a single type (list) and not to sometimes send as an object and sometimes send as a list. @majkshkurti, Which one is better? is it okay to always pass the result as a list?

@metemaddar
Copy link
Contributor

@majkshkurti
for refactoring calculate_oev_gueteklassen we need to pass a json to the endpoint. Is it better to also refactor it as post or otherwise we may need to encode station_config to base64.

@majkshkurti
Copy link
Member

majkshkurti commented Sep 28, 2022

@metemaddar That will break the client implementation as we use a get request for all indicators.
However, I think we can change it, so no problem. I will not suggest using base64. It will make this endpoint practically unusable outside GOAT.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants