From 19e8ddd38de071123258bc0d655e56a106bbd2c7 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 17 Jan 2024 20:18:08 +0100 Subject: [PATCH] support track filters in MultiLineStrings --- .../filterTrackByCoordinateProperties.ts | 264 +++++++++++++----- .../src/generators/track/track.ts | 76 ++--- 2 files changed, 218 insertions(+), 122 deletions(-) diff --git a/libs/layer-composer/src/generators/track/filterTrackByCoordinateProperties.ts b/libs/layer-composer/src/generators/track/filterTrackByCoordinateProperties.ts index 28f96539e4..1663d8bd06 100644 --- a/libs/layer-composer/src/generators/track/filterTrackByCoordinateProperties.ts +++ b/libs/layer-composer/src/generators/track/filterTrackByCoordinateProperties.ts @@ -29,7 +29,167 @@ type FilterTrackByCoordinatePropertiesFn = ( ...args: FilterTrackByCoordinatePropertiesArgs ) => FeatureCollection -// TODO TS types wont work with MultiPoint geoms +type LineCoordinateProperties = Record +type MultiLineCoordinateProperties = Record +type CoordinateProperties = LineCoordinateProperties | MultiLineCoordinateProperties +type CoordinatesAccumulator = { + coordinates: Position[][] + coordinateProperties: CoordinateProperties +} + +type GetCoordinatePropertyValueParams = { + coordinateProperties: CoordinateProperties + coordinateIndex: number + multiLineStringIndex?: number + id: string +} +const getCoordinatePropertyValue = ({ + id, + coordinateProperties, + coordinateIndex, + multiLineStringIndex, +}: GetCoordinatePropertyValueParams) => { + return multiLineStringIndex !== undefined + ? (coordinateProperties as MultiLineCoordinateProperties)?.[id][multiLineStringIndex][ + coordinateIndex + ] + : (coordinateProperties as LineCoordinateProperties)?.[id][coordinateIndex] +} + +type AddPropertyIndexToCoordinateParams = { + id: string + coordinateIndex: number + coordinateValue: string | number + coordinates: CoordinatesAccumulator +} +const addCoordinatePropertyToCoordinate = ({ + id, + coordinateIndex, + coordinateValue, + coordinates, +}: AddPropertyIndexToCoordinateParams) => { + if (!coordinates.coordinateProperties[id]) { + coordinates.coordinateProperties[id] = [] + } + if (!coordinates.coordinateProperties[id][coordinateIndex]) { + coordinates.coordinateProperties[id][coordinateIndex] = [] + } + ;(coordinates.coordinateProperties as MultiLineCoordinateProperties)[id][coordinateIndex].push( + coordinateValue + ) +} + +type GetFilteredCoordinatesParams = { + coordinates: Position[] + filters: TrackCoordinatesPropertyFilter[] + coordinateProperties: CoordinateProperties + multiLineStringIndex?: number +} +const getFilteredCoordinates = ({ + coordinates, + filters, + coordinateProperties, + multiLineStringIndex, +}: GetFilteredCoordinatesParams) => { + let leadingPoint = true + const filteredLines = coordinates.reduce( + (filteredCoordinates, coordinate, index) => { + const matchesPropertyFilters = filters.every(({ id, min, max, values }) => { + const currentValue = getCoordinatePropertyValue({ + id, + coordinateProperties, + coordinateIndex: index, + multiLineStringIndex, + }) + if (min !== undefined && max !== undefined) { + return (currentValue as number) >= min && (currentValue as number) <= max + } + if (values?.length) { + return values.includes(currentValue) + } + return true + }) + const coordinatesIndex = filteredCoordinates.coordinates.length + ? filteredCoordinates.coordinates.length - 1 + : 0 + if (matchesPropertyFilters) { + if (leadingPoint && index > 0) { + leadingPoint = false + const leadingIndex = index - 1 + const leadingCoordinatePoint = coordinates[leadingIndex] + if (!filteredCoordinates.coordinates[coordinatesIndex]) { + filteredCoordinates.coordinates[coordinatesIndex] = [] + } + filteredCoordinates.coordinates[coordinatesIndex].push(leadingCoordinatePoint) + filters.forEach(({ id }) => { + const leadingCoordinateValue = getCoordinatePropertyValue({ + id, + coordinateProperties, + coordinateIndex: leadingIndex, + multiLineStringIndex, + }) + addCoordinatePropertyToCoordinate({ + id, + coordinates: filteredCoordinates, + coordinateIndex: coordinatesIndex, + coordinateValue: leadingCoordinateValue, + }) + }) + } + if (!filteredCoordinates.coordinates[coordinatesIndex]) { + filteredCoordinates.coordinates[coordinatesIndex] = [] + } + filteredCoordinates.coordinates[coordinatesIndex].push(coordinate) + filters.forEach(({ id }) => { + const coordinateValue = getCoordinatePropertyValue({ + id, + coordinateProperties, + coordinateIndex: index, + multiLineStringIndex, + }) + addCoordinatePropertyToCoordinate({ + id, + coordinates: filteredCoordinates, + coordinateIndex: coordinatesIndex, + coordinateValue, + }) + }) + } else if (filteredCoordinates.coordinates[coordinatesIndex]?.length) { + filteredCoordinates.coordinates.push([]) + } + + return filteredCoordinates + }, + { coordinates: [], coordinateProperties: {} } as CoordinatesAccumulator + ) + return filteredLines +} + +const getFilteredLines = ( + feature: Feature, + filters: TrackCoordinatesPropertyFilter[] +) => { + const isMultiLineString = feature.geometry.type === 'MultiLineString' + const coordinateProperties = feature.properties?.coordinateProperties + const lines = isMultiLineString + ? (feature.geometry as MultiLineString).coordinates.map((coordinates, multiLineStringIndex) => + getFilteredCoordinates({ + coordinates, + filters, + coordinateProperties, + multiLineStringIndex, + }) + ) + : [ + getFilteredCoordinates({ + coordinates: (feature.geometry as LineString).coordinates, + filters, + coordinateProperties, + }), + ] + return lines.filter((l) => l.coordinates.length) +} + export const filterTrackByCoordinateProperties: FilterTrackByCoordinatePropertiesFn = ( geojson, { @@ -37,12 +197,12 @@ export const filterTrackByCoordinateProperties: FilterTrackByCoordinatePropertie includeNonTemporalFeatures = false, } = {} as FilterTrackByCoordinatePropertiesParams ): FeatureCollection => { - if (!geojson || !geojson.features) + if (!geojson || !geojson.features) { return { type: 'FeatureCollection', features: [], } - let leadingPoint = true + } const featuresFiltered: Feature[] = geojson.features.reduce( (filteredFeatures: Feature[], feature) => { @@ -51,84 +211,34 @@ export const filterTrackByCoordinateProperties: FilterTrackByCoordinatePropertie .map((p) => p.id) .some((id) => feature?.properties?.coordinateProperties?.[id]?.length > 0) if (hasValues) { - const filteredLines = (feature.geometry as LineString).coordinates.reduce( - (filteredCoordinates, coordinate, index) => { - const matchesPropertyFilters = filters.every(({ id, min, max, values }) => { - const currentValue: number = feature.properties?.coordinateProperties?.[id][index] - if (min !== undefined && max !== undefined) { - return currentValue >= min && currentValue <= max - } - if (values?.length) { - return values.includes(currentValue) - } - return true - }) - // TODO generate a new segment when false so we can cut by properties without generating non existing lines - if (matchesPropertyFilters) { - const coordinatesIndex = filteredCoordinates.coordinates.length - ? filteredCoordinates.coordinates.length - 1 - : 0 - if (leadingPoint && index > 0) { - leadingPoint = false - const leadingIndex = index - 1 - const leadingCoordinatePoint = (feature.geometry as LineString).coordinates[ - leadingIndex - ] - if (!filteredCoordinates.coordinates[coordinatesIndex]) { - filteredCoordinates.coordinates[coordinatesIndex] = [] - } - filteredCoordinates.coordinates[coordinatesIndex].push(leadingCoordinatePoint) - filters.forEach(({ id }) => { - const leadingCoordinateValue: string | number = - feature.properties?.coordinateProperties?.[id][leadingIndex] - if (!filteredCoordinates.coordinateProperties[id]) { - filteredCoordinates.coordinateProperties[id] = [] - } - filteredCoordinates.coordinateProperties[id].push(leadingCoordinateValue) - }) - } - if (!filteredCoordinates.coordinates[coordinatesIndex]) { - filteredCoordinates.coordinates[coordinatesIndex] = [] - } - filteredCoordinates.coordinates[coordinatesIndex].push(coordinate) - filters.forEach(({ id }) => { - const coordinateValue: string | number = - feature.properties?.coordinateProperties?.[id][index] - if (!filteredCoordinates.coordinateProperties[id]) { - filteredCoordinates.coordinateProperties[id] = [] - } - filteredCoordinates.coordinateProperties[id].push(coordinateValue) - }) - } else { - filteredCoordinates.coordinates.push([]) - } - - return filteredCoordinates - }, - { - coordinates: [] as Position[][], - coordinateProperties: {} as Record, - } - ) + const filteredLines = getFilteredLines(feature, filters) - if (!filteredLines.coordinates.length) return filteredFeatures - - const geometry: MultiLineString = { - type: 'MultiLineString', - coordinates: filteredLines.coordinates.filter((c) => c.length > 1), + if (!filteredLines.length) { + return filteredFeatures } - const properties: GeoJsonProperties = { - ...feature.properties, - coordinateProperties: filteredLines.coordinateProperties, - } + const coordinateProperties = filters.reduce( + (acc, { id }) => { + const properties = filteredLines.flatMap( + (line) => (line.coordinateProperties as MultiLineCoordinateProperties)[id] + ) + acc[id] = properties + return acc + }, + {} as Record + ) - const filteredFeature: Feature = { - ...feature, - geometry, - properties, - } - filteredFeatures.push(filteredFeature) + filteredFeatures.push({ + type: 'Feature', + geometry: { + type: 'MultiLineString', + coordinates: filteredLines.flatMap((line) => line.coordinates), + } as MultiLineString, + properties: { + ...feature.properties, + coordinateProperties, + } as GeoJsonProperties, + }) } else if (includeNonTemporalFeatures) { filteredFeatures.push(feature) } diff --git a/libs/layer-composer/src/generators/track/track.ts b/libs/layer-composer/src/generators/track/track.ts index 5ff2d89e10..a889316e05 100644 --- a/libs/layer-composer/src/generators/track/track.ts +++ b/libs/layer-composer/src/generators/track/track.ts @@ -44,29 +44,17 @@ const simplifyTrackWithZoomLevel = ( return simplifiedData } -// const filterByTimerange = (data: FeatureCollection, filters: TrackCoordinatesPropertyFilter[]) => { -// const filteredData = filterTrackByCoordinateProperties(data, { -// filters, -// includeNonTemporalFeatures: true, -// }) -// return filteredData -// } - -const getHighlightedData = ( - data: FeatureCollection, - highlightedStart: string, - highlightedEnd: string -) => { - const startMs = new Date(highlightedStart).getTime() - const endMs = new Date(highlightedEnd).getTime() - - const filters = [ - { id: 'times', min: startMs, max: endMs }, - // { id: 'speed', min: 3, max: 20 }, +const getTimeFilter = (start?: string, end?: string): TrackCoordinatesPropertyFilter[] => { + if (!start || !end) { + return [] + } + return [ + { + id: 'times', + min: new Date(start).getTime(), + max: new Date(end).getTime(), + }, ] - const filteredData = filterTrackByCoordinateProperties(data, { filters }) - - return filteredData } const getHighlightedLayer = ( @@ -134,28 +122,19 @@ class TrackGenerator { .filter((f: any) => f.properties?.id !== undefined) .map((f: any) => f.properties?.id) ) - let propertiesFilter: TrackCoordinatesPropertyFilter[] = Object.entries( + + const propertiesFilter: TrackCoordinatesPropertyFilter[] = Object.entries( config.filters || {} ).map(([id, values]) => ({ id, min: parseFloat(values[0] as string), max: parseFloat(values[1] as string), })) - // TODO improve memoization for the filters array needed here - if (config.start && config.end) { - const startMs = new Date(config.start).getTime() - const endMs = new Date(config.end).getTime() - propertiesFilter.push({ id: 'times', min: startMs, max: endMs }) - } - console.log('🚀 ~ source.data:', source.data) - if (propertiesFilter.length > 0) { - console.log('🚀 ~ propertiesFilter:', propertiesFilter) - source.data = memoizeCache[config.id].filterTrackByCoordinateProperties(source.data, { - filter: propertiesFilter, - includeNonTemporalFeatures: true, - }) - } - console.log('🚀 ~ source.data:', source.data) + + source.data = memoizeCache[config.id].filterTrackByCoordinateProperties(source.data, { + filters: [...getTimeFilter(config.start, config.end), ...propertiesFilter], + includeNonTemporalFeatures: true, + }) // if (config.highlightedEvent) { // const highlightedData = memoizeCache[config.id].getHighlightedEventData( @@ -172,10 +151,17 @@ class TrackGenerator { // } if (config.highlightedTime) { - const highlightedData = memoizeCache[config.id].getHighlightedData( + const highlightedData = memoizeCache[config.id].filterTrackByCoordinatePropertiesHighlight( + // using source.data here to avoid filtering the entire track again + // this makes everything much faster but also harder because the filterTrackByCoordinateProperties + // needs support to filter LineStrings and also source.data, - config.highlightedTime.start, - config.highlightedTime.end + { + filters: [ + ...getTimeFilter(config.highlightedTime.start, config.highlightedTime.end), + ...propertiesFilter, + ], + } ) const highlightedSource = { id: `${config.id}${this.highlightSufix}`, @@ -210,7 +196,6 @@ class TrackGenerator { // } // }) // } - // console.log('🚀 ~ Object.entries ~ filters:', filters) if (uniqIds.length > 1) { let exprLineColor @@ -283,9 +268,10 @@ class TrackGenerator { filterTrackByCoordinateProperties, filterByTimerangeMemoizeEqualityCheck ), - // TODO: the same for filters memoization here - getHighlightedData: memoizeOne(getHighlightedData), - getHighlightedEventData: memoizeOne(getHighlightedData), + filterTrackByCoordinatePropertiesHighlight: memoizeOne( + filterTrackByCoordinateProperties, + filterByTimerangeMemoizeEqualityCheck + ), }) const { sources, uniqIds } = this._getStyleSources(config)