Skip to content

Commit

Permalink
fix: handle missing coordinates (#177)
Browse files Browse the repository at this point in the history
* fix: handle missing coordinates

* refactor: extract abstract profile layer class

* test: update story snapshots

* refactor: apply lint conditions

* fix: sanitize missing coordinates

* test: disable test in CI

* test: disable flaky visual test

* test: remove redundant story snapshots
  • Loading branch information
hkfb authored Dec 24, 2024
1 parent ab54d67 commit 570f68f
Show file tree
Hide file tree
Showing 20 changed files with 130 additions and 117 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/chromium_layers-activity-layer--activity-with-map.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/chromium_layers-activity-layer--phong-shading.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/chromium_layers-activity-layer--use-data-url.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/webkit_layers-activity-layer--phong-shading.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/webkit_layers-activity-layer--profile-width.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/webkit_layers-activity-layer--set-activity-color.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/webkit_layers-activity-layer--use-data-url.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 22 additions & 1 deletion src/layers/activity-layer/activity-layer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "../../constant.stories";
import { StreetLayer } from "../street-layer";
import * as d3 from "d3-color";
import { Color } from "@deck.gl/core";
import { Color, type MapViewState } from "@deck.gl/core";

export default {
title: "Layers / Activity Layer",
Expand Down Expand Up @@ -337,3 +337,24 @@ export const UseDataUrl: StoryObj = {
},
},
};

export const MissingCoordinate: StoryObj = {
render: () => {
const data = "stage-7-parcours.gpx";
const layer = new ActivityLayer({ data });
const initialViewState: MapViewState = {
longitude: -0.3,
latitude: 44.2,
zoom: 10,
pitch: 60,
};
return (
<DeckGL
layers={[layer]}
initialViewState={initialViewState}
controller
></DeckGL>
);
},
tags: ["no-visual-test"],
};
20 changes: 18 additions & 2 deletions src/layers/activity-layer/activity-layer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { GPXLoader, TCXLoader } from "@loaders.gl/kml";
import { FeatureCollection, Feature, LineString } from "geojson";
import { FeatureCollection, Feature, LineString, Position } from "geojson";
import { type DefaultProps } from "@deck.gl/core";
import {
FaceProfileLayer,
FaceProfileLayerProps,
} from "../face-profile-layer/face-profile-layer";
import { simplify } from "@turf/simplify";
import { coordEach } from "@turf/meta";
import _ from "lodash";

export type ActivityLayerProps<DataT = unknown> = FaceProfileLayerProps<DataT>;

Expand All @@ -27,7 +30,20 @@ export class ActivityLayer<

function dataTransform(collection: FeatureCollection) {
const feature: Feature = collection.features[0];
const geometry = feature.geometry as LineString;
const simplified = simplify(feature, { tolerance: 0.0003 });
const geometry = simplified.geometry as LineString;

// Sanitize coordinates.
coordEach(geometry, (coord: Position) => {
if (coord.length === 3 && !_.isUndefined(coord[2])) {
return coord;
}

console.info(`Encountered non 3D coordinates: ${coord}`);

return [coord[0], coord[1], 0];
});

const coordinates = geometry.coordinates;
return [coordinates];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { SimpleMeshLayer, SimpleMeshLayerProps } from "@deck.gl/mesh-layers";
import { type DefaultProps } from "@deck.gl/core";
import {
Polyline,
getOffset,
} from "../../profile-layer/extrudePolylineProfile";
import { UpdateParameters, type Position } from "@deck.gl/core";
import { lineString, multiLineString } from "@turf/helpers";
import { Feature, LineString, MultiLineString } from "geojson";
import { centroid } from "@turf/centroid";
import { cleanCoords } from "@turf/clean-coords";
import { extrudeRoadMeshes } from "../util/extrudeRoadMeshes";

export type AbstractProfileLayerData = Polyline[];

export interface AbstractProfileLayerProps<DataT = unknown>
extends Omit<SimpleMeshLayerProps<DataT>, "mesh"> {
width?: number;
phongShading?: boolean;
}

const getPosition = (data: unknown) => {
const path = lineString(data as Polyline);
const origin = centroid(path as Feature<LineString>).geometry.coordinates;
return [origin[0], origin[1], 0] as Position;
};

const defaultProps: DefaultProps<AbstractProfileLayerProps> = {
...SimpleMeshLayer.defaultProps,
id: "road-layer",
getPosition,
getColor: [200, 100, 150, 255],
parameters: {
cullMode: "back",
},
phongShading: false,
};

export abstract class AbstractProfileLayer<
DataT = AbstractProfileLayerData,
PropsT = AbstractProfileLayerProps,
> extends SimpleMeshLayer<DataT, PropsT & AbstractProfileLayerProps> {
static layerName = "AbstractProfileLayer";
static defaultProps = defaultProps;

_getMesh(activities: AbstractProfileLayerData) {
const paths = multiLineString(activities);

const clean = cleanCoords(paths);

const origin = centroid(clean as Feature<MultiLineString>);

const pathMeterOffset = clean.geometry.coordinates.map(
(polyline: Polyline) =>
getOffset(polyline, origin.geometry.coordinates),
);

const profileWidth = this.props.width ?? 100;

return extrudeRoadMeshes(pathMeterOffset[0] as Polyline, profileWidth);
}

updateState(args: UpdateParameters<this>) {
super.updateState(args);
}
}
Original file line number Diff line number Diff line change
@@ -1,74 +1,29 @@
import { SimpleMeshLayer, SimpleMeshLayerProps } from "@deck.gl/mesh-layers";
import { type DefaultProps } from "@deck.gl/core";
import {
Polyline,
getOffset,
} from "../../profile-layer/extrudePolylineProfile";
import { UpdateParameters, type Position } from "@deck.gl/core";
import { UpdateParameters } from "@deck.gl/core";
import _ from "lodash";
import { lineString, multiLineString } from "@turf/helpers";
import { simplify } from "@turf/simplify";
import { Feature, LineString, MultiLineString } from "geojson";
import { centroid } from "@turf/centroid";
import { extrudeRoadMeshes } from "../util/extrudeRoadMeshes";
import {
AbstractProfileLayer,
AbstractProfileLayerData,
AbstractProfileLayerProps,
} from "../abstract-profile-layer/abstract-profile-layer";

export type SideProfileLayerData = Polyline[];
export type SideProfileLayerData = AbstractProfileLayerData;

export interface SideProfileLayerProps<DataT = unknown>
extends Omit<SimpleMeshLayerProps<DataT>, "mesh"> {
width?: number;
phongShading?: boolean;
}

const getPosition = (data: unknown) => {
const path = lineString(data as Polyline);
const origin = centroid(path as Feature<LineString>).geometry.coordinates;
return [origin[0], origin[1], 0] as Position;
};
export type SideProfileLayerProps<DataT = unknown> =
AbstractProfileLayerProps<DataT>;

const defaultProps: DefaultProps<SideProfileLayerProps> = {
...SimpleMeshLayer.defaultProps,
id: "road-layer",
getPosition,
getColor: [200, 100, 150, 255],
parameters: {
cullMode: "back",
},
phongShading: false,
...AbstractProfileLayer.defaultProps,
id: "side-profile-layer",
};

export class SideProfileLayer<
DataT = SideProfileLayerData,
PropsT = SideProfileLayerProps,
> extends SimpleMeshLayer<DataT, PropsT & SideProfileLayerProps> {
> extends AbstractProfileLayer<DataT, PropsT & SideProfileLayerProps> {
static layerName = "SideProfileLayer";
static defaultProps = defaultProps;

_getMesh(activities: SideProfileLayerData) {
const paths = multiLineString(activities);

const origin = centroid(paths as Feature<MultiLineString>);

const pathMeterOffset = activities.map((polyline: Polyline) =>
getOffset(polyline, origin.geometry.coordinates),
);

const geojson = pathMeterOffset.map((polyline) => lineString(polyline));

const simplified = geojson.map((polyline) =>
simplify(polyline as Feature<LineString>),
);

const extrudedProfile = simplified.map((polyline) =>
extrudeRoadMeshes(
polyline.geometry.coordinates as Polyline,
this.props.width ?? 100,
),
);

return extrudedProfile[0];
}

updateState(args: UpdateParameters<this>) {
super.updateState(args);
if (!args.changeFlags.propsOrDataChanged) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,74 +1,29 @@
import { SimpleMeshLayer, SimpleMeshLayerProps } from "@deck.gl/mesh-layers";
import { type DefaultProps } from "@deck.gl/core";
import {
Polyline,
getOffset,
} from "../../profile-layer/extrudePolylineProfile";
import { UpdateParameters, type Position } from "@deck.gl/core";
import { UpdateParameters } from "@deck.gl/core";
import _ from "lodash";
import { lineString, multiLineString } from "@turf/helpers";
import { simplify } from "@turf/simplify";
import { Feature, LineString, MultiLineString } from "geojson";
import { centroid } from "@turf/centroid";
import { extrudeRoadMeshes } from "../util/extrudeRoadMeshes";
import {
AbstractProfileLayer,
AbstractProfileLayerProps,
AbstractProfileLayerData,
} from "../abstract-profile-layer/abstract-profile-layer";

export type TopProfileLayerData = Polyline[];
export type TopProfileLayerData = AbstractProfileLayerData;

export interface TopProfileLayerProps<DataT = unknown>
extends Omit<SimpleMeshLayerProps<DataT>, "mesh"> {
width?: number;
phongShading?: boolean;
}

const getPosition = (data: unknown) => {
const path = lineString(data as Polyline);
const origin = centroid(path as Feature<LineString>).geometry.coordinates;
return [origin[0], origin[1], 0] as Position;
};
export type TopProfileLayerProps<DataT = unknown> =
AbstractProfileLayerProps<DataT>;

const defaultProps: DefaultProps<TopProfileLayerProps> = {
...SimpleMeshLayer.defaultProps,
id: "road-layer",
getPosition,
getColor: [200, 100, 150, 255],
parameters: {
cullMode: "back",
},
phongShading: false,
...AbstractProfileLayer.defaultProps,
id: "top-profile-layer",
};

export class TopProfileLayer<
DataT = TopProfileLayerData,
PropsT = TopProfileLayerProps,
> extends SimpleMeshLayer<DataT, PropsT & TopProfileLayerProps> {
> extends AbstractProfileLayer<DataT, PropsT & TopProfileLayerProps> {
static layerName = "TopProfileLayer";
static defaultProps = defaultProps;

_getMesh(activities: TopProfileLayerData) {
const paths = multiLineString(activities);

const origin = centroid(paths as Feature<MultiLineString>);

const pathMeterOffset = activities.map((polyline: Polyline) =>
getOffset(polyline, origin.geometry.coordinates),
);

const geojson = pathMeterOffset.map((polyline) => lineString(polyline));

const simplified = geojson.map((polyline) =>
simplify(polyline as Feature<LineString>),
);

const extrudedProfile = simplified.map((polyline) =>
extrudeRoadMeshes(
polyline.geometry.coordinates as Polyline,
this.props.width ?? 100,
),
);

return extrudedProfile[0];
}

updateState(args: UpdateParameters<this>) {
super.updateState(args);
if (!args.changeFlags.propsOrDataChanged) {
Expand Down

0 comments on commit 570f68f

Please sign in to comment.