Skip to content

Commit

Permalink
Merge pull request #17 v2.2.0
Browse files Browse the repository at this point in the history
v2.2.0
  • Loading branch information
LeoKle authored Aug 28, 2023
2 parents 84b8e09 + 5228778 commit 3dcc6e1
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 157 deletions.
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "loa-viewer-backend",
"version": "2.1.0",
"version": "2.2.0",
"description": "",
"main": "app.js",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions backend/src/models/sector.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const sectorSchema = new mongoose.Schema({
});

export const airspaceSchema = new mongoose.Schema({
country: { type: String, required: true },
id: { type: String, required: true },
group: { type: String, required: true },
owner: { type: [String], required: true },
Expand Down
1 change: 1 addition & 0 deletions backend/src/services/sector.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async function retrieveAirspacesFromCountries(countries: string[]): Promise<Airs

if (airspacedata) {
for (const airspace of airspacedata) {
airspace.country = country;
airspaces.push(airspace);
}
}
Expand Down
116 changes: 85 additions & 31 deletions frontend/src/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,38 @@ import { WaypointRecord } from 'interfaces/waypointRecord.interface';
import { DisplayAirspaces } from './Sectors';
import Airspace from '@shared/interfaces/sector.interface';
import sectorService from 'services/sector.service';
import { Button } from 'primereact/button';
import { Toolbar } from 'primereact/toolbar';
import { Divider } from 'primereact/divider';

export default function LoaViewerMap() {
const [conditions, setConditions] = useState<FrontendCondition[]>([]);
const [drawnConditions, setDrawnConditions] = useState<WaypointRecord[]>([]);
const [searchInput, setSearchInput] = useState<string>('');
const [loading, setLoading] = useState(true);

// persistent data
const [conditions, setConditions] = useState<FrontendCondition[]>([]);
const [airspaces, setAirspaces] = useState<Airspace[]>([]);
const [drawnAirspaces, setDrawnAirspaces] = useState<Airspace[]>([]);
const selectableGroups = ['EDMM', 'EDWW', 'EDGG', 'EDYY', 'EDUU', 'APP'].sort((a, b) => a.localeCompare(b));

const selectableGroups = ['EDMM', 'EDWW', 'EDGG', 'EDYY', 'EDUU', 'APP'];
const sortedSelectableGroups = selectableGroups.sort((a, b) => a.localeCompare(b));
const [allStations, setAllStations] = useState<String[]>([]);
// Toolbar:
// Dropdown menu
const [selectedSector, setSelectedSector] = useState<String>('GIN');
const [selectedFir, setSelectedFir] = useState<String>('EDGG');
// Button
const [showVerticalLimits, setShowVerticalLimits] = useState<boolean>(false);
const [conditionSearchRange, setConditionSearchRange] = useState<'ofSelectedSector' | 'all'>('ofSelectedSector');
const [filterFromToSector, setFilterFromToSector] = useState<boolean>(false);
const [filterFromToSectorSelection, setFilterFromToSectorSelection] = useState<'from sector' | 'to sector'>('to sector');
// search bar
const [searchInput, setSearchInput] = useState<string>('');

// filtered data - based on input in toolbar
const [drawnConditions, setDrawnConditions] = useState<WaypointRecord[]>([]);
const [drawnAirspaces, setDrawnAirspaces] = useState<Airspace[]>([]);
const [allStations, setAllStations] = useState<String[]>([]);

useEffect(() => {
conditionService.getConditions().then((data: FrontendCondition[]) => {
const convertedData: FrontendCondition[] = data.map((element: FrontendCondition) => {
const convertedConditionData: FrontendCondition[] = data.map((element: FrontendCondition) => {
return {
_id: element._id,
aerodrome: element.aerodrome,
Expand All @@ -47,18 +60,19 @@ export default function LoaViewerMap() {
to_fir: element.to_fir,
};
});
setConditions(convertedData);
setConditions(convertedConditionData);
});
sectorService.getWaypoints().then((data: Airspace[]) => {
const convertedData: Airspace[] = data.map((element: Airspace) => {
sectorService.getSectors().then((data: Airspace[]) => {
const convertedAirspaceData: Airspace[] = data.map((element: Airspace) => {
return {
country: element.country,
id: element.id,
group: element.group,
owner: element.owner,
sectors: element.sectors,
};
});
setAirspaces(convertedData);
setAirspaces(convertedAirspaceData);
});

setLoading(false);
Expand All @@ -67,53 +81,93 @@ export default function LoaViewerMap() {
const debounceSearch = useDebounce(searchInput, 500);
useEffect(() => {
if (!loading) {
const searchConditions = filterConditionsService(conditions, searchInput, selectedSector);
const searchConditions = filterConditionsService(conditions, searchInput, conditionSearchRange === 'ofSelectedSector' ? selectedSector : undefined, filterFromToSector === true ? filterFromToSectorSelection : false);
groupConditionsByCop(searchConditions).then(groupedConditions => {
setDrawnConditions(groupedConditions);
});
}
}, [debounceSearch, loading, searchInput, conditions, selectedSector]);
}, [debounceSearch, loading, searchInput, conditions, selectedSector, conditionSearchRange, filterFromToSector, filterFromToSectorSelection]);

useEffect(() => {
const stationsSet: Set<string> = new Set();
for (const airspace of airspaces) {
if (airspace.group === selectedFir) {
const owner = airspace.owner[0];
// filter approach stations not belonging to vACC Germany - all approach sectors are not using Vatsim callsigns i.e. no _ in their name
const isNonGermanApproachStation = owner.includes('_');
// filter Maastricht Sectors belonging to vACC Germany - sectors belonging to vACC Germany have 3 or more characters
const isGermanMaastrichtSector = owner.length > 2;
if (!isNonGermanApproachStation && isGermanMaastrichtSector) {
stationsSet.add(owner);
}
if (airspace.country === 'germany' && airspace.group === selectedFir) {
stationsSet.add(airspace.owner[0]);
}
}

const stations: string[] = Array.from(stationsSet);
const sortedStations = stations.sort((a, b) => a.localeCompare(b));
setAllStations(sortedStations);
}, [selectedFir, loading, airspaces]);

// If the FIR changes, select the first station from the FIR
if (!sortedStations.includes(selectedSector as string) && sortedStations.length !== 0) {
setSelectedSector(sortedStations[0]);
}
}, [selectedSector, selectedFir, loading, airspaces]);

useEffect(() => {
const filtered = airspaces.filter(airspace => airspace.owner[0] === selectedSector);
setDrawnAirspaces(filtered);
}, [selectedFir, loading, airspaces, selectedSector]);

const startContent = [
<InputText key="InputTextSearch" type="search" placeholder={conditionSearchRange === 'ofSelectedSector' ? 'Search by sector' : 'Search all conditions'} onChange={e => setSearchInput(e.target.value)} />,
<Divider key="DividerSearch" layout="vertical" />,
<Button
key="ButtonSearchRange"
label="Filter by sector"
style={{ paddingRight: '20px' }}
severity={conditionSearchRange === 'ofSelectedSector' ? 'success' : 'danger'}
icon="pi pi-filter"
tooltip="Search all conditions or only of the selected sector"
tooltipOptions={{ position: 'mouse' }}
onClick={e => setConditionSearchRange(conditionSearchRange === 'all' ? 'ofSelectedSector' : 'all')}
/>,
<Dropdown key="DropdownSelectedStation" options={allStations} value={selectedSector} onChange={e => setSelectedSector(e.value)} disabled={conditionSearchRange === 'all'} />,
<Dropdown key="DropdownSelectedFIR" options={selectableGroups} value={selectedFir} onChange={e => setSelectedFir(e.value)} disabled={conditionSearchRange === 'all'} />,
<Divider key="DividerStationSelection" layout="vertical" />,
<Button
key="ButtonFilterFromTo"
label="Filter by"
severity={filterFromToSector === false ? 'danger' : 'success'}
icon="pi pi-filter"
tooltip="Filter in- or outgoing conditions"
tooltipOptions={{ position: 'mouse' }}
onClick={e => setFilterFromToSector(!filterFromToSector)}
disabled={conditionSearchRange !== 'ofSelectedSector'}
/>,
<Dropdown
key="DropdownFromTo"
options={['from sector', 'to sector']}
value={filterFromToSectorSelection}
onChange={e => setFilterFromToSectorSelection(e.value)}
disabled={!filterFromToSector || conditionSearchRange !== 'ofSelectedSector'}
/>,
];

const endContent = [
<Button
key="ButtonVerticalLimits"
label="Show vertical limits"
severity={showVerticalLimits === true ? 'success' : 'danger'}
icon={showVerticalLimits === true ? 'pi pi-check' : 'pi pi-times'}
onClick={e => setShowVerticalLimits(!showVerticalLimits)}
disabled={conditionSearchRange === 'all'}
/>,
];

return (
<>
<div>
<div style={{ position: 'absolute', zIndex: 1, top: '10%', left: '50%', transform: 'translate(-50%, -50%)', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<InputText type="search" placeholder="Search" onChange={e => setSearchInput(e.target.value)} />
<Dropdown options={allStations} value={selectedSector} onChange={e => setSelectedSector(e.value)} />
<Dropdown options={sortedSelectableGroups} value={selectedFir} onChange={e => setSelectedFir(e.value)} />
</div>
<Toolbar start={startContent} end={endContent} />

<MapContainer center={[50.026292, 8.765245]} zoom={8} style={{ width: '100vw', height: '100vh', zIndex: 0 }} maxZoom={10} minZoom={6}>
<MapContainer style={{ width: '100vw', height: '100vh', zIndex: 0 }} center={[50.026292, 8.765245]} zoom={8} maxZoom={10} minZoom={6}>
<TileLayer
url="https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
/>
<DisplayAirspaces airspaces={drawnAirspaces} combineSectors={true} />
{conditionSearchRange !== 'all' && <DisplayAirspaces airspaces={drawnAirspaces} showVerticalLimits={showVerticalLimits} />}
<Markers key={'Markers'} conditions={drawnConditions} />
</MapContainer>
</div>
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/components/MapConditionTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { WaypointRecord } from 'interfaces/waypointRecord.interface';
import { Column } from 'primereact/column';
import { DataTable } from 'primereact/datatable';

interface PropsTable {
conditions: WaypointRecord;
}

export function MapConditionTable({ conditions }: PropsTable) {
const agreements = conditions.conditions;

const values = agreements.map(condition => {
return {
id: `${condition._id}`,
adep_ades: `${condition.adep_ades === 'ADEP' ? '\u2191' : condition.adep_ades === 'ADES' ? '\u2193' : ''}${condition.aerodrome}`,
cop: `${condition.cop}`,
level: `${condition.feet ? 'A' : 'FL'}
${condition.level}
${condition.xc === null ? '' : condition.xc === 'B' ? '\u2193' : '\u2191'}`,
sc: `${condition.special_conditions}`,
from_sector: `${condition.from_sector}`,
to_sector: `${condition.to_sector}`,
from_fir: `${condition.from_fir}`,
to_fir: `${condition.to_fir}`,
};
});

const hasSpecialConditions = agreements.some(condition => condition.special_conditions);

const columns = [
<Column key="adep_ades" field="adep_ades" header="AD" />,
<Column key="cop" field="cop" header="COP" />,
<Column key="level" field="level" header="Level" />,
hasSpecialConditions && <Column key="sc" field="sc" header="Special Conditions" />,
<Column key="from_sector" field="from_sector" header="From Sector" />,
<Column key="to_sector" field="to_sector" header="To Sector" />,
<Column key="from_fir" field="from_fir" header="from FIR" />,
<Column key="to_fir" field="to_fir" header="to FIR" />,
];

return (
<>
<DataTable className="map-table-style" value={values} style={{ textAlign: 'center', fontSize: '13px', fontWeight: 'bold', padding: '10px' }}>
{columns}
</DataTable>
</>
);
}
75 changes: 4 additions & 71 deletions frontend/src/components/Markers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
import { WaypointRecord } from 'interfaces/waypointRecord.interface';
import location from '../img/location.png';
import React from 'react';
import { MapConditionTable } from './MapConditionTable';

interface ExtendedWaypointRecord extends WaypointRecord {
drawn: boolean;
Expand Down Expand Up @@ -98,7 +99,7 @@ function Markers({ conditions }: { conditions: WaypointRecord[] }) {
<Marker
key={`${waypoint.name}-table`}
position={[latitude, longitude]}
icon={MarkerConditionTable(condition, zoom)}
icon={MarkerConditionTable(condition)}
eventHandlers={{ click: () => handleMarkerClick(waypoint.name) }}
zIndexOffset={condition.zIndex}
/>
Expand All @@ -125,79 +126,11 @@ function MarkerNameWidget(name: string) {
});
}

function MarkerConditionTable(condition: WaypointRecord, zoom: number) {
function MarkerConditionTable(condition: WaypointRecord) {
return new DivIcon({
className: 'custom-icon',
html: renderToStaticMarkup(<ConditionTableIcon zoom={zoom} conditions={condition} />),
html: renderToStaticMarkup(<MapConditionTable conditions={condition} />),
});
}

interface PropsTable {
zoom: number;
conditions: WaypointRecord;
}

function ConditionTableIcon({ zoom, conditions }: PropsTable) {
const tableStyle = {
fontSize: 1 / zoom + 10,
};

const agreements = conditions.conditions;
let hasSpecialConditions = false;
let columnSpan = 3;

agreements.forEach(condition => {
if (condition.special_conditions) {
hasSpecialConditions = true;
columnSpan = 4;
}
});

return (
<table className="map-table-style" style={tableStyle}>
<thead>
<tr>
<th className="center" colSpan={columnSpan} />
<th className="center line background-black" colSpan={2}>
Sector
</th>
<th className="center line background-black " colSpan={2}>
FIR
</th>
</tr>
<tr>
<th className="line background-black background-black">AD</th>
<th className="line background-black">COP</th>
<th className="line background-black">Level</th>
{hasSpecialConditions ? <th className="line background-black">Special Conditions</th> : null}
<th className="line background-black">From</th>
<th className="line background-black">To</th>
<th className="line background-black">From</th>
<th className="line background-black">To</th>
</tr>
</thead>
<tbody>
{agreements.map((condition, index) => (
<tr key={index}>
<td className="line background-black">
{condition.adep_ades === 'ADEP' ? '\u2191' : condition.adep_ades === 'ADES' ? '\u2193' : ''} {condition.aerodrome}
</td>
<td className="line background-black">{condition.cop}</td>
<td className="line background-black">
{condition.feet ? 'A' : 'FL'}
{condition.level}
{condition.xc}
</td>
{hasSpecialConditions ? <td className="line background-black">{condition.special_conditions}</td> : null}
<td className="line background-black">{condition.from_sector}</td>
<td className="line background-black">{condition.to_sector}</td>
<td className="line background-black">{condition.from_fir}</td>
<td className="line background-black">{condition.to_fir}</td>
</tr>
))}
</tbody>
</table>
);
}

export default Markers;
Loading

0 comments on commit 3dcc6e1

Please sign in to comment.