diff --git a/src/components/Universe/Controls/CameraAnimations/index.ts b/src/components/Universe/Controls/CameraAnimations/index.ts
index e9f04569f..bc8fca123 100644
--- a/src/components/Universe/Controls/CameraAnimations/index.ts
+++ b/src/components/Universe/Controls/CameraAnimations/index.ts
@@ -24,7 +24,7 @@ export const useCameraAnimations = (
const isUserDragging = useControlStore((s) => s.isUserDragging)
- const { graphStyle, graphRadius, disableCameraRotation } = useGraphStore((s) => s)
+ const { graphRadius, disableCameraRotation } = useGraphStore((s) => s)
useEffect(() => {
if (!enabled) {
@@ -33,22 +33,24 @@ export const useCameraAnimations = (
}
}, [enabled])
- useEffect(() => {
- if (cameraControlsRef.current && graphRadius) {
- if (graphStyle === 'sphere') {
- cameraControlsRef.current.maxDistance = 8000
- cameraControlsRef.current.minDistance = 200
- cameraControlsRef.current.setTarget(0, 0, 500, true)
- } else {
- cameraControlsRef.current.maxDistance = cameraControlsRef.current.getDistanceToFitSphere(graphRadius + 200)
- cameraControlsRef.current.minDistance = 100
- }
- }
- }, [graphRadius, graphStyle, cameraControlsRef])
+ // useEffect(() => {
+ // if (cameraControlsRef.current && graphRadius) {
+ // cameraControlsRef.current.maxDistance = cameraControlsRef.current.getDistanceToFitSphere(graphRadius + 200)
+ // cameraControlsRef.current.minDistance = 100
+ // }
+ // }, [graphRadius, cameraControlsRef])
useEffect(() => {
if (!selectedNode && cameraControlsRef.current) {
- cameraControlsRef.current.setLookAt(initialCameraPosition.x, initialCameraPosition.y, graphRadius, 0, 0, 0, true)
+ cameraControlsRef.current.setLookAt(
+ initialCameraPosition.x,
+ initialCameraPosition.y,
+ graphRadius + 200,
+ 0,
+ 0,
+ 0,
+ true,
+ )
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedNode, graphRadius])
diff --git a/src/components/Universe/Graph/Connections/LineComponent.tsx b/src/components/Universe/Graph/Connections/LineComponent.tsx
index 8c9b9177a..92737f371 100644
--- a/src/components/Universe/Graph/Connections/LineComponent.tsx
+++ b/src/components/Universe/Graph/Connections/LineComponent.tsx
@@ -5,6 +5,7 @@ import { memo, useEffect, useRef } from 'react'
import { Line2 } from 'three-stdlib'
import { useGraphStore } from '~/stores/useGraphStore'
import { LINE_WIDTH } from '../../constants'
+import { fontProps } from '../Cubes/Text/constants'
type LineComponentProps = {
label: string
@@ -78,8 +79,8 @@ const _LineComponent = (props: LineComponentProps) => {
points={[sourceX, sourceY, sourceZ, targetX, targetY, targetZ]}
/>
-
- {label}1
+
+ {label}
diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/Connection/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/Connection/index.tsx
index 7372715c3..8c5f202d5 100644
--- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/Connection/index.tsx
+++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/Connection/index.tsx
@@ -1,6 +1,7 @@
import { Line, Text } from '@react-three/drei'
import { memo, useRef } from 'react'
import { Line2 } from 'three-stdlib'
+import { fontProps } from '../../../Text/constants'
type LineComponentProps = {
label: string
@@ -29,7 +30,9 @@ const _Connection = (props: LineComponentProps) => {
points={[sourceX, sourceY, sourceZ, targetX, targetY, targetZ]}
/>
-
+
+
+
{label}
diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Node/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Node/index.tsx
index 3f04d8abf..1f07b1446 100644
--- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Node/index.tsx
+++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Node/index.tsx
@@ -57,12 +57,16 @@ export const Node = ({ onClick, node, selected, rounded = true }: Props) => {
)
}
-const Wrapper = styled(Flex)``
+const Wrapper = styled(Flex)`
+ background: black;
+`
const Text = styled(Flex)`
color: ${colors.white};
margin-left: 16px;
font-weight: 700;
+ width: 100px;
+ font-size: 16px;
`
const Tag = styled(Flex)`
@@ -90,8 +94,11 @@ const Tag = styled(Flex)`
`
const Selected = styled(Tag)`
- width: 300px;
- height: 150px;
+ width: 200px;
+ height: 100px;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
`
const IconButton = styled(Flex)`
diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx
index 4c4e693e2..b8e52c5fe 100644
--- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx
+++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx
@@ -1,25 +1,26 @@
import { Html } from '@react-three/drei'
import { forceLink, forceManyBody, forceRadial, forceSimulation } from 'd3-force-3d'
-import { memo, useCallback, useEffect, useRef, useState } from 'react'
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Box3, Color, Group, Sphere, Vector3 } from 'three'
import { Line2 } from 'three-stdlib'
import { useShallow } from 'zustand/react/shallow'
import { usePrevious } from '~/hooks/usePrevious'
+import { fetchNodeEdges } from '~/network/fetchGraphData'
import { useDataStore } from '~/stores/useDataStore'
import { useGraphStore, useSelectedNode, useSelectedNodeRelativeIds } from '~/stores/useGraphStore'
import { useSchemaStore } from '~/stores/useSchemaStore'
import { ForceSimulation } from '~/transformers/forceSimulation'
-import { GraphData, Link, NodeExtended } from '~/types'
+import { GraphData, Link, Node, NodeExtended } from '~/types'
import { LinkPosition } from '../..'
import { Connections } from './Connections'
-import { Node } from './Node'
+import { Node as GraphNode } from './Node'
const MAX_LENGTH = 6
export const SelectionDataNodes = memo(() => {
const [simulation2d, setSimulation2D] = useState(null)
- const { dataInitial } = useDataStore((s) => s)
+ const { dataInitial, nodesNormalized, addNewNode } = useDataStore((s) => s)
const selectedNode = useSelectedNode()
const groupRef = useRef(null)
@@ -31,28 +32,87 @@ export const SelectionDataNodes = memo(() => {
const { normalizedSchemasByType } = useSchemaStore((s) => s)
- const { selectionGraphData, setSelectionData, setSelectedNode, setSelectionGraphRadius } = useGraphStore(
- useShallow((s) => s),
- )
+ const { selectionGraphData, selectionPath, setSelectionData, setSelectedNode, setSelectionGraphRadius } =
+ useGraphStore(useShallow((s) => s))
+
+ const pathNodes = useMemo(() => {
+ const nodes: NodeExtended[] = selectionPath
+ .slice(-3, -1)
+ .filter((id) => !!nodesNormalized.get(id))
+ .map((i, index) => {
+ const node = nodesNormalized.get(i) as unknown as Node
+
+ return { ...node, fx: 0, fy: -(index + 1) * 200, fz: 0, x: 0, y: 0, z: 0 }
+ })
+
+ return nodes
+ }, [nodesNormalized, selectionPath])
+
+ useEffect(() => {
+ const init = async () => {
+ if (selectedNode?.ref_id && selectedNode.ref_id !== prevSelectedNodeId) {
+ try {
+ const data = await fetchNodeEdges(selectedNode.ref_id, 0, 5)
+
+ if (data) {
+ const filteredNodes: NodeExtended[] = data.nodes.filter(
+ (node, index) => node.ref_id !== selectedNode.ref_id && index < 7,
+ )
+
+ const graphNodes = filteredNodes.map((node: Node) => ({ ...node, x: 0, y: 0, z: 0 }))
+
+ const nodes: NodeExtended[] = [
+ ...graphNodes,
+ { ...selectedNode, x: 0, y: 0, z: 0, fx: 0, fy: 0, fz: 0 } as NodeExtended,
+ ]
+
+ const links = data.edges.filter(
+ (link: Link) =>
+ nodes.some((node: NodeExtended) => node.ref_id === link.target) &&
+ nodes.some((node: NodeExtended) => node.ref_id === link.source),
+ )
+
+ setSelectionData({ nodes, links: links as unknown as GraphData['links'] })
+ setSimulation2D(null)
+ linksPositionRef.current = new Map()
+
+ //
+
+ addNewNode({ nodes: filteredNodes, edges: links })
+ }
+ } catch (error) {
+ console.error(error)
+ }
+ }
+ }
+
+ if (selectedNode) {
+ init()
+ }
+ }, [addNewNode, prevSelectedNodeId, selectedNode, setSelectionData])
useEffect(() => {
- const structuredNodes = structuredClone(dataInitial?.nodes || [])
+ return
+
const structuredLinks = structuredClone(dataInitial?.links || [])
if (prevSelectedNodeId === selectedNode?.ref_id) {
return
}
- const nodes = structuredNodes
- .filter(
- (f: NodeExtended) => f.ref_id === selectedNode?.ref_id || selectedNodeRelativeIds.includes(f?.ref_id || ''),
- )
- .map((n: NodeExtended) => {
- const fixedPosition = n.ref_id === selectedNode?.ref_id ? { fx: 0, fy: 0, fz: 0 } : {}
+ const graphNodes: NodeExtended[] = selectedNodeRelativeIds
+ .filter((id) => !!nodesNormalized.get(id))
+ .map((id: string) => {
+ const node = nodesNormalized.get(id) as unknown as Node
- return { ...n, x: 0, y: 0, z: 0, ...fixedPosition }
+ return { ...node, x: 0, y: 0, z: 0 }
})
+ const nodes: NodeExtended[] = [
+ ...graphNodes,
+ { ...selectedNode, x: 0, y: 0, z: 0, fx: 0, fy: 0, fz: 0 } as NodeExtended,
+ ]
+
if (nodes) {
const links = structuredLinks.filter(
(link: Link) =>
@@ -64,7 +124,7 @@ export const SelectionDataNodes = memo(() => {
setSimulation2D(null)
linksPositionRef.current = new Map()
}
- }, [dataInitial, selectedNode, selectedNodeRelativeIds, setSelectionData, prevSelectedNodeId])
+ }, [dataInitial, selectedNode, selectedNodeRelativeIds, setSelectionData, prevSelectedNodeId, nodesNormalized])
useEffect(() => {
if (simulation2d || !selectionGraphData.nodes.length) {
@@ -84,8 +144,8 @@ export const SelectionDataNodes = memo(() => {
.id((d: NodeExtended) => d.ref_id)
.distance(() => 150),
)
- .force('radial', forceRadial(500, 0, 0, 0).strength(0))
- .force('charge', forceManyBody().strength(-1000))
+ .force('radial', forceRadial(20, 0, 0, 0).strength(0))
+ .force('charge', forceManyBody().strength(-500))
.alpha(1)
.restart()
@@ -94,13 +154,6 @@ export const SelectionDataNodes = memo(() => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectionGraphData, simulation2d])
- useEffect(
- () => () => {
- setSelectionData({ nodes: [], links: [] })
- },
- [setSelectionData],
- )
-
useEffect(() => {
if (!simulation2d) {
return
@@ -124,8 +177,8 @@ export const SelectionDataNodes = memo(() => {
const grConnections = groupRef.current.getObjectByName('simulation-3d-group__connections') as Group
grConnections.children.forEach((g, i) => {
- const r = g.children[0] // Assuming Line is the first child
- const text = g.children[1] // Assuming Text is the second child
+ const r = g.children[0]
+ const text = g.children[1]
if (r instanceof Line2) {
const Line = r as Line2
@@ -139,26 +192,31 @@ export const SelectionDataNodes = memo(() => {
return
}
- const { x: sx, y: sy, z: sz } = sourceNode
- const { x: tx, y: ty, z: tz } = targetNode
+ const { x: sx, y: sy } = sourceNode
+ const { x: tx, y: ty } = targetNode
- // Set positions for the link
linksPositionRef.current.set(link.ref_id, {
sx,
sy,
- sz,
tx,
ty,
- tz,
+ sz: 0,
+ tz: 0,
})
- const midPoint = new Vector3((sx + tx) / 2, (sy + ty) / 2, (sz + tz) / 2)
+ const midPoint = new Vector3((sx + tx) / 2, (sy + ty) / 2, 0)
+
+ text.position.set(midPoint.x, midPoint.y, 1)
+
+ let angle = Math.atan2(ty - sy, tx - sx)
+
+ if (tx < sx || (Math.abs(tx - sx) < 0.01 && ty < sy)) {
+ angle += Math.PI
+ }
- // Position the text
- text.position.set(midPoint.x, midPoint.y, midPoint.z)
+ text.rotation.set(0, 0, angle)
- // Set the line positions
- Line.geometry.setPositions([sx, sy, sz, tx, ty, tz])
+ Line.geometry.setPositions([sx, sy, 0, tx, ty, 0])
const { material } = Line
@@ -191,18 +249,37 @@ export const SelectionDataNodes = memo(() => {
)
return (
-
- {selectionGraphData?.nodes.map((node) => (
-
-
- handleSelect(node)} selected={node.ref_id === selectedNode?.ref_id} />
-
-
-
-
- ))}
-
-
+ <>
+
+ {selectionGraphData?.nodes.map((node) => (
+
+
+ handleSelect(node)}
+ selected={node.ref_id === selectedNode?.ref_id}
+ />
+
+
+ ))}
+
+
+ {false && (
+
+ {pathNodes.map((node) => (
+
+
+ handleSelect(node)}
+ selected={node.ref_id === selectedNode?.ref_id}
+ />
+
+
+ ))}
+
+ )}
+ >
)
})
diff --git a/src/components/Universe/Graph/Cubes/index.tsx b/src/components/Universe/Graph/Cubes/index.tsx
index 8cfb6bcd6..60aa4579f 100644
--- a/src/components/Universe/Graph/Cubes/index.tsx
+++ b/src/components/Universe/Graph/Cubes/index.tsx
@@ -7,42 +7,12 @@ import { useDataStore, useNodeTypes } from '~/stores/useDataStore'
import { useGraphStore, useHoveredNode, useSelectedNode } from '~/stores/useGraphStore'
import { NodeExtended } from '~/types'
import { colors } from '~/utils'
+import { COLORS_MAP } from '../constant'
import { NodePoints } from './NodePoints'
import { NodeWrapper } from './NodeWrapper'
-import { RelevanceBadges } from './RelevanceBadges'
const POINTER_IN_DELAY = 200
-const COLORS_MAP = [
- '#fff',
- '#9747FF',
- '#00887A',
- '#0098A6',
- '#0288D1',
- '#33691E',
- '#465A65',
- '#512DA7',
- '#5C6BC0',
- '#5D4038',
- '#662C00',
- '#689F39',
- '#6B1B00',
- '#750000',
- '#78909C',
- '#7E57C2',
- '#8C6E63',
- '#AA47BC',
- '#BF360C',
- '#C2175B',
- '#EC407A',
- '#EF6C00',
- '#F5511E',
- '#FF9696',
- '#FFC064',
- '#FFCD29',
- '#FFEA60',
-]
-
export const Cubes = memo(() => {
const selectedNode = useSelectedNode()
const hoveredNode = useHoveredNode()
@@ -150,7 +120,7 @@ export const Cubes = memo(() => {
index={index}
node={node}
scale={node.scale || 1}
- stopFrames={hideUniverse}
+ stopFrames={false}
/>
)
})}
@@ -160,7 +130,6 @@ export const Cubes = memo(() => {
-
>
)
})
diff --git a/src/components/Universe/Graph/constant.ts b/src/components/Universe/Graph/constant.ts
index 5c05f711f..c80036e18 100644
--- a/src/components/Universe/Graph/constant.ts
+++ b/src/components/Universe/Graph/constant.ts
@@ -19,3 +19,33 @@ export const getNodeColorByType = (nodeType: string, returnString?: boolean) =>
return color
}
+
+export const COLORS_MAP = [
+ '#fff',
+ '#9747FF',
+ '#00887A',
+ '#0098A6',
+ '#0288D1',
+ '#33691E',
+ '#465A65',
+ '#512DA7',
+ '#5C6BC0',
+ '#5D4038',
+ '#662C00',
+ '#689F39',
+ '#6B1B00',
+ '#750000',
+ '#78909C',
+ '#7E57C2',
+ '#8C6E63',
+ '#AA47BC',
+ '#BF360C',
+ '#C2175B',
+ '#EC407A',
+ '#EF6C00',
+ '#F5511E',
+ '#FF9696',
+ '#FFC064',
+ '#FFCD29',
+ '#FFEA60',
+]
diff --git a/src/components/Universe/Graph/index.tsx b/src/components/Universe/Graph/index.tsx
index 2d653449f..17d26bd6a 100644
--- a/src/components/Universe/Graph/index.tsx
+++ b/src/components/Universe/Graph/index.tsx
@@ -24,7 +24,6 @@ export type LinkPosition = {
export const Graph = () => {
const { dataInitial, isLoadingNew, isFetching, dataNew, resetDataNew } = useDataStore((s) => s)
const groupRef = useRef(null)
- const cameraSettled = useRef(false)
const { normalizedSchemasByType } = useSchemaStore((s) => s)
const linksPositionRef = useRef(new Map())
@@ -78,25 +77,6 @@ export const Graph = () => {
return
}
- simulation.on('tick', () => {
- if (!cameraSettled.current && simulation.alpha() < 0.1) {
- const nodesVector = simulation.nodes().map((i: NodeExtended) => new Vector3(i.x, i.y, i.z))
-
- const boundingBox = new Box3().setFromPoints(nodesVector)
-
- const boundingSphere = new Sphere()
-
- boundingBox.getBoundingSphere(boundingSphere)
-
- const sphereRadius = Math.min(5000, boundingSphere.radius)
-
- if (false) {
- setGraphRadius(sphereRadius)
- cameraSettled.current = true
- }
- }
- })
-
simulation.on('end', () => {
const nodesVector = simulation.nodes().map((i: NodeExtended) => {
// eslint-disable-next-line no-param-reassign
@@ -182,7 +162,15 @@ export const Graph = () => {
const boundingBox = new Box3().setFromPoints(nodesVector)
- console.log(boundingBox)
+ const boundingSphere = new Sphere()
+
+ boundingBox.getBoundingSphere(boundingSphere)
+
+ const sphereRadius = Math.min(5000, boundingSphere.radius)
+
+ console.log(sphereRadius)
+
+ setGraphRadius(sphereRadius)
})
}, [dataInitial, simulation, setGraphRadius, normalizedSchemasByType])
diff --git a/src/components/Universe/index.tsx b/src/components/Universe/index.tsx
index 2226c8875..2ee57bdb2 100644
--- a/src/components/Universe/index.tsx
+++ b/src/components/Universe/index.tsx
@@ -178,7 +178,7 @@ const _Universe = () => {
-
+
diff --git a/src/stores/useGraphStore/index.ts b/src/stores/useGraphStore/index.ts
index 02c54d9c2..c91a9c43f 100644
--- a/src/stores/useGraphStore/index.ts
+++ b/src/stores/useGraphStore/index.ts
@@ -82,6 +82,7 @@ export type GraphStore = {
simulationHelpers: SimulationHelpers
isHovering: boolean
activeEdge: Link | null
+ selectionPath: string[]
setDisableCameraRotation: (rotation: boolean) => void
setScrollEventsDisabled: (rotation: boolean) => void
@@ -99,6 +100,7 @@ export type GraphStore = {
simulationCreate: (nodes: Node[], links: Link[]) => void
setIsHovering: (isHovering: boolean) => void
removeSimulation: () => void
+ addToSelectionPath: (id: string) => void
}
const defaultData: Omit<
@@ -121,6 +123,7 @@ const defaultData: Omit<
| 'simulationCreate'
| 'setIsHovering'
| 'removeSimulation'
+ | 'addToSelectionPath'
> = {
data: null,
simulation: null,
@@ -138,6 +141,7 @@ const defaultData: Omit<
showSelectionGraph: false,
simulationHelpers: defaultSimulationHelpers,
isHovering: false,
+ selectionPath: [],
}
export const useGraphStore = create()((set, get) => ({
@@ -158,6 +162,11 @@ export const useGraphStore = create()((set, get) => ({
setActiveEdge: (activeEdge) => {
set({ activeEdge })
},
+ addToSelectionPath: (id: string) => {
+ const { selectionPath } = get()
+
+ set({ selectionPath: [...selectionPath, id] })
+ },
setSelectedNode: (selectedNode) => {
if (!selectedNode) {
set({
@@ -165,10 +174,11 @@ export const useGraphStore = create()((set, get) => ({
selectedNode: null,
disableCameraRotation: false,
showSelectionGraph: false,
+ selectionPath: [],
})
}
- const { selectedNode: stateSelectedNode, simulation } = get()
+ const { selectedNode: stateSelectedNode, simulation, selectionPath } = get()
if (stateSelectedNode?.ref_id !== selectedNode?.ref_id) {
const selectedNodeWithCoordinates =
@@ -179,6 +189,7 @@ export const useGraphStore = create()((set, get) => ({
selectedNode: selectedNodeWithCoordinates,
disableCameraRotation: true,
showSelectionGraph: !!selectedNode,
+ selectionPath: [...selectionPath, selectedNodeWithCoordinates.ref_id],
})
}
},
diff --git a/src/types/index.ts b/src/types/index.ts
index 9fdd137db..f33828606 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -5,6 +5,55 @@ type QueryData = {
ref_id: string
}
+export type Node = {
+ boost?: number | null
+ children?: string[]
+ x: number
+ y: number
+ z: number
+ edge_count: number
+ hidden?: boolean
+ colors?: string[]
+ date?: number
+ description?: string
+ episode_title?: string
+ hosts?: Guests[]
+ guests?: (null | string | Guests)[]
+ id?: string
+ image_url?: string
+ sender_pic?: string
+ sender_alias?: string
+ message_content?: string
+ keyword?: boolean
+ label: string
+ source_link?: string
+ link?: string
+ name: string
+ node_type: string
+ ref_id: string
+ scale?: number
+ show_title?: string
+ text?: string
+ timestamp?: string
+ topics?: string[]
+ type?: string
+ weight?: number
+ tweet_id?: string
+ posted_by?: PostedBy
+ twitter_handle?: string
+ profile_picture?: string
+ verified?: boolean
+ unique_id?: string
+ properties?: { [key: string]: never | undefined }
+ media_url?: string
+ start?: number
+ end?: number
+ longitude?: number
+ latitude?: number
+ coordinates?: Coordinates
+ audio?: Audio[]
+}
+
export type FetchDataResponse = {
nodes: Node[]
edges: Link[]
@@ -69,49 +118,6 @@ export type NodeRequest = {
}
}
-export type Node = {
- boost?: number | null
- children?: string[]
- x: number
- y: number
- z: number
- edge_count: number
- hidden?: boolean
- colors?: string[]
- date?: number
- description?: string
- episode_title?: string
- hosts?: Guests[]
- guests?: (null | string | Guests)[]
- id?: string
- image_url?: string
- sender_pic?: string
- sender_alias?: string
- message_content?: string
- keyword?: boolean
- label: string
- source_link?: string
- link?: string
- name: string
- node_type: string
- ref_id: string
- scale?: number
- show_title?: string
- text?: string
- timestamp?: string
- topics?: string[]
- type?: string
- weight?: number
- tweet_id?: string
- posted_by?: PostedBy
- twitter_handle?: string
- profile_picture?: string
- verified?: boolean
- unique_id?: string
- properties?: { [key: string]: never | undefined }
- media_url?: string
-}
-
export type DataSeriesNode = {
id: string
image_url?: string
@@ -147,13 +153,6 @@ export type NodeExtended = Node & {
fx?: number
fy?: number
fz?: number
- start?: number
- end?: number
- longitude?: number
- latitude?: number
- coordinates?: Coordinates
- audio?: Audio[]
- properties?: { [key: string]: never | undefined }
}
export type Link = {