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

CARTO: Automatically add labels to line & polygon layers #9449

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
23 changes: 3 additions & 20 deletions modules/carto/src/layers/cluster-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {cellToParent} from 'quadbin';
import {_Tile2DHeader as Tile2DHeader} from '@deck.gl/geo-layers';
import {Accessor, log} from '@deck.gl/core';
import {BinaryFeatureCollection} from '@loaders.gl/schema';
import {createBinaryPointFeature, createEmptyBinary} from '../utils';

export type Aggregation = 'any' | 'average' | 'count' | 'min' | 'max' | 'sum';
export type AggregationProperties<FeaturePropertiesT> = {
Expand Down Expand Up @@ -168,25 +169,7 @@ export function clustersToBinary<FeaturePropertiesT>(
}

return {
shape: 'binary-feature-collection',
points: {
type: 'Point',
positions: {value: positions, size: 2},
properties: data,
numericProps: {},
featureIds: {value: featureIds, size: 1},
globalFeatureIds: {value: featureIds, size: 1}
},
lines: {
type: 'LineString',
pathIndices: {value: EMPTY_UINT16ARRAY, size: 1},
...EMPTY_BINARY_PROPS
},
polygons: {
type: 'Polygon',
polygonIndices: {value: EMPTY_UINT16ARRAY, size: 1},
primitivePolygonIndices: {value: EMPTY_UINT16ARRAY, size: 1},
...EMPTY_BINARY_PROPS
}
...createEmptyBinary(),
points: createBinaryPointFeature(positions, featureIds, featureIds, {}, data)
};
}
302 changes: 302 additions & 0 deletions modules/carto/src/layers/label-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
import {GeoBoundingBox} from '@deck.gl/geo-layers';
import {TypedArray} from '@loaders.gl/loader-utils';
import {BinaryPointFeature, BinaryLineFeature, BinaryPolygonFeature} from '@loaders.gl/schema';
import {copyNumericProps, createBinaryPointFeature, initializeNumericProps} from '../utils';

type Vec2 = [number, number] | TypedArray;
type TileBBox = GeoBoundingBox;
type Properties = BinaryPointFeature['properties'];
type LineInfo = {index: number; length: number};

export function createPointsFromLines(
lines: BinaryLineFeature,
uniqueIdProperty?: string
): BinaryPointFeature | null {
const hasNumericUniqueId = uniqueIdProperty ? uniqueIdProperty in lines.numericProps : false;
const idToLineInfo = new Map<string | number | undefined, LineInfo>();

// First pass: find the longest line for each unique ID
// If we don't have a uniqueIdProperty, treat each line as unique
for (let i = 0; i < lines.pathIndices.value.length - 1; i++) {
const pathIndex = lines.pathIndices.value[i];
const featureId = lines.featureIds.value[pathIndex];
let uniqueId: string | number | undefined;

if (uniqueIdProperty === undefined) {
uniqueId = featureId;
} else if (hasNumericUniqueId) {
uniqueId = lines.numericProps[uniqueIdProperty].value[pathIndex];
} else if (lines.properties[featureId] && uniqueIdProperty in lines.properties[featureId]) {
uniqueId = lines.properties[featureId][uniqueIdProperty];
} else {
uniqueId = undefined;
}
const length = getLineLength(lines, i);
if (!idToLineInfo.has(uniqueId) || length > idToLineInfo.get(uniqueId)!.length) {
idToLineInfo.set(uniqueId, {index: i, length});
}
}

const positions: number[] = [];
const properties: Properties = [];
const featureIds: number[] = [];
const globalFeatureIds: number[] = [];
const numericProps = initializeNumericProps(idToLineInfo.size, lines.numericProps);

// Second pass: create points for the longest line of each unique ID
let pointIndex = 0;
for (const [_, {index}] of idToLineInfo) {
const midpoint = getLineMidpoint(lines, index);
positions.push(...midpoint);

const pathIndex = lines.pathIndices.value[index];
const featureId = lines.featureIds.value[pathIndex];
featureIds.push(pointIndex);
properties.push(lines.properties[featureId]);
globalFeatureIds.push(lines.globalFeatureIds.value[pathIndex]);
copyNumericProps(lines.numericProps, numericProps, pathIndex, pointIndex);
pointIndex++;
}

return createBinaryPointFeature(
positions,
featureIds,
globalFeatureIds,
numericProps,
properties
);
}

export function createPointsFromPolygons(
polygons: Required<BinaryPolygonFeature>,
tileBbox: TileBBox,
props: any
): BinaryPointFeature {
const {west, south, east, north} = tileBbox;
const tileArea = (east - west) * (north - south);
const minPolygonArea = tileArea * 0.0001; // 0.1% threshold

const positions: number[] = [];
const properties: Properties = [];
const featureIds: number[] = [];
const globalFeatureIds: number[] = [];
const numericProps = initializeNumericProps(
polygons.polygonIndices.value.length - 1,
polygons.numericProps
);

// Process each polygon
let pointIndex = 0;
let triangleIndex = 0;
const {extruded} = props;
for (let i = 0; i < polygons.polygonIndices.value.length - 1; i++) {
const startIndex = polygons.polygonIndices.value[i];
const endIndex = polygons.polygonIndices.value[i + 1];

// Skip small polygons
if (getPolygonArea(polygons, i) < minPolygonArea) {
continue;
}

const centroid = getPolygonCentroid(polygons, i);
let maxArea = -1;
let largestTriangleCenter: [number, number] = [0, 0];
let centroidIsInside = false;

// Scan triangles until we find ones that don't belong to this polygon
while (triangleIndex < polygons.triangles.value.length) {
const i1 = polygons.triangles.value[triangleIndex];

// If we've moved past the current polygon's triangles, break
if (i1 >= endIndex) {
break;
}

// If we've already found a triangle containing the centroid, skip the rest
if (centroidIsInside) {
triangleIndex += 3;
continue;
}

const i2 = polygons.triangles.value[triangleIndex + 1];
const i3 = polygons.triangles.value[triangleIndex + 2];
const v1 = polygons.positions.value.subarray(
i1 * polygons.positions.size,
i1 * polygons.positions.size + polygons.positions.size
);
const v2 = polygons.positions.value.subarray(
i2 * polygons.positions.size,
i2 * polygons.positions.size + polygons.positions.size
);
const v3 = polygons.positions.value.subarray(
i3 * polygons.positions.size,
i3 * polygons.positions.size + polygons.positions.size
);

if (isPointInTriangle(centroid, v1, v2, v3)) {
centroidIsInside = true;
} else {
const area = getTriangleArea(v1, v2, v3);
if (area > maxArea) {
maxArea = area;
largestTriangleCenter = [(v1[0] + v2[0] + v3[0]) / 3, (v1[1] + v2[1] + v3[1]) / 3];
}
}

triangleIndex += 3;
}

const labelPoint = centroidIsInside ? centroid : largestTriangleCenter;
if (isPointInBounds(labelPoint, tileBbox)) {
positions.push(...labelPoint);
const featureId = polygons.featureIds.value[startIndex];
if (extruded) {
const elevation = props.getElevation(undefined, {
data: polygons,
index: featureId
});
positions.push(elevation * props.elevationScale);
}
properties.push(polygons.properties[featureId]);
featureIds.push(pointIndex);
globalFeatureIds.push(polygons.globalFeatureIds.value[startIndex]);
copyNumericProps(polygons.numericProps, numericProps, startIndex, pointIndex);
pointIndex++;
}
}

// Trim numeric properties arrays to actual size
if (polygons.numericProps) {
Object.keys(numericProps).forEach(prop => {
numericProps[prop].value = numericProps[prop].value.slice(0, pointIndex);
});
}

return createBinaryPointFeature(
positions,
featureIds,
globalFeatureIds,
numericProps,
properties,
extruded ? 3 : 2
);
}

// Helper functions
function getPolygonArea(polygons: Required<BinaryPolygonFeature>, index: number): number {
const {
positions: {value: positions, size},
polygonIndices: {value: indices},
triangles: {value: triangles}
} = polygons;

const startIndex = indices[index];
const endIndex = indices[index + 1];
let area = 0;
let triangleIndex = 0;

// Find first triangle of this polygon
while (triangleIndex < triangles.length) {
const i1 = triangles[triangleIndex];
if (i1 >= startIndex) break;
triangleIndex += 3;
}

// Process triangles until we hit the next polygon
while (triangleIndex < triangles.length) {
const i1 = triangles[triangleIndex];
if (i1 >= endIndex) break;

const i2 = triangles[triangleIndex + 1];
const i3 = triangles[triangleIndex + 2];
const v1 = positions.subarray(i1 * size, i1 * size + size);
const v2 = positions.subarray(i2 * size, i2 * size + size);
const v3 = positions.subarray(i3 * size, i3 * size + size);

area += getTriangleArea(v1, v2, v3);
triangleIndex += 3;
}

return area;
}

function isPointInBounds([x, y]: [number, number], {west, east, south, north}: TileBBox): boolean {
return x >= west && x < east && y >= south && y < north;
}

function isPointInTriangle(p: Vec2, v1: Vec2, v2: Vec2, v3: Vec2): boolean {
const area = Math.abs((v2[0] - v1[0]) * (v3[1] - v1[1]) - (v3[0] - v1[0]) * (v2[1] - v1[1])) / 2;
const area1 = Math.abs((v1[0] - p[0]) * (v2[1] - p[1]) - (v2[0] - p[0]) * (v1[1] - p[1])) / 2;
const area2 = Math.abs((v2[0] - p[0]) * (v3[1] - p[1]) - (v3[0] - p[0]) * (v2[1] - p[1])) / 2;
const area3 = Math.abs((v3[0] - p[0]) * (v1[1] - p[1]) - (v1[0] - p[0]) * (v3[1] - p[1])) / 2;

// Account for floating point precision
return Math.abs(area - (area1 + area2 + area3)) < 1e-10;
}

function getTriangleArea([x1, y1]: Vec2, [x2, y2]: Vec2, [x3, y3]: Vec2): number {
return Math.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2);
}

function getPolygonCentroid(polygons: BinaryPolygonFeature, index: number): [number, number] {
const {
positions: {value: positions, size}
} = polygons;
const startIndex = size * polygons.polygonIndices.value[index];
const endIndex = size * polygons.polygonIndices.value[index + 1];

let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;

for (let i = startIndex; i < endIndex; i += size) {
const [x, y] = positions.subarray(i, i + 2);
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}

return [(minX + maxX) / 2, (minY + maxY) / 2];
}

function getSegmentLength(lines: BinaryLineFeature, index: number): number {
const {
positions: {value}
} = lines;
const [x1, y1, x2, y2] = value.subarray(index, index + 4);
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}

function getLineLength(lines: BinaryLineFeature, index: number): number {
const {
positions: {size}
} = lines;
const startIndex = size * lines.pathIndices.value[index];
const endIndex = size * lines.pathIndices.value[index + 1];
let length = 0;
for (let j = startIndex; j < endIndex; j += size) {
length += getSegmentLength(lines, j);
}
return length;
}

function getLineMidpoint(lines: BinaryLineFeature, index: number): [number, number] {
const {
positions: {value: positions},
pathIndices: {value: pathIndices}
} = lines;
const startIndex = pathIndices[index] * 2;
const endIndex = pathIndices[index + 1] * 2;
const numPoints = (endIndex - startIndex) / 2;

if (numPoints === 2) {
// For lines with only two vertices, interpolate between them
const [x1, y1, x2, y2] = positions.subarray(startIndex, startIndex + 4);
return [(x1 + x2) / 2, (y1 + y2) / 2];
}
// For lines with multiple vertices, use the middle vertex
const midPointIndex = startIndex + Math.floor(numPoints / 2) * 2;
return [positions[midPointIndex], positions[midPointIndex + 1]];
}
4 changes: 3 additions & 1 deletion modules/carto/src/layers/schema/carto-tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Copyright (c) vis.gl contributors

import {readPackedTypedArray} from './fast-pbf';
import {TypedArray} from '@loaders.gl/loader-utils';

// KeyValueObject ========================================
interface KeyValueObject {
Expand Down Expand Up @@ -88,7 +89,8 @@ class FieldsReader {
// NumericProp ========================================

export interface NumericProp {
value: number[];
value: TypedArray;
size: number;
}

class NumericPropReader {
Expand Down
7 changes: 4 additions & 3 deletions modules/carto/src/layers/schema/spatialjson-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
// Copyright (c) vis.gl contributors

import {bigIntToHex} from 'quadbin';
import {BinaryPointFeature} from '@loaders.gl/schema';

export type IndexScheme = 'h3' | 'quadbin';
type TypedArray = Float32Array | Float64Array;

export type Indices = {value: BigUint64Array};
export type NumericProps = Record<string, {value: number[] | TypedArray}>;
export type Properties = Record<string, string | number | boolean | null>;
export type NumericProps = BinaryPointFeature['numericProps'];
export type Properties = BinaryPointFeature['properties'];
export type Cells = {
indices: Indices;
numericProps: NumericProps;
properties: Properties[];
properties: Properties;
};
export type SpatialBinary = {scheme?: IndexScheme; cells: Cells};
export type SpatialJson = {
Expand Down
Loading
Loading