@equinor/esv-intersection
Version:
Intersection component package with testing and automatic documentation.
397 lines (362 loc) • 10.4 kB
text/typescript
import { interpolateRgb, quantize } from 'd3-interpolate';
import { scaleOrdinal } from 'd3-scale';
import { convertColor } from '../utils/color';
import {
StratUnit,
SurfaceMetaAndValues,
SurfaceLine,
SurfaceArea,
SurfaceData,
} from './interfaces';
const TRANSLUCENT_RED = 0x80000000;
const WHITE = 0xffffffff;
type MappedSurfaces = {
name: string;
isBase: boolean;
values: number[];
color: string;
visualization: string;
};
type StratGroup = {
age: number;
name: string;
};
type Stratigraphy = {
unit: StratUnit;
group: string;
name: string;
isBase: boolean;
values: number[];
color: string;
visualization: string;
};
type MappedGroup = {
id: string;
label: string;
color: string;
top: number[];
};
interface SurfaceAreaGrouping {
[propType: string]: SurfaceArea[];
}
/**
* Generate surface data from trajectory, stratcolum and surface data
* Code originally developed for the REP project
* @param trajectory Projected trajectory generated from the poslog used when retrieving surface data from surface API
* @param stratColumn Strat columnd from SMDA
* @param surfaceData - Surfaces meta data with surface values in data section
* @return Surface areas ready for rendering in geolayer
*/
export function generateSurfaceData(
trajectory: number[][],
stratColumn: StratUnit[],
surfaceData: SurfaceMetaAndValues[],
): SurfaceData {
const filteredSurfaces: SurfaceMetaAndValues[] = surfaceData.filter(
s => s.data.values,
);
const mappedSurfaces = mapSurfaceData(filteredSurfaces);
const stratGroups = new Map<string, StratGroup>();
const stratigraphies = combineSurfacesAndStratColumn(
mappedSurfaces,
stratColumn,
stratGroups,
);
sortStratigraphies(stratigraphies);
const lines: SurfaceLine[] = getSurfaceLines(mappedSurfaces, trajectory);
const surfaceAreas: SurfaceAreaGrouping = generateSurfaceAreas(
trajectory,
stratigraphies,
stratColumn,
);
const groups: MappedGroup[] = mapGroups(stratGroups, surfaceAreas);
const groupAreas: SurfaceArea[] = generateGroupAreas(groups, trajectory);
//Combine group areas with surface areas
const areas: SurfaceArea[] = [
...groupAreas,
...Object.values(surfaceAreas)
.flat()
.filter(d => !d.exclude),
];
const data = {
lines,
areas,
};
return data;
}
/**
* Get surfaces which should be rendered as lines
* @param mappedSurfaces
* @param trajectory
*/
function getSurfaceLines(
mappedSurfaces: MappedSurfaces[],
trajectory: number[][],
): SurfaceLine[] {
const lines: SurfaceLine[] = mappedSurfaces
.filter((d: MappedSurfaces) => d.visualization === 'line')
.map((l: MappedSurfaces) => ({
id: l.name,
label: l.name,
width: 2,
color: convertColor(l.color || 'black'),
data: trajectory.map((p, j) => [p[0]!, l.values[j]!]),
}));
return lines;
}
function generateGroupAreas(
groups: MappedGroup[],
trajectory: number[][],
): SurfaceArea[] {
const groupAreas = groups.map((g: MappedGroup, i: number) => {
const next: MappedGroup | null =
i + 1 < groups.length ? groups[i + 1]! : null;
return {
id: g.id,
color: convertColor(g.color),
data: trajectory.map((p: number[], j: number) => [
p[0]!,
g.top[j]!,
...(next ? [next.top[j]!] : []),
]),
};
});
return groupAreas;
}
function mapGroups(
stratGroups: Map<string, StratGroup>,
surfaceAreas: SurfaceAreaGrouping,
): MappedGroup[] {
const groups = Array.from(stratGroups.values())
.sort((a: StratGroup, b: StratGroup) => a.age - b.age)
.filter((g: StratGroup) => {
const surfaces = surfaceAreas[g.name];
const isValid = surfaces && surfaces.length > 0;
if (!isValid) {
console.warn(
`Intersection surface group '${g.name}' has no valid entries and will be discarded.`,
);
}
return isValid;
})
.map((g: StratGroup, i: number) => {
const surface = surfaceAreas[g.name]!;
const top = surface[0]!;
return {
id: g.name,
label: g.name,
color: unassignedColorScale(i),
top: top.data.map((d: number[]) => d[1]!),
};
});
return groups;
}
function combineSurfacesAndStratColumn(
mappedSurfaces: MappedSurfaces[],
stratColumn: StratUnit[],
stratGroups: Map<string, StratGroup>,
): Stratigraphy[] {
const firstUnit =
stratColumn && stratColumn.find((d: StratUnit) => d.stratUnitLevel === 1);
const defaultGroupName: string = firstUnit ? firstUnit.identifier : 'SEABED';
const stratigrafies = mappedSurfaces
.filter(
(d: MappedSurfaces) =>
d.visualization === 'interval' || d.visualization === 'none',
)
.map((s: MappedSurfaces) => {
const path: StratUnit[] = [];
const stratUnit = findStratcolumnUnit(stratColumn, s.name, path);
if (!stratUnit) {
console.warn(`No match for ${s.name} in strat column`);
}
const group = path[0]! || stratUnit;
const groupName: string = (group && group.identifier) || defaultGroupName;
if (group && !stratGroups.has(groupName)) {
stratGroups.set(groupName, {
age: group.topAge,
name: group.identifier,
});
}
return {
...s,
unit: stratUnit!,
group: groupName,
};
});
return stratigrafies;
}
/**
* Sort stratigrafies on unit and age, base after top and higher level after lower
* @param stratigrafies
*/
function sortStratigraphies(stratigrafies: Stratigraphy[]): void {
stratigrafies.sort((a: Stratigraphy, b: Stratigraphy) => {
if (!a.unit && !b.unit) {
return 0;
}
if (!a.unit) {
return -1;
}
if (!b.unit) {
return 1;
}
const aAge = a.isBase ? a.unit.baseAge : a.unit.topAge;
const bAge = b.isBase ? b.unit.baseAge : b.unit.topAge;
if (aAge !== bAge) {
return aAge - bAge;
}
if (a.isBase && !b.isBase) {
return 1;
}
if (!a.isBase && b.isBase) {
return -1;
}
return a.unit.stratUnitLevel - b.unit.stratUnitLevel;
});
}
/**
* @param {[]} units
* @param {string} unitname
* @param {[]} path
*/
function findStratcolumnUnit(
units: StratUnit[],
unitname: string,
path: StratUnit[] = [],
): StratUnit | null {
const unit = units.find(
(u: StratUnit) => u.identifier.toLowerCase() === unitname.toLowerCase(),
);
if (unit) {
// Build path
let temp: StratUnit | undefined = unit;
do {
path.unshift(temp);
temp = units.find(
(u: StratUnit) => u.identifier === temp!.stratUnitParent,
);
} while (temp);
return unit;
}
return null;
}
function mapSurfaceData(surfaces: SurfaceMetaAndValues[]): MappedSurfaces[] {
return surfaces.map((s: SurfaceMetaAndValues) => {
const displayName: string = s.visualSettings.displayName;
const name: string = displayName.replace(/\s(Base|Top)/gi, '');
const isBase: boolean = displayName.toLowerCase().endsWith('base');
return {
name,
isBase,
values: s.data.values,
color: s.visualSettings.colors.crossSection,
visualization: s.visualSettings.crossSection.toLowerCase(),
};
});
}
function getColorFromUnit(unit: StratUnit): number {
if (unit.colorR === null || unit.colorG === null || unit.colorB === null) {
return TRANSLUCENT_RED;
}
const res: number = (unit.colorR << 16) | (unit.colorG << 8) | unit.colorB;
return res;
}
const unassignedColorScale = scaleOrdinal<number, string>()
.domain([0, 100])
.range(quantize(interpolateRgb('#e6f1cf', '#85906d'), 10));
/**
* Find the best matching base index based on name or by values
*/
function findBestMatchingBaseIndex(
top: Stratigraphy,
index: number,
surfaces: Stratigraphy[],
stratColumn: StratUnit[],
): number | undefined {
const nextIndex: number = index + 1;
if (!surfaces || nextIndex >= surfaces.length) {
return undefined;
}
// If there is a matching base by name, use that. More robust, does not rely on sorting
const baseSurfaceIndex = surfaces.findIndex(
(candidate: Stratigraphy) =>
candidate.isBase && candidate.name === top.name,
);
if (baseSurfaceIndex !== -1) {
return baseSurfaceIndex;
}
for (let i = nextIndex; i < surfaces.length; i++) {
const candidate = surfaces[i];
if (!candidate?.isBase) {
return i;
}
if (isAnchestor(top, candidate, stratColumn)) {
return i;
}
}
return undefined;
}
function isAnchestor(
descendant: Stratigraphy,
candidate: Stratigraphy,
stratColumn: StratUnit[],
): boolean {
const path: StratUnit[] = [];
findStratcolumnUnit(stratColumn, descendant.name, path);
return path.some((p: StratUnit) => candidate.name === p.identifier);
}
function generateSurfaceAreas(
projection: number[][],
surfaces: Stratigraphy[],
stratColumn: StratUnit[],
): SurfaceAreaGrouping {
const areas: SurfaceAreaGrouping = surfaces.reduce(
(acc: SurfaceAreaGrouping, surface: Stratigraphy, i: number) => {
if (!surface.isBase) {
if (!acc[surface.group]) {
acc[surface.group] = [];
}
const baseIndex = findBestMatchingBaseIndex(
surface,
i,
surfaces,
stratColumn,
);
acc[surface.group]?.push({
id: surface.name,
label: surface.name,
color: (surface.unit && getColorFromUnit(surface.unit)) || WHITE,
exclude: surface.visualization === 'none' || !surface.unit,
data: projection.map((p, j) => {
const baseValue =
surface.values[j] != null
? getBaseValue(baseIndex, surfaces, j)
: undefined;
return [p[0]!, surface.values[j]!, baseValue!];
}),
});
}
return acc;
},
{},
);
return areas;
}
// get the value from the surface with the supplied index,
// iterate to next surface if value is null
function getBaseValue(
index: number | undefined,
surfaces: Stratigraphy[],
datapoint: number,
): number | undefined {
if (!surfaces || !index || index >= surfaces.length) {
return undefined;
}
for (let i: number = index; i < surfaces.length; i++) {
if (surfaces[i]?.values[datapoint] != null) {
return surfaces[i]?.values[datapoint];
}
}
return undefined;
}