diff --git a/package.json b/package.json index 08d9498..7bb3011 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "@types/three": "0.148.1", "@types/uuid": "^9.0.0", "@vitejs/plugin-basic-ssl": "^1.0.1", + "color-rgba": "^2.4.0", "geolib": "^3.3.3", + "jq-web": "^0.5.1", "postprocessing": "^6.29.3", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/layers/LabelLayer.tsx b/src/layers/LabelLayer.tsx new file mode 100644 index 0000000..d78555a --- /dev/null +++ b/src/layers/LabelLayer.tsx @@ -0,0 +1,123 @@ +import { UniverseTelemetrySource } from "@formant/universe-core"; +import { useContext, useEffect, useMemo, useState } from "react"; +import { DataVisualizationLayer } from "./DataVisualizationLayer"; +import { IUniverseLayerProps } from "./types"; +import { Label } from "./objects/Label"; +import { UniverseDataContext } from "./common/UniverseDataContext"; +import { LayerContext } from "./common/LayerContext"; +import { defined } from "../common/defined"; +// @ts-ignore +import jq from "jq-web"; +import { parseColor } from "./utils/parseColor"; +interface ILabelLayer extends IUniverseLayerProps { + dataSource?: UniverseTelemetrySource; + color?: string; + textColor?: string; + jsonQuery?: string; + template?: string; + value?: string; + colorStates?: { + [key: string]: { + color: string; + textColor: string; + }; + }; +} + +export function LabelLayer(props: ILabelLayer) { + const { + dataSource, + color, + textColor, + jsonQuery, + template, + value, + colorStates, + } = props; + const [currentValue, setCurrentValue] = useState(value || ""); + const [currentColor] = useState( + color ? parseColor(color) : undefined + ); + const [currentTextColor] = useState( + textColor ? parseColor(textColor) : undefined + ); + const universeData = useContext(UniverseDataContext); + const layerContext = useContext(LayerContext); + + useEffect(() => { + setCurrentValue(value || ""); + }, [value]); + + const label = useMemo(() => { + let extractedValue = currentValue; + if ( + dataSource?.sourceType === "telemetry" && + dataSource.streamType === "json" && + jsonQuery + ) { + try { + extractedValue = jq.json(JSON.parse(currentValue), jsonQuery); + if (typeof extractedValue === "string") { + extractedValue = extractedValue; + } else { + extractedValue = JSON.stringify(extractedValue); + } + } catch (e) { + console.error(e); + extractedValue = "error parsing json"; + } + } + + let evaluatedColor = currentColor; + let evaluatedTextColor = currentTextColor; + if (colorStates) { + const colorState = colorStates[extractedValue]; + if (colorState) { + evaluatedColor = colorState.color; + evaluatedTextColor = colorState.textColor; + } + } + const templatedValue = template + ? template.replace("{}", extractedValue) + : extractedValue; + const label = new Label( + templatedValue, + false, + evaluatedColor ? evaluatedColor : undefined, + evaluatedTextColor ? evaluatedTextColor : undefined + ); + return label; + }, [colorStates, currentValue, color, jsonQuery, template]); + + useEffect(() => { + if (dataSource?.sourceType === "telemetry") { + if (dataSource.streamType === "json") { + const unsub = universeData.subscribeToJson( + defined(layerContext?.deviceId), + dataSource, + (data) => { + if (typeof data === "symbol") return; + setCurrentValue(data as string); + } + ); + return () => unsub(); + } else if (dataSource?.streamType === "text") { + const unsub = universeData.subscribeToText( + defined(layerContext?.deviceId), + dataSource, + (data) => { + if (typeof data === "symbol") return; + setCurrentValue(data as string); + } + ); + return () => unsub(); + } + } + }, [dataSource]); + + return ( + + + + ); +} diff --git a/src/layers/common/ExampleUniverseData.ts b/src/layers/common/ExampleUniverseData.ts index 993bd1f..22d16fe 100644 --- a/src/layers/common/ExampleUniverseData.ts +++ b/src/layers/common/ExampleUniverseData.ts @@ -225,7 +225,8 @@ export class ExampleUniverseData implements IUniverseData { _source: UniverseDataSource, _callback: (data: T | DataStatus) => void ): CloseSubscription { - throw new Error("Method not implemented."); + _callback(JSON.stringify({ abc: "123", xyz: [10, 9, 8] }) as any); + return () => {}; } subscribeToText( @@ -233,7 +234,8 @@ export class ExampleUniverseData implements IUniverseData { _source: UniverseDataSource, _callback: (text: string | DataStatus) => void ): CloseSubscription { - throw new Error("Method not implemented."); + _callback("Hello world!"); + return () => {}; } getStatistics(): Promise { diff --git a/src/layers/objects/Label.ts b/src/layers/objects/Label.ts index 5078ff1..8cf8eeb 100644 --- a/src/layers/objects/Label.ts +++ b/src/layers/objects/Label.ts @@ -1,5 +1,6 @@ import { definedAndNotNull } from "@formant/universe-core"; import { Group, Sprite, SpriteMaterial, Texture } from "three"; +import { FormantColors } from "../utils/FormantColors"; function roundRect( ctx: CanvasRenderingContext2D, @@ -34,7 +35,12 @@ export class Label extends Group { currentText: string; - constructor(text: string, private sizeAttenuate: boolean = true) { + constructor( + text: string, + private sizeAttenuate: boolean = true, + private color: string = FormantColors.module, + private textColor: string = FormantColors.silver + ) { super(); this.currentText = text; this.update(); @@ -71,13 +77,13 @@ export class Label extends Group { textWidth + padding, textHeight + padding, 10, - "#2d3855" + this.color ); context.globalAlpha = 1; // background color context.font = font; - context.fillStyle = "#bac4e2"; + context.fillStyle = this.textColor; context.fillText(message, 0 + 10, fontsize + 10); // canvas contents will be used for a texture diff --git a/src/layers/utils/parseColor.ts b/src/layers/utils/parseColor.ts new file mode 100644 index 0000000..3411064 --- /dev/null +++ b/src/layers/utils/parseColor.ts @@ -0,0 +1,7 @@ +// @ts-ignore +import parse from "color-rgba"; + +export function parseColor(color: string): string { + const vals = parse(color); + return `rgba(${vals[0]}, ${vals[1]}, ${vals[2]}, ${vals[3]})`; +} diff --git a/tsconfig.json b/tsconfig.json index 3d0a51a..3bc5285 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "typeRoots": ["./types", "./node_modules/@types"] }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/types/color-rgba.d.ts b/types/color-rgba.d.ts new file mode 100644 index 0000000..d5658cb --- /dev/null +++ b/types/color-rgba.d.ts @@ -0,0 +1,8 @@ +// expose types for jq-web +declare module "jq-web" { + export function json(data: any, query: string): any; +} + +declare module "color-rgba" { + export function parse(color: string): [number, number, number, number]; +} diff --git a/yarn.lock b/yarn.lock index e48a5ba..46970f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1112,6 +1112,31 @@ color-name@1.1.3: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-name@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-parse@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/color-parse/-/color-parse-1.4.2.tgz#78651f5d34df1a57f997643d86f7f87268ad4eb5" + integrity sha512-RI7s49/8yqDj3fECFZjUI1Yi0z/Gq1py43oNJivAIIDSyJiOZLfYCRQEgn8HEVAj++PcRe8AnL2XF0fRJ3BTnA== + dependencies: + color-name "^1.0.0" + +color-rgba@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-2.4.0.tgz#ae85819c530262c29fc2da129fc7c8f9efc57015" + integrity sha512-Nti4qbzr/z2LbUWySr7H9dk3Rl7gZt7ihHAxlgT4Ho90EXWkjtkL1avTleu9yeGuqrt/chxTB6GKK8nZZ6V0+Q== + dependencies: + color-parse "^1.4.2" + color-space "^2.0.0" + +color-space@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/color-space/-/color-space-2.0.0.tgz#ae7813abcbe3dabda9e3e2266b0675f688b24977" + integrity sha512-Bu8P/usGNuVWushjxcuaGSkhT+L2KX0cvgMGMTF0KJ7lFeqonhsntT68d6Yu3uwZzCmbF7KTB9EV67AGcUXhJw== + convert-source-map@^1.5.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" @@ -1388,6 +1413,11 @@ its-fine@^1.0.6: dependencies: "@types/react-reconciler" "^0.28.0" +jq-web@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/jq-web/-/jq-web-0.5.1.tgz#4456cd3be4c2688c43f287b1fec2e3ea29e427e3" + integrity sha512-3Fa3E6g3U1O1j46ljy0EM10yRr4txzILga8J7bqOG8F89gZ6Lilz82WG9z6TItWpYEO0YGa4W8yFGj+NMM1xqQ== + js-md5@^0.7.3: version "0.7.3" resolved "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz"