Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 80 additions & 6 deletions cypress/e2e/map/createRoute.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Priority,
ReusableComponentsVehicleModeEnum,
RouteDirectionEnum,
RouteTypeOfLineEnum,
StopAreaInput,
Expand Down Expand Up @@ -68,6 +69,34 @@ const rootOpts: Cypress.SuiteConfigOverrides = {
tags: [Tag.Routes, Tag.Map],
scrollBehavior: 'bottom',
};

function expectVisibleOptionsToMatchLineIds(
options: JQuery<HTMLElement>,
lineIds: string[],
) {
const visibleOptionIds = [...options]
.map((option) => option.getAttribute('data-testid'))
.map((testId) => testId?.split('::option::')[1]);

expect(lineIds).to.include.members(visibleOptionIds);
}

function assertLineOptionsMatchVehicleMode(
vehicleMode: ReusableComponentsVehicleModeEnum,
) {
const lineIds = baseDbResources.lines
.filter((line) => line.primary_vehicle_mode === vehicleMode)
.map((line) => line.line_id as string);

cy.getByTestId('RoutePropertiesFormComponent::chooseLineDropdown').click();

cy.get('[role="option"]').should(($options) => {
expectVisibleOptionsToMatchLineIds($options, lineIds);
});

cy.getByTestId('RoutePropertiesFormComponent::chooseLineDropdown').click();
}

describe('Route creation', rootOpts, () => {
let dbResources: SupportedResources;

Expand Down Expand Up @@ -115,7 +144,7 @@ describe('Route creation', rootOpts, () => {
});

it(
'Should create a new route',
'Should create a new bus route',
{
tags: [Tag.Smoke, Tag.Network],
},
Expand All @@ -125,7 +154,9 @@ describe('Route creation', rootOpts, () => {
MapPage.map.visit(mapLocation);
MapPage.map.waitForLoadToComplete();

MapFooter.getCreateRouteButton().click();
MapFooter.createRoute();
assertLineOptionsMatchVehicleMode(ReusableComponentsVehicleModeEnum.Bus);

MapPage.routePropertiesForm.fillRouteProperties({
finnishName: 'Test route',
label: '901Y',
Expand Down Expand Up @@ -224,6 +255,49 @@ describe('Route creation', rootOpts, () => {
},
);

it(
'Should create a new tram route',
{
tags: [Tag.Network],
},
() => {
const versionComment = 'E2E create tram route reason';

MapPage.map.visit(mapLocation);
MapPage.map.waitForLoadToComplete();

MapFooter.createRoute(ReusableComponentsVehicleModeEnum.Tram);
assertLineOptionsMatchVehicleMode(ReusableComponentsVehicleModeEnum.Tram);

MapPage.routePropertiesForm.fillRouteProperties({
finnishName: 'Test tram route',
label: '1112Y',
variant: '56',
line: '8543',
direction: RouteDirectionEnum.Outbound,
origin: {
finnishName: 'Test tram origin FIN',
finnishShortName: 'Test tram origin FIN shortName',
swedishName: 'Test tram origin SWE',
swedishShortName: 'Test tram origin SWE shortName',
},
destination: {
finnishName: 'Test tram destination FIN',
finnishShortName: 'Test tram destination FIN shortName',
swedishName: 'Test tram destination SWE',
swedishShortName: 'Test tram destination SWE shortName',
},
priority: Priority.Standard,
versionComment,
validityStartISODate: '2025-01-01',
validityEndISODate: '2030-12-01',
});

MapPage.editRouteModal.save();
MapPage.map.getLoader().should('exist');
},
);

it('should cancel creating a new route', () => {
MapPage.map.visit(mapLocation);
MapPage.map.waitForLoadToComplete();
Expand Down Expand Up @@ -267,7 +341,7 @@ describe('Route creation', rootOpts, () => {
MapPage.map.visit(mapLocation);

MapPage.map.waitForLoadToComplete();
MapFooter.getCreateRouteButton().click();
MapFooter.createRoute();
MapPage.routePropertiesForm.fillRouteProperties({
finnishName: 'Test route',
label: '901X',
Expand Down Expand Up @@ -372,7 +446,7 @@ describe('Route creation', rootOpts, () => {

MapPage.map.waitForLoadToComplete();

MapFooter.getCreateRouteButton().click();
MapFooter.createRoute();
MapPage.routePropertiesForm.fillRouteProperties({
finnishName: 'Erronous route',
label: '901F',
Expand Down Expand Up @@ -447,7 +521,7 @@ describe('Route creation', rootOpts, () => {

MapPage.map.waitForLoadToComplete();

MapFooter.getCreateRouteButton().click();
MapFooter.createRoute();
MapPage.routePropertiesForm.fillRouteProperties({
finnishName: 'Indefinite end time route',
label: '901I',
Expand Down Expand Up @@ -511,7 +585,7 @@ describe('Route creation', rootOpts, () => {

MapPage.map.waitForLoadToComplete();

MapFooter.getCreateRouteButton().click();
MapFooter.createRoute();
MapPage.routePropertiesForm.fillRouteProperties({
finnishName: 'Based on template test route',
label: '901T',
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/map/editRouteShape.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe('Edit route geometry', { tags: [Tag.Routes, Tag.Map] }, () => {
lng: mapLocation.lng,
});

MapFooter.getCreateRouteButton().click();
MapFooter.createRoute();
MapPage.routePropertiesForm.fillRouteProperties({
finnishName: 'Template route',
label: '902',
Expand Down
11 changes: 9 additions & 2 deletions cypress/pageObjects/map/MapFooter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ export class MapFooter {
return cy.getByTestId('MapFooter::drawRouteButton');
}

static createRoute() {
return MapFooter.getCreateRouteButton()
static createRoute(
vehicleMode: ReusableComponentsVehicleModeEnum = ReusableComponentsVehicleModeEnum.Bus,
) {
MapFooter.getCreateRouteButton()
.should('be.visible')
.and('be.enabled')
.click();

return cy
.get(`[data-vehicle-mode="${vehicleMode}"]`)
.should('be.visible')
.click();
}

static addStop(
Expand Down
43 changes: 36 additions & 7 deletions ui/src/api/routing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from 'axios';
import { ReusableComponentsVehicleModeEnum } from '../generated/graphql';
import { MapMatchingNoSegmentError } from '../utils';

type LatLng = {
Expand All @@ -11,15 +12,21 @@ type RouteBody = {
readonly linkSearchRadius?: number;
};

const positionToLatLng = (pos: GeoJSON.Position): LatLng => {
function positionToLatLng(pos: GeoJSON.Position): LatLng {
return { lng: pos[0], lat: pos[1] };
};
}

const apiClient = axios.create({
baseURL: '/api/mapmatching/api/route/v1',
});

const getBus = (coordinates: RouteBody) => apiClient.post('/bus', coordinates);
function getBus(coordinates: RouteBody) {
return apiClient.post('/bus', coordinates);
}

function getTram(coordinates: RouteBody) {
return apiClient.post('/tram', coordinates);
}

export type BusRouteResponse = {
readonly code: 'Ok';
Expand All @@ -45,17 +52,39 @@ export type BusRouteResponse = {
}[];
};

export const getBusRoute = async (
async function getRouteByVehicleMode(
coordinates: ReadonlyArray<GeoJSON.Position>,
) => {
vehicleMode: ReusableComponentsVehicleModeEnum,
) {
const request: RouteBody = {
routePoints: coordinates.map(positionToLatLng),
};
const response = await getBus(request);
const response =
vehicleMode === ReusableComponentsVehicleModeEnum.Tram
? await getTram(request)
: await getBus(request);

if (response.data.code === 'NoSegment') {
throw new MapMatchingNoSegmentError(response.data.message);
}

return response.data as BusRouteResponse;
};
}

export async function getBusRoute(
coordinates: ReadonlyArray<GeoJSON.Position>,
) {
return getRouteByVehicleMode(
coordinates,
ReusableComponentsVehicleModeEnum.Bus,
);
}

export async function getTramRoute(
coordinates: ReadonlyArray<GeoJSON.Position>,
) {
return getRouteByVehicleMode(
coordinates,
ReusableComponentsVehicleModeEnum.Tram,
);
}
9 changes: 8 additions & 1 deletion ui/src/components/forms/route/ChooseLineDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LineForComboboxFragment } from '../../../generated/graphql';
import { useAppSelector } from '../../../hooks';
import { selectEditedRouteData } from '../../../redux';
import { MAX_DATE, MIN_DATE } from '../../../time';
import {
ComboboxInputProps,
Expand Down Expand Up @@ -36,10 +38,15 @@ export const ChooseLineDropdown: FC<ChooseLineDropdownProps> = ({
onBlur,
}) => {
const { t } = useTranslation();
const { vehicleMode } = useAppSelector(selectEditedRouteData);

const [query, setQuery] = useState('');

const { lines, selectedLine } = useChooseLineDropdown(query, value);
const { lines, selectedLine } = useChooseLineDropdown(
query,
value,
vehicleMode,
);

const options = lines?.map(mapToOption) ?? [];

Expand Down
13 changes: 10 additions & 3 deletions ui/src/components/forms/route/useChooseLineDropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ import { DateTime } from 'luxon';
import { useState } from 'react';
import {
LineForComboboxFragment,
ReusableComponentsVehicleModeEnum,
useGetLinesForComboboxQuery,
useGetSelectedLineDetailsByIdQuery,
} from '../../../generated/graphql';
import { useDebouncedString } from '../../../hooks';
import { mapToSqlLikeValue, mapToVariables } from '../../../utils';

const GQL_GET_LINES_FOR_COMBOBOX = gql`
query GetLinesForCombobox($labelPattern: String!, $date: date!) {
query GetLinesForCombobox(
$labelPattern: String!
$date: date!
$primary_vehicle_mode: reusable_components_vehicle_mode_enum
) {
route_line(
limit: 10
where: {
label: { _ilike: $labelPattern }
primary_vehicle_mode: { _eq: $primary_vehicle_mode }
_or: [
{ validity_end: { _gte: $date } }
{ validity_end: { _is_null: true } }
Expand Down Expand Up @@ -48,7 +54,7 @@ const GQL_LINE_FOR_COMBOBOX = gql`
export const useChooseLineDropdown = (
query: string,
lineId?: string,
observationDate?: DateTime,
vehicleMode?: ReusableComponentsVehicleModeEnum,
): {
lines: ReadonlyArray<LineForComboboxFragment>;
selectedLine?: LineForComboboxFragment;
Expand All @@ -63,7 +69,8 @@ export const useChooseLineDropdown = (
const linesResult = useGetLinesForComboboxQuery(
mapToVariables({
labelPattern: `${mapToSqlLikeValue(debouncedQuery)}%`,
date: observationDate ?? today.toISO(),
date: today.toISO(),
primary_vehicle_mode: vehicleMode,
}),
);

Expand Down
5 changes: 2 additions & 3 deletions ui/src/components/map/FloatingAddModeFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ export const FloatingAddModeFooter: FC<FloatingAddModeFooterProps> = ({
};

const onCancelDrawMode = () => {
// Return to route modal mode when canceling drawing mode
dispatch(setRouteMetadataFormOpenAction(true));
// Stop the drawing mode but keep the route creation process
// Close metadata modal and stop route creation (same behavior as route cancel).
dispatch(setRouteMetadataFormOpenAction(false));
onCancel();
};

Expand Down
5 changes: 3 additions & 2 deletions ui/src/components/map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
useState,
} from 'react';
import { MapLayerMouseEvent } from 'react-map-gl/maplibre';
import { ReusableComponentsVehicleModeEnum } from '../../generated/graphql';
import { useAppDispatch, useAppSelector } from '../../hooks';
import {
MapEntityEditorViewState,
Expand Down Expand Up @@ -70,8 +71,8 @@ function useRouteEditorImperativeHandle(
routeEditorRef: RefObject<RouteEditorRef>,
) {
useImperativeHandle(ref, () => ({
onDrawRoute: () => {
routeEditorRef.current?.onDrawRoute();
onDrawRoute: (vehicleMode: ReusableComponentsVehicleModeEnum) => {
routeEditorRef.current?.onDrawRoute(vehicleMode);
},
onEditRoute: () => {
routeEditorRef.current?.onEditRoute();
Expand Down
5 changes: 4 additions & 1 deletion ui/src/components/map/MapFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import some from 'lodash/some';
import { FC } from 'react';
import { ReusableComponentsVehicleModeEnum } from '../../generated/graphql';
import { useAppSelector } from '../../hooks';
import {
MapEntityEditorViewState,
Expand Down Expand Up @@ -37,7 +38,9 @@ function useActiveModes() {
}

type MapFooterProps = {
readonly onDrawRoute: () => void;
readonly onDrawRoute: (
vehicleMode: ReusableComponentsVehicleModeEnum,
) => void;
readonly onEditRoute: () => void;
readonly onStopEditRoute: () => void;
readonly onDeleteRoute: () => void;
Expand Down
Loading
Loading