@equinor/esv-intersection
Version:
Intersection component package with testing and automatic documentation.
377 lines (343 loc) • 9.58 kB
text/typescript
import { Annotation } from '../interfaces';
type Pick = {
pickIdentifier?: string;
confidence: string | null;
depthReferencePoint: string;
md: number;
mdUnit: string;
tvd: number;
};
type PickWithId = {
identifier: string;
} & Pick;
type Unit = {
identifier: string;
top: string;
base: string;
baseAge: number;
topAge: number;
colorR: number;
colorG: number;
colorB: number;
stratUnitLevel: number;
lithologyType: number;
stratUnitParent: number;
};
type UnitDto = {
unitName: string;
topSurface: string;
baseSurface: string;
ageBase: number;
ageTop: number;
color: {
r: number;
g: number;
b: number;
};
level: number;
lithType: number;
parent: number;
};
type PickAndUnit = PickWithId & UnitDto;
type PairedPickAndUnit = {
name: string;
mdEntry: number;
tvdEntry: number;
color: { r: number; g: number; b: number };
level: number;
entryPick: PickAndUnit;
mdExit: number;
tvdExit: number;
exitPick: PickAndUnit;
confidenceEntry: string;
confidenceExit: string;
from?: number;
to?: number;
};
const mapPick = (p: PickWithId, groupName: string): Annotation => ({
title: p.pickIdentifier || p.identifier,
group: groupName,
label: `${p.md} ${p.mdUnit} ${p.depthReferencePoint}`,
color: groupName === 'strat-picks' ? '#227' : 'rgba(0,0,0,0.8)',
md: p.md,
});
function getReferencePicks(picks: PickWithId[]): Annotation[] {
if (!picks) {
return [];
}
return picks.map((p: PickWithId) => mapPick(p, 'ref-picks'));
}
function getEntryPicks(formationPicks: PairedPickAndUnit[]): Annotation[] {
if (!formationPicks) {
return [];
}
return formationPicks
.filter((d: PairedPickAndUnit) => d.entryPick.md === d.from)
.map((p: PairedPickAndUnit) => mapPick(p.entryPick, 'strat-picks'));
}
function getFilteredExitPicks(
formationPicks: PairedPickAndUnit[],
): Annotation[] {
if (!formationPicks) {
return [];
}
return (
formationPicks
.filter(
(d: PairedPickAndUnit) =>
formationPicks.findIndex(
(p: PairedPickAndUnit) =>
Math.abs(p.entryPick.md - d.exitPick.md) < 0.5,
) === -1,
)
.map((p: PairedPickAndUnit) => mapPick(p.exitPick, 'strat-picks'))
// Remove duplicates from unitpicks filling in gaps in formation
.filter(
(obj: Annotation, i: number, array: Annotation[]) =>
i ===
array.findIndex(
(v: Annotation) => v.title === obj.title && v.md === obj.md,
),
)
);
}
export const getPicksData = (picksData: {
unitPicks: PairedPickAndUnit[];
nonUnitPicks: PickWithId[];
}): Annotation[] =>
[
...getReferencePicks(picksData.nonUnitPicks),
...getEntryPicks(picksData.unitPicks),
...getFilteredExitPicks(picksData.unitPicks),
].sort((a, b) => a.md! - b.md!);
/**
* @param {Unit} u
*/
const unitDto = (u: Unit): UnitDto => ({
unitName: u.identifier,
topSurface: u.top,
baseSurface: u.base,
ageBase: u.baseAge,
ageTop: u.topAge,
color: {
r: u.colorR === null ? 255 : u.colorR,
g: u.colorG === null ? 255 : u.colorG,
b: u.colorB === null ? 255 : u.colorB,
},
level: u.stratUnitLevel,
lithType: u.lithologyType,
parent: u.stratUnitParent,
});
/**
*
* @param {number} from
* @param {number} to
* @param {{ from: number; to: number; itm: PairedPickAndUnit }[]} arr
* @param {number} arr.to
* @param {number} arr.from
* @returns {[number, number][]}
*/
function findGaps(
from: number,
to: number,
arr: { from: number; to: number; itm: PairedPickAndUnit }[],
): [number, number][] {
if (arr.length === 0) {
return [[from, to]];
}
const gaps: [number, number][] = [];
let d = from;
let i = 0;
while (d < to && i < arr.length) {
const itm = arr[i]!;
if (itm.from > d) {
gaps.push([d, Math.min(itm.from, to)]);
}
d = Math.min(to, Math.max(from, itm.to));
i += 1;
}
if (d < to) {
gaps.push([d, to]);
}
return gaps;
}
/**
* @param {Unit[]} units
* @returns {UnitDto[]}
*/
const transformStratColumn = (units: Unit[]): UnitDto[] => units.map(unitDto);
/**
* Join picks data with strat column units
* @param {Pick[]} picks picks
* @param {Unit[]} stratColumn strat column
*/
function joinPicksAndStratColumn(
picks: Pick[],
stratColumn: Unit[],
): { joined: PickAndUnit[]; nonUnitPicks: PickWithId[] } {
const transformed = transformStratColumn(stratColumn);
const nonUnitPicks: PickWithId[] = [];
const joined: PickAndUnit[] = [];
picks.forEach((p: Pick) => {
const matches = transformed.filter(
(u: UnitDto) =>
p.pickIdentifier?.search(
new RegExp(`(${u.topSurface}|${u.baseSurface})`, 'i'),
) !== -1,
);
if (matches.length > 0) {
matches.forEach((u: UnitDto) =>
joined.push({
md: p.md,
tvd: p.tvd,
identifier: p.pickIdentifier!,
confidence: p.confidence,
mdUnit: p.mdUnit,
depthReferencePoint: p.depthReferencePoint,
...u,
}),
);
} else {
nonUnitPicks.push({ identifier: p.pickIdentifier!, ...p });
}
});
return { joined, nonUnitPicks };
}
/**
* Find matching pairs of entry/exit picks
* @param {PickAndUnit[]} joined picks joined with strat column units
*/
function pairJoinedPicks(joined: PickAndUnit[]): PairedPickAndUnit[] {
// pair picks by unit name
const pairs = [];
let current = null;
const sorted = joined
.filter((d: PickAndUnit) => d.level)
.sort(
(a: PickAndUnit, b: PickAndUnit) =>
a.unitName.localeCompare(b.unitName) ||
a.md - b.md ||
a.ageTop - b.ageTop,
);
while (sorted.length > 0) {
current = sorted.shift()!;
const name = current.identifier;
let pairWithName: string;
const isTop = name === current.topSurface;
const isBase = name === current.baseSurface;
if (isTop) {
pairWithName = current.baseSurface;
} else if (isBase) {
pairWithName = current.topSurface;
} else {
console.warn(`Unable to match ${name} with top or base surface, ignored`);
continue;
}
let top: PickAndUnit | undefined;
let base: PickAndUnit | undefined;
const pairWith = sorted.find(
(p: PickAndUnit) => p.identifier === pairWithName,
);
if (!pairWith) {
console.warn(`Unable to find ${pairWithName} pick for ${name}`);
if (isTop) {
top = current;
base = joined
.filter((d: PickAndUnit) => d.level)
.sort((a: PickAndUnit, b: PickAndUnit) => a.md - b.md)
.find((p: PickAndUnit) => p.md > top!.md);
if (base) {
console.warn(`Using ${base.identifier} as base for ${name}`);
} else {
console.warn(
`Unable to find a base pick for ${name} pick at ${top.md}, ignored`,
);
continue;
}
} else if (isBase) {
base = current;
top = joined
.filter((d: PickAndUnit) => d.level)
.sort((a: PickAndUnit, b: PickAndUnit) => b.md - a.md)
.find((p: PickAndUnit) => p.md < base!.md);
if (top) {
console.warn(`Using ${top.identifier} as top for ${name}`);
} else {
console.warn(
`Unable to find a top pick for ${name} pick at ${base.md}, ignored`,
);
continue;
}
} else {
console.warn(`${name} ignored`);
continue;
}
} else {
top = isTop ? current : pairWith;
base = isTop ? pairWith : current;
if (top.md > base.md) {
[top, base] = [base, top];
}
sorted.splice(sorted.indexOf(pairWith), 1);
}
pairs.push(<PairedPickAndUnit>{
name: top.unitName,
mdEntry: top.md,
tvdEntry: top.tvd,
color: top.color,
level: top.level,
entryPick: top,
mdExit: base.md,
tvdExit: base.tvd,
exitPick: base,
confidenceEntry: top.confidence,
confidenceExit: base.confidence,
});
}
return pairs;
}
/**
* Transform data for formation track
* @param {Pick[]} picks picks
* @param {Unit[]} stratColumn strat column
*/
export function transformFormationData(
picks: Pick[],
stratColumn: Unit[],
): { unitPicks: PairedPickAndUnit[]; nonUnitPicks: PickWithId[] } {
const { joined, nonUnitPicks } = joinPicksAndStratColumn(picks, stratColumn);
const pairs = pairJoinedPicks(joined);
const itemstack = pairs
.filter((d: PairedPickAndUnit) => d.mdEntry < d.mdExit)
.sort((a, b) => a.mdEntry - b.mdEntry || a.level - b.level)
.reverse();
// flatten groups of unit picks, so that the highest level is
// given presedence over lower levels for overlapping picks.
const unitPicks = [];
while (itemstack.length > 0) {
const first = itemstack.pop()!;
const group: PairedPickAndUnit[] = [];
while (
itemstack.length > 0 &&
itemstack[itemstack.length - 1]?.level! > first.level
) {
group.push(itemstack.pop()!);
}
group.reverse();
group.push(first);
const arr: { from: number; to: number; itm: PairedPickAndUnit }[] = [];
group.forEach((itm: PairedPickAndUnit) => {
const gaps = findGaps(itm.mdEntry, itm.mdExit, arr);
arr.push(...gaps.map(g => ({ from: g[0], to: g[1], itm })));
});
arr.sort((a, b) => a.from - b.from);
unitPicks.push(
...arr.map(d => ({
from: d.from,
to: d.to,
...d.itm,
})),
);
}
return { unitPicks, nonUnitPicks };
}