diff --git a/remix/.storybook/preview-head.html b/remix/.storybook/preview-head.html index 4e77d3c..42d2aaa 100644 --- a/remix/.storybook/preview-head.html +++ b/remix/.storybook/preview-head.html @@ -1,2 +1,7 @@ + diff --git a/remix/app/fb/components/Map/Colors.tsx b/remix/app/fb/components/Map/Colors.tsx index aef7286..02ddc28 100644 --- a/remix/app/fb/components/Map/Colors.tsx +++ b/remix/app/fb/components/Map/Colors.tsx @@ -25,6 +25,8 @@ export const Colors = { mainThemeColor: '#002e94', white: '#ffffff', + + terrain: '#883d00bc', }; ConfigProvider.config({ diff --git a/remix/app/fb/components/VerticalProfileChart/VerticalProfileChart.new.tsx b/remix/app/fb/components/VerticalProfileChart/VerticalProfileChart.new.tsx new file mode 100644 index 0000000..98b99fe --- /dev/null +++ b/remix/app/fb/components/VerticalProfileChart/VerticalProfileChart.new.tsx @@ -0,0 +1,99 @@ +import _ from 'lodash'; +import { useResizeDetector } from 'react-resize-detector'; +import styled from 'styled-components'; +import type { Route } from '../../../domain'; +import type { AirspaceSegmentOverlap } from '../../../domain/AirspaceIntersection/routeAirspaceOverlaps'; +import type { ElevationAtPoint } from '../../elevationOnRoute'; +import { Colors } from '../Map/Colors'; + +export type SetWaypointAltitude = ({ + waypointPosition, + altitude, +}: { + waypointPosition: number; + altitude: number | null; +}) => void; + +export type VerticalProfileChartProps = { + route: Route; + elevation: ElevationAtPoint; + setWaypointAltitude?: SetWaypointAltitude; + airspaceOverlaps: AirspaceSegmentOverlap[]; + fitToSpace?: boolean; +}; + +export const VerticalProfileChart = ({ + route, + elevation, + setWaypointAltitude, + airspaceOverlaps, + fitToSpace, +}: VerticalProfileChartProps) => { + const { width: availableWidth, height: availableHeight, ref } = useResizeDetector(); + + // if (availableHeight === undefined) { + // return <>; + // } + + const totalDistance = route.totalDistance; + + const widthInPixels = fitToSpace + ? undefined + : Math.max(availableWidth || 0, totalDistance * 15); + + const minAltitude = _.min(route.waypoints.map((w) => w.altitude)); + const maxAltitude = _.max(route.waypoints.map((w) => w.altitude)); + const minElevation = _.min(elevation.elevations); + const maxElevation = _.max(elevation.elevations) || 0; + const maxY = (_.max([maxElevation, maxAltitude]) || 2000) + 2000; + const minY = _.min([minElevation, minAltitude]) || 0; + + const oneFootInPixels = availableHeight / (maxY - minY); + + const oneNmInPixels = widthInPixels / totalDistance; + + console.log('availableHeight', availableHeight); + console.log('widthInPixels', widthInPixels); + console.log('oneFootInPixels', oneFootInPixels); + console.log('oneNmInPixels', oneNmInPixels); + console.log('minY - maxY', `${minY} - ${maxY}`); + + const toY = (y: number) => availableHeight + minY * oneFootInPixels - y * oneFootInPixels; + + const d = `M${elevation.distancesFromStartInNm + .map((dist, i) => `${dist * oneNmInPixels} ${toY(elevation.elevations[i])}`) + .join('L')}L${totalDistance * oneNmInPixels} ${toY(minY)}`; + + return ( + + + + + + + + + ); +}; + +const ChartOuterContainer = styled.div` + height: 100%; + overflow-y: scroll; +`; + +const ChartContainer = styled.div` + ${({ $width }) => `${$width ? `width: ${$width}px` : 'width: 100%'};`} + height: 100%; +`; +type ContainerProps = { $width?: number }; diff --git a/remix/app/fb/components/VerticalProfileChart/VerticalProfileChart.tsx b/remix/app/fb/components/VerticalProfileChart/VerticalProfileChart.tsx index 49a3b59..3ba4043 100644 --- a/remix/app/fb/components/VerticalProfileChart/VerticalProfileChart.tsx +++ b/remix/app/fb/components/VerticalProfileChart/VerticalProfileChart.tsx @@ -34,7 +34,7 @@ export type SetWaypointAltitude = ({ altitude: number | null; }) => void; -type Props = { +export type VerticalProfileChartProps = { route: Route; elevation: ElevationAtPoint; setWaypointAltitude?: SetWaypointAltitude; @@ -52,7 +52,7 @@ export const VerticalProfileChart = ({ setWaypointAltitude, airspaceOverlaps, fitToSpace, -}: Props) => { +}: VerticalProfileChartProps) => { const { width: availableWidth, ref } = useResizeDetector(); const onDragEnd = @@ -491,7 +491,7 @@ const hashCode = (s: string) => { function spaceBackgroundColor(type: AirspaceType | DangerZoneType) { return type === AirspaceType.CTR - ? '#99ABD1' + ? '#99ABD17d' : type === DangerZoneType.P ? '#ff00009f' : type === DangerZoneType.D @@ -499,7 +499,7 @@ function spaceBackgroundColor(type: AirspaceType | DangerZoneType) { : type === DangerZoneType.R ? '#ff6a003d' : type === AirspaceType.SIV - ? Colors.sivThickBorder + ? '#7398807d' : '#21003f7d'; } diff --git a/remix/app/services/elevation/ElevationService.server.tsx b/remix/app/services/elevation/ElevationService.server.tsx index e3d64ee..d579c08 100644 --- a/remix/app/services/elevation/ElevationService.server.tsx +++ b/remix/app/services/elevation/ElevationService.server.tsx @@ -1,31 +1,9 @@ import retry from 'async-retry'; import _ from 'lodash'; -import memoize from 'memoizee'; -import type { Response } from 'node-fetch'; import { requestOpenElevationChunkMemoized } from './requestOpenElevationChunkMemoized'; -import { sha1 } from './sha1'; export type LatLng = [lat: number, lng: number]; -const requestGoogleChunk = memoize( - async (chunk: LatLng[]): Promise => { - return await fetch( - `https://maps.googleapis.com/maps/api/elevation/json?key=${null}&locations=${chunk - .map(([lat, lng]) => `${lat},${lng}`) - .join('|')}`, - ) - .then((res) => res.json()) - //@ts-ignore - .then((json) => json.results.map((result) => result.elevation.toFixed(2))); - }, - { - promise: true, - maxAge: 1000 * 60 * 60 * 24, // 1 day - max: 100, - normalizer: (args: [LatLng[]]) => sha1(args[0].join(',')), - }, -); - const requestOpenElevationChunkMemoizedWithRetry = async (chunk: LatLng[]) => { return await retry(async (bail) => await requestOpenElevationChunkMemoized(chunk), { retries: 3, @@ -34,15 +12,9 @@ const requestOpenElevationChunkMemoizedWithRetry = async (chunk: LatLng[]) => { }); }; -async function googleElevationService(latLngs: [lat: number, lng: number][]) { - const chunks = _.chunk(latLngs, 300); - console.log(chunks.map((c) => c.length)); - const dataSets = await Promise.all(chunks.map(requestGoogleChunk)); - - return _.flatten(dataSets); -} - -export async function openElevationApiElevationService(latLngs: [lat: number, lng: number][]) { +export async function openElevationApiElevationService( + latLngs: [lat: number, lng: number][], +): Promise { const chunks = _.chunk(latLngs, 300); const dataSets = await Promise.all( @@ -52,22 +24,4 @@ export async function openElevationApiElevationService(latLngs: [lat: number, ln return _.flatten(dataSets); } -class HTTPResponseError extends Error { - response: Response; - - constructor(response: Response) { - super(`HTTP Error Response: ${response.status} ${response.statusText}`); - this.response = response; - } -} - -export const checkStatus = (response: Response) => { - if (response.ok) { - // response.status >= 200 && response.status < 300 - return response; - } else { - throw new HTTPResponseError(response); - } -}; - export const log = () => ''; diff --git a/remix/app/services/elevation/ElevationService.tsx b/remix/app/services/elevation/ElevationService.tsx deleted file mode 100644 index 47be56e..0000000 --- a/remix/app/services/elevation/ElevationService.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import retry from 'async-retry'; -import _ from 'lodash'; -import memoize from 'memoizee'; -import type { Response } from 'node-fetch'; -import { requestOpenElevationChunkMemoized } from './requestOpenElevationChunkMemoized'; -import { sha1 } from './sha1'; - -export type LatLng = [lat: number, lng: number]; - -const requestGoogleChunk = memoize( - async (chunk: LatLng[]): Promise => { - return await fetch( - `https://maps.googleapis.com/maps/api/elevation/json?key=${null}&locations=${chunk - .map(([lat, lng]) => `${lat},${lng}`) - .join('|')}`, - ) - .then((res) => res.json()) - //@ts-ignore - .then((json) => json.results.map((result) => result.elevation.toFixed(2))); - }, - { - promise: true, - maxAge: 1000 * 60 * 60 * 24, // 1 day - max: 100, - normalizer: (args: [LatLng[]]) => sha1(args[0].join(',')), - }, -); - -const requestOpenElevationChunkMemoizedWithRetry = async (chunk: LatLng[]) => { - console.log('requesting', chunk.length); - return await retry(async (bail) => await requestOpenElevationChunkMemoized(chunk), { - retries: 3, - minTimeout: 500, - onRetry: () => console.log('retrying'), - }); -}; - -async function googleElevationService(latLngs: [lat: number, lng: number][]) { - const chunks = _.chunk(latLngs, 300); - console.log(chunks.map((c) => c.length)); - const dataSets = await Promise.all(chunks.map(requestGoogleChunk)); - - return _.flatten(dataSets); -} - -export async function openElevationApiElevationService(latLngs: [lat: number, lng: number][]) { - const chunks = _.chunk(latLngs, 300); - - const dataSets = await Promise.all( - chunks.map((chunk) => requestOpenElevationChunkMemoizedWithRetry(chunk)), - ); - - return _.flatten(dataSets); -} - -class HTTPResponseError extends Error { - response: Response; - - constructor(response: Response) { - super(`HTTP Error Response: ${response.status} ${response.statusText}`); - this.response = response; - } -} - -export const checkStatus = (response: Response) => { - if (response.ok) { - // response.status >= 200 && response.status < 300 - return response; - } else { - throw new HTTPResponseError(response); - } -}; diff --git a/remix/app/services/elevation/index.tsx b/remix/app/services/elevation/index.tsx index b4ca1e9..8770c44 100644 --- a/remix/app/services/elevation/index.tsx +++ b/remix/app/services/elevation/index.tsx @@ -1,2 +1,2 @@ -export * from './ElevationService'; +export * from './ElevationService.server'; export * from './requestOpenElevationChunkMemoized'; diff --git a/remix/app/services/elevation/requestOpenElevationChunkMemoized.tsx b/remix/app/services/elevation/requestOpenElevationChunkMemoized.tsx index e175c14..fd131ef 100644 --- a/remix/app/services/elevation/requestOpenElevationChunkMemoized.tsx +++ b/remix/app/services/elevation/requestOpenElevationChunkMemoized.tsx @@ -1,29 +1,32 @@ import memoize from 'memoizee'; -import type { LatLng } from './ElevationService'; -import { checkStatus } from './ElevationService'; +import type { Response as NodeFetchResponse } from 'node-fetch'; +import type { LatLng } from './ElevationService.server'; import { sha1 } from './sha1'; +type Result = { + latitude: number; + longitude: number; + elevation: number; +}; + const requestOpenElevationChunk = async (chunk: LatLng[]): Promise => { - //@ts-ignore - const fetch = (await import('node-fetch')).default; - console.log(`issuing a request to open elevation`); - const response = await fetch(`https://api.open-elevation.com/api/v1/lookup`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - locations: chunk.map(([lat, lng]) => ({ latitude: lat, longitude: lng })), - }), - }); try { + const response = await fetch(`https://api.open-elevation.com/api/v1/lookup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + locations: chunk.map(([lat, lng]) => ({ latitude: lat, longitude: lng })), + }), + }); + const r = checkStatus(response); - const json = await r.json(); - //@ts-ignore + const json = (await r.json()) as { results: Result[] }; return json.results.map((result) => result.elevation.toFixed(2)); } catch (e) { - // console.log(e); + console.error(e); throw e; } }; @@ -37,3 +40,21 @@ export const requestOpenElevationChunkMemoized = memoize(requestOpenElevationChu return hash; }, }); + +const checkStatus = (response: Response | NodeFetchResponse) => { + if (response.ok) { + // response.status >= 200 && response.status < 300 + return response; + } else { + throw new HTTPResponseError(response); + } +}; + +class HTTPResponseError extends Error { + response: Response | NodeFetchResponse; + + constructor(response: Response | NodeFetchResponse) { + super(`HTTP Error Response: ${response.status} ${response.statusText}`); + this.response = response; + } +} diff --git a/remix/stories/VerticalProfileChart.new.stories.tsx b/remix/stories/VerticalProfileChart.new.stories.tsx new file mode 100644 index 0000000..07070a3 --- /dev/null +++ b/remix/stories/VerticalProfileChart.new.stories.tsx @@ -0,0 +1,176 @@ +import type { ComponentMeta } from '@storybook/react'; +import { foldW } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/function'; +import { draw } from 'io-ts/lib/Decoder'; +import { useEffect, useState } from 'react'; +import { AiracData, AirspaceType, DangerZoneType, Height } from 'ts-aerodata-france'; +import currentCycle from 'ts-aerodata-france/build/jsonData/2022-10-06.json'; +import { Route } from '~/domain/Route'; +import type { VerticalProfileChartProps } from '~/fb/components/VerticalProfileChart/VerticalProfileChart'; +import { VerticalProfileChart } from '~/fb/components/VerticalProfileChart/VerticalProfileChart.new'; +import type { ElevationAtPoint } from '~/fb/elevationOnRoute'; +import { elevationOnRoute } from '~/fb/elevationOnRoute'; +import { openElevationApiElevationService } from '~/services/elevation'; +import '../app/styles/global.css'; +import longRouteJSON from './route.json'; +import shortRouteJSON from './shortRoute.json'; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: 'Example/VerticalProfileChartNew', + component: VerticalProfileChart, + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: {}, +} as ComponentMeta; + +type LoadedData = { loaded: { airacData: AiracData } }; + +export const ShortRoute = ( + args: VerticalProfileChartProps, + { loaded: { airacData } }: LoadedData, +) => { + if (!airacData) { + return null; + } + + return ( + + ); +}; + +export const LongRoute = ( + args: VerticalProfileChartProps, + { loaded: { airacData } }: LoadedData, +) => { + if (!airacData) { + return null; + } + + console.log('rendering'); + const route = decodeRoute(airacData)(longRouteJSON); + console.log('route', route); + return ( + + a.type === DangerZoneType.D && + a.lowerLimit.toString().includes('ASFC'), + )[1]!, + segments: [[100, 110]], + }, + // { + // airspace: airacData.dangerZones.filter( + // (a) => + // a.type === DangerZoneType.D && + // a.lowerLimit.toString().includes('ASFC'), + // )[0]!, + // segments: [[50, 90]], + // }, + { + airspace: { + ...airacData.dangerZones.filter( + (a) => + a.type === DangerZoneType.D && + a.lowerLimit.toString().includes('ASFC'), + )[0]!, + name: 'D 999', + lowerLimit: new Height(1000), + }, + segments: [[50, 90]], + }, + { + airspace: airacData.airspaces.filter( + (a) => a.lowerLimit.feetValue > 5000, + )[0]!, // Should not get displayed + segments: [[50, 90]], + }, + { + airspace: airacData.airspaces.filter( + (a) => a.type === AirspaceType.CTR && a.lowerLimit.toString() === 'SFC', + )[8]!, + segments: [[-100, 30]], + }, + { + airspace: airacData.dangerZones.filter( + (a) => + a.type === DangerZoneType.P && + a.higherLimit.toString().includes('ASFC') && + a.higherLimit.feetValue <= 4000, + )[3]!, + segments: [[130, 150]], + }, + { + airspace: airacData.dangerZones.filter( + (a) => + a.type === DangerZoneType.P && + a.higherLimit.toString().includes('ASFC') && + a.higherLimit.feetValue <= 4000, + )[2]!, + segments: [[130, 150]], + }, + { + airspace: airacData.dangerZones.filter( + (a) => + a.type === DangerZoneType.P && + a.higherLimit.toString().includes('ASFC') && + a.higherLimit.feetValue <= 4000, + )[4]!, + segments: [[130, 150]], + }, + ], + }} + /> + ); +}; + +const RouteC = (args: VerticalProfileChartProps & { airacData: AiracData }) => { + const [elevation, setElevation] = useState(); + console.log('rendering component with airacData', args.airacData); + console.log('elevation', args.elevation); + + useEffect(() => { + elevationOnRoute({ + elevationService: { + getElevationsForLatLngs: (latLngs) => + openElevationApiElevationService(latLngs.map(({ lat, lng }) => [lat, lng])), + }, + })(args.route).then((e) => setElevation(e)); + }, []); + + return elevation ? : <>; +}; + +const loaders = [ + async () => ({ + airacData: await AiracData.loadCycle(currentCycle), + }), +]; + +ShortRoute.loaders = loaders; +LongRoute.loaders = loaders; + +const decodeRoute = (airacData: AiracData) => (routeJSON: unknown) => { + return pipe( + Route.codec(airacData).decode(routeJSON), + foldW( + (e) => { + console.log(draw(e)); + return Route.empty(); + }, + (r) => r, + ), + ); +}; diff --git a/remix/stories/VerticalProfileChart.stories.tsx b/remix/stories/VerticalProfileChart.stories.tsx index 71beae2..5b7752e 100644 --- a/remix/stories/VerticalProfileChart.stories.tsx +++ b/remix/stories/VerticalProfileChart.stories.tsx @@ -1,125 +1,170 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta } from '@storybook/react'; import { foldW } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/function'; import { draw } from 'io-ts/lib/Decoder'; import { useEffect, useState } from 'react'; -import { AiracData, AirspaceType, DangerZoneType } from 'ts-aerodata-france'; +import { AiracData, AirspaceType, DangerZoneType, Height } from 'ts-aerodata-france'; +import currentCycle from 'ts-aerodata-france/build/jsonData/2022-10-06.json'; import { Route } from '~/domain/Route'; +import type { VerticalProfileChartProps } from '~/fb/components/VerticalProfileChart/VerticalProfileChart'; import { VerticalProfileChart } from '~/fb/components/VerticalProfileChart/VerticalProfileChart'; import type { ElevationAtPoint } from '~/fb/elevationOnRoute'; -import { elevationOnRoute, emptyElevation } from '~/fb/elevationOnRoute'; -import { localApiElevationService } from '~/fb/ElevationService/localApiElevationService'; +import { elevationOnRoute } from '~/fb/elevationOnRoute'; +import { openElevationApiElevationService } from '~/services/elevation'; import '../app/styles/global.css'; -import routeJSON from './route.json'; +import longRouteJSON from './route.json'; import shortRouteJSON from './shortRoute.json'; // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export export default { - title: 'Example/VerticalProfileChart', + title: 'Example/VerticalProfileChart1', component: VerticalProfileChart, // More on argTypes: https://storybook.js.org/docs/react/api/argtypes argTypes: {}, } as ComponentMeta; -const airacData = AiracData.loadCycle(AiracCycles.SEP_08_2022); -// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Template: ComponentStory = (args) => { +type LoadedData = { loaded: { airacData: AiracData } }; + +export const ShortRoute = ( + args: VerticalProfileChartProps, + { loaded: { airacData } }: LoadedData, +) => { + if (!airacData) { + return null; + } + + return ( + + ); +}; + +export const LongRoute = ( + args: VerticalProfileChartProps, + { loaded: { airacData } }: LoadedData, +) => { + if (!airacData) { + return null; + } + + console.log('rendering'); + const route = decodeRoute(airacData)(longRouteJSON); + console.log('route', route); + return ( + + a.type === DangerZoneType.D && + a.lowerLimit.toString().includes('ASFC'), + )[1]!, + segments: [[100, 110]], + }, + // { + // airspace: airacData.dangerZones.filter( + // (a) => + // a.type === DangerZoneType.D && + // a.lowerLimit.toString().includes('ASFC'), + // )[0]!, + // segments: [[50, 90]], + // }, + { + airspace: { + ...airacData.dangerZones.filter( + (a) => + a.type === DangerZoneType.D && + a.lowerLimit.toString().includes('ASFC'), + )[0]!, + name: 'D 999', + lowerLimit: new Height(1000), + }, + segments: [[50, 90]], + }, + { + airspace: airacData.airspaces.filter( + (a) => a.lowerLimit.feetValue > 5000, + )[0]!, // Should not get displayed + segments: [[50, 90]], + }, + { + airspace: airacData.airspaces.filter( + (a) => a.type === AirspaceType.CTR && a.lowerLimit.toString() === 'SFC', + )[8]!, + segments: [[-100, 30]], + }, + { + airspace: airacData.dangerZones.filter( + (a) => + a.type === DangerZoneType.P && + a.higherLimit.toString().includes('ASFC') && + a.higherLimit.feetValue <= 4000, + )[3]!, + segments: [[130, 150]], + }, + { + airspace: airacData.dangerZones.filter( + (a) => + a.type === DangerZoneType.P && + a.higherLimit.toString().includes('ASFC') && + a.higherLimit.feetValue <= 4000, + )[2]!, + segments: [[130, 150]], + }, + { + airspace: airacData.dangerZones.filter( + (a) => + a.type === DangerZoneType.P && + a.higherLimit.toString().includes('ASFC') && + a.higherLimit.feetValue <= 4000, + )[4]!, + segments: [[130, 150]], + }, + ], + }} + /> + ); +}; + +const RouteC = (args: VerticalProfileChartProps & { airacData: AiracData }) => { const [elevation, setElevation] = useState(); + console.log('rendering component with airacData', args.airacData); + console.log('elevation', args.elevation); useEffect(() => { elevationOnRoute({ - elevationService: localApiElevationService, + elevationService: { + getElevationsForLatLngs: (latLngs) => + openElevationApiElevationService(latLngs.map(({ lat, lng }) => [lat, lng])), + }, })(args.route).then((e) => setElevation(e)); }, []); - const newArgs = { ...args, elevation: elevation || emptyElevation }; - - return ; + return elevation ? : <>; }; -// export const Primary = Template.bind({}); -export const WithElevation = Template.bind({}); -export const ShortRoute = Template.bind({}); -// More on args: https://storybook.js.org/docs/react/writing-stories/args -// Primary.args = { -// route: Route.factory({ -// waypoints: [ -// latLngWaypointFactory({ latLng: { lat: 42, lng: 2.5 }, name: 'WPT 1', altitude: 12 }), -// latLngWaypointFactory({ latLng: { lat: 42, lng: 3 }, name: 'WPT 2', altitude: 3000 }), -// latLngWaypointFactory({ latLng: { lat: 42, lng: 3.5 }, name: 'WPT 3', altitude: 123 }), -// ], -// }), -// elevation: emptyElevation, -// airspaceOverlaps: [], -// }; +const loaders = [ + async () => ({ + airacData: await AiracData.loadCycle(currentCycle), + }), +]; -WithElevation.args = { - route: pipe( - Route.codec(airacData).decode(routeJSON), - foldW( - (e) => { - console.log(draw(e)); - return Route.empty(); - }, - (r) => r, - ), - ), - airspaceOverlaps: [ - { - airspace: airacData.dangerZones.filter( - (a) => a.type === DangerZoneType.D && a.lowerLimit.toString().includes('ASFC'), - )[1]!, - segments: [[100, 110]], - }, - { - airspace: airacData.dangerZones.filter( - (a) => a.type === DangerZoneType.D && a.lowerLimit.toString().includes('ASFC'), - )[0]!, - segments: [[50, 90]], - }, - { - airspace: airacData.airspaces.filter((a) => a.lowerLimit.feetValue > 5000)[0]!, // Should not get displayed - segments: [[50, 90]], - }, - { - airspace: airacData.airspaces.filter( - (a) => a.type === AirspaceType.CTR && a.lowerLimit.toString() === 'SFC', - )[8]!, - segments: [[-100, 30]], - }, - { - airspace: airacData.dangerZones.filter( - (a) => - a.type === DangerZoneType.P && - a.higherLimit.toString().includes('ASFC') && - a.higherLimit.feetValue <= 4000, - )[3]!, - segments: [[130, 150]], - }, - { - airspace: airacData.dangerZones.filter( - (a) => - a.type === DangerZoneType.P && - a.higherLimit.toString().includes('ASFC') && - a.higherLimit.feetValue <= 4000, - )[2]!, - segments: [[130, 150]], - }, - { - airspace: airacData.dangerZones.filter( - (a) => - a.type === DangerZoneType.P && - a.higherLimit.toString().includes('ASFC') && - a.higherLimit.feetValue <= 4000, - )[4]!, - segments: [[130, 150]], - }, - ], -}; +ShortRoute.loaders = loaders; +LongRoute.loaders = loaders; -ShortRoute.args = { - route: pipe( - Route.codec(airacData).decode(shortRouteJSON), +const decodeRoute = (airacData: AiracData) => (routeJSON: unknown) => { + return pipe( + Route.codec(airacData).decode(routeJSON), foldW( (e) => { console.log(draw(e)); @@ -127,6 +172,5 @@ ShortRoute.args = { }, (r) => r, ), - ), - airspaceOverlaps: [], + ); };