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

HTML-based VerticalProfileChart #33

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
5 changes: 5 additions & 0 deletions remix/.storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/main.min.css" />
<style>
.firebase-emulator-warning {
display: none;
}
</style>
2 changes: 2 additions & 0 deletions remix/app/fb/components/Map/Colors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export const Colors = {

mainThemeColor: '#002e94',
white: '#ffffff',

terrain: '#883d00bc',
};

ConfigProvider.config({
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<ChartOuterContainer ref={ref}>
<ChartContainer $width={widthInPixels}>
<svg
width={widthInPixels}
height={availableHeight}
viewBox={`0 0 ${widthInPixels} ${availableHeight}`}
>
<rect
fill="none"
stroke="black"
x="0"
y="0"
width={widthInPixels}
height={availableHeight}
/>
<path d={d} stroke={'brown'} fill={`${Colors.terrain}`} />
</svg>
</ChartContainer>
</ChartOuterContainer>
);
};

const ChartOuterContainer = styled.div`
height: 100%;
overflow-y: scroll;
`;

const ChartContainer = styled.div<ContainerProps>`
${({ $width }) => `${$width ? `width: ${$width}px` : 'width: 100%'};`}
height: 100%;
`;
type ContainerProps = { $width?: number };
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type SetWaypointAltitude = ({
altitude: number | null;
}) => void;

type Props = {
export type VerticalProfileChartProps = {
route: Route;
elevation: ElevationAtPoint;
setWaypointAltitude?: SetWaypointAltitude;
Expand All @@ -52,7 +52,7 @@ export const VerticalProfileChart = ({
setWaypointAltitude,
airspaceOverlaps,
fitToSpace,
}: Props) => {
}: VerticalProfileChartProps) => {
const { width: availableWidth, ref } = useResizeDetector();

const onDragEnd =
Expand Down Expand Up @@ -491,15 +491,15 @@ const hashCode = (s: string) => {

function spaceBackgroundColor(type: AirspaceType | DangerZoneType) {
return type === AirspaceType.CTR
? '#99ABD1'
? '#99ABD17d'
: type === DangerZoneType.P
? '#ff00009f'
: type === DangerZoneType.D
? '#ff6a003d'
: type === DangerZoneType.R
? '#ff6a003d'
: type === AirspaceType.SIV
? Colors.sivThickBorder
? '#7398807d'
: '#21003f7d';
}

Expand Down
52 changes: 3 additions & 49 deletions remix/app/services/elevation/ElevationService.server.tsx
Original file line number Diff line number Diff line change
@@ -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<any> => {
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,
Expand All @@ -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<any> {
const chunks = _.chunk(latLngs, 300);

const dataSets = await Promise.all(
Expand 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 = () => '';
72 changes: 0 additions & 72 deletions remix/app/services/elevation/ElevationService.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion remix/app/services/elevation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './ElevationService';
export * from './ElevationService.server';
export * from './requestOpenElevationChunkMemoized';
55 changes: 38 additions & 17 deletions remix/app/services/elevation/requestOpenElevationChunkMemoized.tsx
Original file line number Diff line number Diff line change
@@ -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<any> => {
//@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;
}
};
Expand All @@ -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;
}
}
Loading