Compare commits
2 Commits
0a076d29f5
...
ae164c47a8
| Author | SHA1 | Date | |
|---|---|---|---|
| ae164c47a8 | |||
| 0bbd6a013d |
@@ -1,14 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
env: { browser: true, es2020: true, node: true },
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
||||||
plugins: ['react-refresh'],
|
|
||||||
rules: {
|
|
||||||
'react-refresh/only-export-components': 'warn',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +0,0 @@
|
|||||||
# SCM syntax highlighting & preventing 3-way merges
|
|
||||||
pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff
|
|
||||||
27
.gitignore
vendored
27
.gitignore
vendored
@@ -1,27 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
# pixi environments
|
|
||||||
.pixi/*
|
|
||||||
!.pixi/config.toml
|
|
||||||
11
Dockerfile
11
Dockerfile
@@ -1,11 +0,0 @@
|
|||||||
FROM node:22 as node_builder
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
COPY . /usr/src/app
|
|
||||||
RUN yarn
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
FROM nginxinc/nginx-unprivileged:1.29
|
|
||||||
|
|
||||||
COPY --from=node_builder /usr/src/app/dist /usr/share/nginx/html
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
16
index.html
16
index.html
@@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="manifest" href="/manifest.json">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="icon" type="image/png" sizes="196x196" href="favicon-196.png">
|
|
||||||
<link rel="apple-touch-icon" href="apple-icon-180.png">
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
||||||
<title>MapComponents + Vite + React + TS</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
12845
package-lock.json
generated
12845
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "FossGIS Workshop 2026 3D Tiles",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc && vite build",
|
|
||||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@mapcomponents/deck-gl": "^1.8.9",
|
|
||||||
"@mapcomponents/react-maplibre": "^1.8.9",
|
|
||||||
"react": "^19.2.4",
|
|
||||||
"react-dom": "^19.2.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^19.2.14",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
||||||
"@typescript-eslint/parser": "^8.57.2",
|
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
|
||||||
"eslint": "^9.39.4",
|
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"vite": "^8.0.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
pixi.lock
18
pixi.lock
@@ -1,18 +0,0 @@
|
|||||||
version: 6
|
|
||||||
environments:
|
|
||||||
default:
|
|
||||||
channels:
|
|
||||||
- url: https://conda.anaconda.org/conda-forge/
|
|
||||||
options:
|
|
||||||
pypi-prerelease-mode: if-necessary-or-explicit
|
|
||||||
packages:
|
|
||||||
win-64:
|
|
||||||
- conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda
|
|
||||||
packages:
|
|
||||||
- conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda
|
|
||||||
sha256: 5e38e51da1aa4bc352db9b4cec1c3e25811de0f4408edaa24e009a64de6dbfdf
|
|
||||||
md5: e626ee7934e4b7cb21ce6b721cff8677
|
|
||||||
license: MIT
|
|
||||||
license_family: MIT
|
|
||||||
size: 31271315
|
|
||||||
timestamp: 1774517904472
|
|
||||||
11
pixi.toml
11
pixi.toml
@@ -1,11 +0,0 @@
|
|||||||
[workspace]
|
|
||||||
authors = ["Arne Zitting <arne.zitting@student.jade-hs.de>"]
|
|
||||||
channels = ["conda-forge"]
|
|
||||||
name = "Projekt Visualisierung - Code"
|
|
||||||
platforms = ["win-64"]
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[tasks]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
nodejs = ">=25.8.2,<25.9"
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
107426
public/assets/hedges.geojson
107426
public/assets/hedges.geojson
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
269831
public/assets/trees.geojson
269831
public/assets/trees.geojson
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "MapComponents app",
|
|
||||||
"name": "MapComponents + TypeScript + React App",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "manifest-icon-192.maskable.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "manifest-icon-192.maskable.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "manifest-icon-512.maskable.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "manifest-icon-512.maskable.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
||||||
44
src/App.tsx
44
src/App.tsx
@@ -1,44 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import "./App.css";
|
|
||||||
import {
|
|
||||||
MapLibreMap,
|
|
||||||
Sidebar,
|
|
||||||
TopToolbar,
|
|
||||||
} from "@mapcomponents/react-maplibre";
|
|
||||||
import IconButton from "@mui/material/IconButton";
|
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
|
||||||
import LayerTree from "./components/LayerTree";
|
|
||||||
import Light from "./components/Light";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MapLibreMap
|
|
||||||
options={{
|
|
||||||
style: "https://wms.wheregroup.com/tileserver/style/osm-bright.json",
|
|
||||||
zoom: 19,
|
|
||||||
center: [9.9347680519611, 51.531935000614226],
|
|
||||||
}}
|
|
||||||
style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: 0 }}
|
|
||||||
/>
|
|
||||||
<TopToolbar
|
|
||||||
buttons={
|
|
||||||
<IconButton
|
|
||||||
onClick={() => setSidebarOpen((prev) => !prev)}
|
|
||||||
aria-label="toggle sidebar"
|
|
||||||
>
|
|
||||||
<MenuIcon />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Sidebar open={sidebarOpen} setOpen={setSidebarOpen}>
|
|
||||||
<LayerTree />
|
|
||||||
</Sidebar>
|
|
||||||
<Light />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { MlGeoJsonLayer } from "@mapcomponents/react-maplibre";
|
|
||||||
import type { FeatureCollection } from "geojson";
|
|
||||||
|
|
||||||
export default function HedgeLayer() {
|
|
||||||
const [geojson, setGeojson] = useState<FeatureCollection | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch("/assets/hedges.geojson")
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then(setGeojson);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!geojson) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MlGeoJsonLayer
|
|
||||||
layerId="hedge"
|
|
||||||
geojson={geojson}
|
|
||||||
type="line"
|
|
||||||
insertBeforeLayer="waterway-name"
|
|
||||||
options={{
|
|
||||||
layout: {
|
|
||||||
"line-cap": "round",
|
|
||||||
"line-join": "round",
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
"line-color": "#4caf50",
|
|
||||||
"line-width": 6,
|
|
||||||
"line-opacity": 0.9,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import List from "@mui/material/List";
|
|
||||||
import ListItem from "@mui/material/ListItem";
|
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
|
||||||
import ListItemText from "@mui/material/ListItemText";
|
|
||||||
import Checkbox from "@mui/material/Checkbox";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Divider from "@mui/material/Divider";
|
|
||||||
|
|
||||||
import ThreeDTilesLayer from "./ThreeDTilesLayer/ThreeDTilesLayer_1";
|
|
||||||
//import TreeLayer from "./TreeLayer/TreeLayer_1";
|
|
||||||
//import HedgeLayer from "./HedgeLayer/HedgeLayer";
|
|
||||||
|
|
||||||
const LAYERS = [
|
|
||||||
//{ id: "hedges", label: "Hedges", component: <HedgeLayer key="hedges" /> },
|
|
||||||
{ id: "tiles3d", label: "3D Buildings", component: <ThreeDTilesLayer key="tiles3d" /> },
|
|
||||||
//{ id: "trees", label: "Trees", component: <TreeLayer sizeScale={0.5} key="trees" /> },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export default function LayerTree() {
|
|
||||||
const [visible, setVisible] = useState<Record<string, boolean>>(
|
|
||||||
Object.fromEntries(LAYERS.map((l) => [l.id, true])),
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggle = (id: string) => setVisible((v) => ({ ...v, [id]: !v[id] }));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography
|
|
||||||
variant="overline"
|
|
||||||
sx={{ px: 2, pt: 2, display: "block", color: "text.secondary" }}
|
|
||||||
>
|
|
||||||
Layers
|
|
||||||
</Typography>
|
|
||||||
<List dense disablePadding>
|
|
||||||
{LAYERS.map((layer) => (
|
|
||||||
<ListItem
|
|
||||||
key={layer.id}
|
|
||||||
onClick={() => toggle(layer.id)}
|
|
||||||
sx={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<ListItemIcon sx={{ minWidth: 36 }}>
|
|
||||||
<Checkbox
|
|
||||||
edge="start"
|
|
||||||
checked={!!visible[layer.id]}
|
|
||||||
disableRipple
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={layer.label} />
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
<Divider sx={{ my: 1 }} />
|
|
||||||
{LAYERS.map((layer) => visible[layer.id] && layer.component)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { LightingEffect, AmbientLight, DirectionalLight } from "@deck.gl/core";
|
|
||||||
import { useDeckGl } from "@mapcomponents/deck-gl";
|
|
||||||
|
|
||||||
const effect = new LightingEffect({
|
|
||||||
ambientLight: new AmbientLight({ color: [255, 255, 255], intensity: 1.8 }),
|
|
||||||
sunLight: new DirectionalLight({
|
|
||||||
color: [255, 240, 200],
|
|
||||||
intensity: 2.0,
|
|
||||||
direction: [-2, -4, -3],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function LightingSetup() {
|
|
||||||
const { addEffect, removeEffect } = useDeckGl();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
addEffect(effect);
|
|
||||||
return () => removeEffect(effect);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import Ml3DTileLayer from "../../lib/MlTile3DLayer/Ml3DTilesLayer";
|
|
||||||
|
|
||||||
type Props = {};
|
|
||||||
|
|
||||||
export default function ThreeDTilesLayer({}: Props) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Ml3DTileLayer
|
|
||||||
id="3d-tiles-layer"
|
|
||||||
data="https://sgx.geodatenzentrum.de/gdz_basemapde_3d_gebaeude/lod2_4978_null.json"
|
|
||||||
//beforeId="waterway-name"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { useCallback, useRef } from "react";
|
|
||||||
import Ml3DTileLayer from "../../lib/MlTile3DLayer/Ml3DTilesLayer";
|
|
||||||
import type { Tile3D } from "@loaders.gl/tiles";
|
|
||||||
import type { Color } from "@deck.gl/core";
|
|
||||||
|
|
||||||
type Props = {};
|
|
||||||
|
|
||||||
export default function ThreeDTilesLayer({}: Props) {
|
|
||||||
const selectedRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
const getFeatureColor = useCallback(
|
|
||||||
(featureId: number, tile: Tile3D): Color => {
|
|
||||||
const props = tile.content?.propertyTable?.[featureId];
|
|
||||||
const surface = (props?.surface as string)?.toLowerCase();
|
|
||||||
|
|
||||||
if (surface === "roof") {
|
|
||||||
return [255, 80, 80];
|
|
||||||
}
|
|
||||||
return [235, 235, 235];
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Ml3DTileLayer
|
|
||||||
id="3d-tiles-layer"
|
|
||||||
data="https://sgx.geodatenzentrum.de/gdz_basemapde_3d_gebaeude/lod2_4978_null.json"
|
|
||||||
getFeatureColor={() => [0,200,0]}
|
|
||||||
//getFeatureColor={getFeatureColor}
|
|
||||||
beforeId="waterway-name"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
|
||||||
import { Enhanced3DTilePickingInfo } from "../../lib/MlTile3DLayer/Tiles3DLayer2";
|
|
||||||
import Ml3DTileLayer from "../../lib/MlTile3DLayer/Ml3DTilesLayer";
|
|
||||||
import type { Tile3D } from "@loaders.gl/tiles";
|
|
||||||
import type { Color } from "@deck.gl/core";
|
|
||||||
|
|
||||||
type Props = {};
|
|
||||||
|
|
||||||
export default function ThreeDTilesLayer({}: Props) {
|
|
||||||
const [colorVersion, setColorVersion] = useState(0);
|
|
||||||
const selectedRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
const getFeatureColor = useCallback(
|
|
||||||
(featureId: number, tile: Tile3D): Color => {
|
|
||||||
const props = tile.content?.propertyTable?.[featureId];
|
|
||||||
const gmlId = props?.gml_id as string | undefined;
|
|
||||||
const surface = (props?.surface as string)?.toLowerCase();
|
|
||||||
|
|
||||||
if (selectedRef.current && gmlId === selectedRef.current) {
|
|
||||||
return [0, 200, 0];
|
|
||||||
}
|
|
||||||
if (surface === "roof") {
|
|
||||||
return [255, 80, 80];
|
|
||||||
}
|
|
||||||
return [235, 235, 235];
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onClick = useCallback((info: unknown) => {
|
|
||||||
const pickInfo = info as Enhanced3DTilePickingInfo;
|
|
||||||
if (!pickInfo.picked) return;
|
|
||||||
|
|
||||||
const gmlId = pickInfo.featureProperties?.gml_id as string | undefined;
|
|
||||||
if (gmlId) {
|
|
||||||
const isDeselect = selectedRef.current === gmlId;
|
|
||||||
selectedRef.current = isDeselect ? null : gmlId;
|
|
||||||
setColorVersion((v) => v + 1);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Ml3DTileLayer
|
|
||||||
id="3d-tiles-layer"
|
|
||||||
data="https://sgx.geodatenzentrum.de/gdz_basemapde_3d_gebaeude/lod2_4978_null.json"
|
|
||||||
pickable={true}
|
|
||||||
getFeatureColor={getFeatureColor}
|
|
||||||
//onClick={onClick}
|
|
||||||
onClick={(info) => console.log(info)}
|
|
||||||
updateTrigger={colorVersion}
|
|
||||||
beforeId="waterway-name"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
|
||||||
import { Enhanced3DTilePickingInfo } from "../../lib/MlTile3DLayer/Tiles3DLayer2";
|
|
||||||
import Ml3DTileLayer from "../../lib/MlTile3DLayer/Ml3DTilesLayer";
|
|
||||||
import type { Tile3D } from "@loaders.gl/tiles";
|
|
||||||
import type { Color } from "@deck.gl/core";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Divider from "@mui/material/Divider";
|
|
||||||
import Paper from "@mui/material/Paper";
|
|
||||||
|
|
||||||
type Props = {};
|
|
||||||
|
|
||||||
export default function ThreeDTilesLayer({}: Props) {
|
|
||||||
const [colorVersion, setColorVersion] = useState(0);
|
|
||||||
const selectedRef = useRef<string | null>(null);
|
|
||||||
const [selectedProps, setSelectedProps] = useState<Record<
|
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
> | null>(null);
|
|
||||||
|
|
||||||
const getFeatureColor = useCallback(
|
|
||||||
(featureId: number, tile: Tile3D): Color => {
|
|
||||||
const props = tile.content?.propertyTable?.[featureId];
|
|
||||||
const gmlId = props?.gml_id as string | undefined;
|
|
||||||
const surface = (props?.surface as string)?.toLowerCase();
|
|
||||||
|
|
||||||
if (selectedRef.current && gmlId === selectedRef.current) {
|
|
||||||
return [0, 200, 0];
|
|
||||||
}
|
|
||||||
if (surface === "roof") {
|
|
||||||
return [255, 80, 80];
|
|
||||||
}
|
|
||||||
return [235, 235, 235];
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onClick = useCallback((info: unknown) => {
|
|
||||||
const pickInfo = info as Enhanced3DTilePickingInfo;
|
|
||||||
if (!pickInfo.picked) return;
|
|
||||||
|
|
||||||
const gmlId = pickInfo.featureProperties?.gml_id as string | undefined;
|
|
||||||
if (gmlId) {
|
|
||||||
const isDeselect = selectedRef.current === gmlId;
|
|
||||||
selectedRef.current = isDeselect ? null : gmlId;
|
|
||||||
setSelectedProps(
|
|
||||||
isDeselect ? null : (pickInfo.featureProperties ?? null),
|
|
||||||
);
|
|
||||||
setColorVersion((v) => v + 1);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Ml3DTileLayer
|
|
||||||
id="3d-tiles-layer"
|
|
||||||
data="https://sgx.geodatenzentrum.de/gdz_basemapde_3d_gebaeude/lod2_4978_null.json"
|
|
||||||
pickable={true}
|
|
||||||
getFeatureColor={getFeatureColor}
|
|
||||||
onClick={onClick}
|
|
||||||
updateTrigger={colorVersion}
|
|
||||||
beforeId="waterway-name"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedProps && (
|
|
||||||
<Paper variant="outlined" sx={{ m: 2, overflow: "hidden" }}>
|
|
||||||
<Box sx={{ px: 2, py: 1.5, bgcolor: "primary.main" }}>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle2"
|
|
||||||
sx={{ color: "primary.contrastText", fontWeight: 700 }}
|
|
||||||
>
|
|
||||||
Selected Building
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Divider />
|
|
||||||
<Box sx={{ px: 2, py: 1 }}>
|
|
||||||
{Object.entries(selectedProps).map(([key, value]) => (
|
|
||||||
<Box
|
|
||||||
key={key}
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
py: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ mr: 2, flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
{key}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ textAlign: "right", wordBreak: "break-all" }}
|
|
||||||
>
|
|
||||||
{String(value)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { MlSceneGraphLayer } from "@mapcomponents/deck-gl";
|
|
||||||
|
|
||||||
interface TreeFeature {
|
|
||||||
type: "Feature";
|
|
||||||
properties: {
|
|
||||||
leaf_type?: string;
|
|
||||||
height?: string | number;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
geometry: {
|
|
||||||
type: "Point";
|
|
||||||
coordinates: [number, number] | [number, number, number];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPosition = (d: unknown): [number, number, number] => {
|
|
||||||
const f = d as TreeFeature;
|
|
||||||
const [lon, lat] = f.geometry.coordinates;
|
|
||||||
return [lon, lat, 0];
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TreeLayerProps {
|
|
||||||
mapId?: string;
|
|
||||||
sizeScale?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TreeLayer = ({ mapId, sizeScale = 15 }: TreeLayerProps) => {
|
|
||||||
const [pineFeatures, setPineFeatures] = useState<TreeFeature[]>([]);
|
|
||||||
const [broadleafFeatures, setBroadleafFeatures] = useState<TreeFeature[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch("/assets/trees.geojson")
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data: { features: TreeFeature[] }) => {
|
|
||||||
const features = data.features;
|
|
||||||
setPineFeatures(
|
|
||||||
features.filter((f) => f.properties.leaf_type === "needleleaved")
|
|
||||||
);
|
|
||||||
setBroadleafFeatures(
|
|
||||||
features.filter((f) => f.properties.leaf_type !== "needleleaved")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{pineFeatures.length > 0 && (
|
|
||||||
<MlSceneGraphLayer
|
|
||||||
id="pine-trees"
|
|
||||||
mapId={mapId}
|
|
||||||
data={pineFeatures}
|
|
||||||
scenegraph="/assets/low_poly_pine.glb"
|
|
||||||
getPosition={getPosition}
|
|
||||||
getOrientation={[0, 0, 90]}
|
|
||||||
sizeScale={sizeScale}
|
|
||||||
_lighting="pbr"
|
|
||||||
pickable
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{broadleafFeatures.length > 0 && (
|
|
||||||
<MlSceneGraphLayer
|
|
||||||
id="broadleaf-trees"
|
|
||||||
mapId={mapId}
|
|
||||||
data={broadleafFeatures}
|
|
||||||
scenegraph="/assets/low_poly_tree.glb"
|
|
||||||
getPosition={getPosition}
|
|
||||||
getOrientation={[0, 0, 90]}
|
|
||||||
sizeScale={sizeScale}
|
|
||||||
_lighting="pbr"
|
|
||||||
pickable
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TreeLayer;
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { MlSceneGraphLayer } from "@mapcomponents/deck-gl";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Divider from "@mui/material/Divider";
|
|
||||||
import Paper from "@mui/material/Paper";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
|
|
||||||
interface TreeFeature {
|
|
||||||
type: "Feature";
|
|
||||||
properties: {
|
|
||||||
leaf_type?: string;
|
|
||||||
height?: string | number;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
geometry: {
|
|
||||||
type: "Point";
|
|
||||||
coordinates: [number, number] | [number, number, number];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPosition = (d: unknown): [number, number, number] => {
|
|
||||||
const f = d as TreeFeature;
|
|
||||||
const [lon, lat] = f.geometry.coordinates;
|
|
||||||
return [lon, lat, 0];
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TreeLayerProps {
|
|
||||||
mapId?: string;
|
|
||||||
sizeScale?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TreeLayer = ({ mapId, sizeScale = 15 }: TreeLayerProps) => {
|
|
||||||
const [pineFeatures, setPineFeatures] = useState<TreeFeature[]>([]);
|
|
||||||
const [broadleafFeatures, setBroadleafFeatures] = useState<TreeFeature[]>([]);
|
|
||||||
const [selectedFeature, setSelectedFeature] = useState<TreeFeature | null>(null);
|
|
||||||
|
|
||||||
const selectedProps = selectedFeature ? (selectedFeature.properties as Record<string, unknown>) : null;
|
|
||||||
|
|
||||||
const onClick = useCallback((info: unknown) => {
|
|
||||||
const pickInfo = info as { picked?: boolean; object?: TreeFeature };
|
|
||||||
if (!pickInfo.picked || !pickInfo.object) return;
|
|
||||||
const feature = pickInfo.object;
|
|
||||||
setSelectedFeature((prev) =>
|
|
||||||
JSON.stringify(prev?.geometry.coordinates) === JSON.stringify(feature.geometry.coordinates)
|
|
||||||
? null
|
|
||||||
: feature
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch("/assets/trees.geojson")
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data: { features: TreeFeature[] }) => {
|
|
||||||
const features = data.features;
|
|
||||||
setPineFeatures(
|
|
||||||
features.filter((f) => f.properties.leaf_type === "needleleaved")
|
|
||||||
);
|
|
||||||
setBroadleafFeatures(
|
|
||||||
features.filter((f) => f.properties.leaf_type !== "needleleaved")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isSelected = useCallback(
|
|
||||||
(f: TreeFeature) =>
|
|
||||||
selectedFeature !== null &&
|
|
||||||
JSON.stringify(f.geometry.coordinates) ===
|
|
||||||
JSON.stringify(selectedFeature.geometry.coordinates),
|
|
||||||
[selectedFeature]
|
|
||||||
);
|
|
||||||
|
|
||||||
const yellowColor: [number, number, number, number] = [255, 255, 0, 255];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{pineFeatures.length > 0 && (
|
|
||||||
<MlSceneGraphLayer
|
|
||||||
id="pine-trees"
|
|
||||||
mapId={mapId}
|
|
||||||
data={pineFeatures.filter((f) => !isSelected(f)) as unknown[]}
|
|
||||||
scenegraph="/assets/low_poly_pine.glb"
|
|
||||||
getPosition={getPosition}
|
|
||||||
getOrientation={[0, 0, 90]}
|
|
||||||
sizeScale={sizeScale}
|
|
||||||
_lighting="pbr"
|
|
||||||
updateTriggers={{ data: selectedFeature }}
|
|
||||||
pickable
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{broadleafFeatures.length > 0 && (
|
|
||||||
<MlSceneGraphLayer
|
|
||||||
id="broadleaf-trees"
|
|
||||||
mapId={mapId}
|
|
||||||
data={broadleafFeatures.filter((f) => !isSelected(f)) as unknown[]}
|
|
||||||
scenegraph="/assets/low_poly_tree.glb"
|
|
||||||
getPosition={getPosition}
|
|
||||||
getOrientation={[0, 0, 90]}
|
|
||||||
sizeScale={sizeScale}
|
|
||||||
_lighting="pbr"
|
|
||||||
updateTriggers={{ data: selectedFeature }}
|
|
||||||
pickable
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedFeature && (
|
|
||||||
<MlSceneGraphLayer
|
|
||||||
id="selected-tree"
|
|
||||||
mapId={mapId}
|
|
||||||
data={[selectedFeature] as unknown[]}
|
|
||||||
scenegraph={
|
|
||||||
selectedFeature.properties.leaf_type === "needleleaved"
|
|
||||||
? "/assets/low_poly_pine.glb"
|
|
||||||
: "/assets/low_poly_tree.glb"
|
|
||||||
}
|
|
||||||
getPosition={getPosition}
|
|
||||||
getOrientation={[0, 0, 90]}
|
|
||||||
sizeScale={sizeScale}
|
|
||||||
_lighting="flat"
|
|
||||||
getColor={() => yellowColor}
|
|
||||||
pickable
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedProps && (
|
|
||||||
<Paper variant="outlined" sx={{ m: 2, overflow: "hidden" }}>
|
|
||||||
<Box sx={{ px: 2, py: 1.5, bgcolor: "primary.main" }}>
|
|
||||||
<Typography variant="subtitle2" sx={{ color: "primary.contrastText", fontWeight: 700 }}>
|
|
||||||
Selected Tree
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Divider />
|
|
||||||
<Box sx={{ px: 2, py: 1 }}>
|
|
||||||
{Object.entries(selectedProps).map(([key, value]) => (
|
|
||||||
<Box key={key} sx={{ display: "flex", justifyContent: "space-between", py: 0.5 }}>
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mr: 2, flexShrink: 0 }}>
|
|
||||||
{key}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ textAlign: "right", wordBreak: "break-all" }}>
|
|
||||||
{String(value)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TreeLayer;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// This file is inspired by @mapcomponents/deck-gl's Ml3DTileLayer
|
|
||||||
// (https://github.com/mapcomponents/react-map-components-maplibre)
|
|
||||||
//
|
|
||||||
// Original work: Copyright (c) 2021 WhereGroup GmbH — MIT License
|
|
||||||
// Modified to use the custom Tile3DLayer2 class (Tiles3DLayer2.tsx) instead of
|
|
||||||
// deck.gl's built-in Tile3DLayer, enabling in-place layer replacement without
|
|
||||||
// restarting tile loading and exposing the updateTrigger prop.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { useContext, useEffect, useMemo, useRef } from 'react';
|
|
||||||
import { useMap } from '@mapcomponents/react-maplibre';
|
|
||||||
//import { Tile3DLayer, Tile3DLayerProps } from '@deck.gl/geo-layers';
|
|
||||||
import { default as Tile3DLayer, Tile3DLayer2Props } from './Tiles3DLayer2';
|
|
||||||
import { DeckGlContext } from '@mapcomponents/deck-gl';
|
|
||||||
import type { Layer } from '@deck.gl/core';
|
|
||||||
|
|
||||||
export interface Ml3DTileLayerProps extends Tile3DLayer2Props {
|
|
||||||
/**
|
|
||||||
* Id of the target MapLibre instance in mapContext
|
|
||||||
*/
|
|
||||||
mapId?: string;
|
|
||||||
/**
|
|
||||||
* Id of an existing layer in the mapLibre instance to help specify the layer order
|
|
||||||
* This layer will be visually beneath the layer with the "beforeId" id.
|
|
||||||
*/
|
|
||||||
beforeId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Ml3DTileLayer = (props: Ml3DTileLayerProps) => {
|
|
||||||
const { mapId, ...Ml3DTileProps } = props;
|
|
||||||
const mapHook = useMap({ mapId: mapId });
|
|
||||||
const deckGlContext = useContext(DeckGlContext);
|
|
||||||
const layerRef = useRef<Layer | null>(null);
|
|
||||||
|
|
||||||
const tile3dLayer = useMemo(() => {
|
|
||||||
if (!Ml3DTileProps.data) return null;
|
|
||||||
else
|
|
||||||
return new Tile3DLayer({
|
|
||||||
...Ml3DTileProps,
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
Ml3DTileProps.data,
|
|
||||||
Ml3DTileProps.id,
|
|
||||||
Ml3DTileProps.pickable,
|
|
||||||
Ml3DTileProps.onTileLoad,
|
|
||||||
Ml3DTileProps.onTileUnload,
|
|
||||||
Ml3DTileProps.loadOptions,
|
|
||||||
Ml3DTileProps.loaders,
|
|
||||||
Ml3DTileProps.visible,
|
|
||||||
Ml3DTileProps.opacity,
|
|
||||||
Ml3DTileProps.pointSize,
|
|
||||||
Ml3DTileProps.beforeId,
|
|
||||||
Ml3DTileProps.getFeatureColor,
|
|
||||||
Ml3DTileProps.onClick,
|
|
||||||
Ml3DTileProps.updateTrigger,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Add or replace the layer in the deck.gl layer array in-place.
|
|
||||||
// This avoids remove+add which would destroy the tileset via MapboxOverlay.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!mapHook.map || !tile3dLayer) return;
|
|
||||||
|
|
||||||
const prev = layerRef.current;
|
|
||||||
layerRef.current = tile3dLayer;
|
|
||||||
|
|
||||||
deckGlContext.setDeckGlLayerArray((layers) => {
|
|
||||||
if (prev) {
|
|
||||||
// Replace old layer reference in-place
|
|
||||||
return layers.map((l) => (l === prev ? tile3dLayer : l));
|
|
||||||
}
|
|
||||||
// First mount: append
|
|
||||||
return [...layers, tile3dLayer];
|
|
||||||
});
|
|
||||||
}, [mapHook.map, tile3dLayer]);
|
|
||||||
|
|
||||||
// Remove layer only on unmount
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
const layer = layerRef.current;
|
|
||||||
if (layer) {
|
|
||||||
deckGlContext.setDeckGlLayerArray((layers) =>
|
|
||||||
layers.filter((l) => l !== layer)
|
|
||||||
);
|
|
||||||
layerRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Ml3DTileLayer;
|
|
||||||
@@ -1,544 +0,0 @@
|
|||||||
// This file is derived from deck.gl's Tile3DLayer
|
|
||||||
// (https://github.com/visgl/deck.gl/blob/master/modules/geo-layers/src/tile-3d-layer/tile-3d-layer.ts)
|
|
||||||
//
|
|
||||||
// Original work: Copyright (c) Vis.gl contributors — MIT License
|
|
||||||
// Modified work: per-feature GPU picking, per-feature color via getFeatureColor,
|
|
||||||
// glTF→MESH conversion with EXT_structural_metadata property table support,
|
|
||||||
// and enhanced PickingInfo type (Enhanced3DTilePickingInfo).
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import {Geometry} from '@luma.gl/engine';
|
|
||||||
import {
|
|
||||||
Accessor,
|
|
||||||
Color,
|
|
||||||
CompositeLayer,
|
|
||||||
CompositeLayerProps,
|
|
||||||
COORDINATE_SYSTEM,
|
|
||||||
FilterContext,
|
|
||||||
GetPickingInfoParams,
|
|
||||||
Layer,
|
|
||||||
LayersList,
|
|
||||||
log,
|
|
||||||
PickingInfo,
|
|
||||||
UpdateParameters,
|
|
||||||
Viewport,
|
|
||||||
DefaultProps,
|
|
||||||
} from '@deck.gl/core';
|
|
||||||
import {PointCloudLayer} from '@deck.gl/layers';
|
|
||||||
import {ScenegraphLayer} from '@deck.gl/mesh-layers';
|
|
||||||
// Internal MeshLayer supports featureIds + PBR; the public SimpleMeshLayer does not.
|
|
||||||
import MeshLayer from '../../../node_modules/@deck.gl/geo-layers/dist/mesh-layer/mesh-layer.js';
|
|
||||||
import {load} from '@loaders.gl/core';
|
|
||||||
import type {MeshAttributes} from '@loaders.gl/schema';
|
|
||||||
import {Tileset3D, Tile3D, TILE_TYPE} from '@loaders.gl/tiles';
|
|
||||||
import {Tiles3DLoader} from '@loaders.gl/3d-tiles';
|
|
||||||
|
|
||||||
export type Enhanced3DTilePickingInfo = PickingInfo & {
|
|
||||||
sourceTile: Tile3D | null;
|
|
||||||
featureId?: number;
|
|
||||||
featureProperties?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SINGLE_DATA = [0];
|
|
||||||
|
|
||||||
export type Tile3DLayer2Props<DataT = unknown> = _Tile3DLayer2Props<DataT> &
|
|
||||||
CompositeLayerProps;
|
|
||||||
|
|
||||||
type _Tile3DLayer2Props<DataT> = {
|
|
||||||
data: string;
|
|
||||||
getPointColor?: Accessor<DataT, Color>;
|
|
||||||
pointSize?: number;
|
|
||||||
/** @deprecated Use `loaders` instead */
|
|
||||||
loader?: typeof Tiles3DLoader;
|
|
||||||
onTilesetLoad?: (tileset: Tileset3D) => void;
|
|
||||||
onTileLoad?: (tile: Tile3D) => void;
|
|
||||||
onTileUnload?: (tile: Tile3D) => void;
|
|
||||||
onTileError?: (tile: Tile3D, url: string, message: string) => void;
|
|
||||||
/** Per-feature color function. Receives featureId and owning Tile3D. Returns [r,g,b] or [r,g,b,a] in 0–255. */
|
|
||||||
getFeatureColor?: (featureId: number, tile: Tile3D) => Color;
|
|
||||||
/** Tile-level fallback color for MESH tiles when getFeatureColor is not provided. */
|
|
||||||
_getMeshColor?: (tile: Tile3D) => Color;
|
|
||||||
/** Bump to force a per-feature color rebuild without restarting tile loading. */
|
|
||||||
updateTrigger?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultProps: DefaultProps<Tile3DLayer2Props> = {
|
|
||||||
getPointColor: {type: 'accessor', value: [0, 0, 0, 255]},
|
|
||||||
pointSize: 1.0,
|
|
||||||
data: '',
|
|
||||||
loader: Tiles3DLoader,
|
|
||||||
onTilesetLoad: {type: 'function', value: () => {}},
|
|
||||||
onTileLoad: {type: 'function', value: () => {}},
|
|
||||||
onTileUnload: {type: 'function', value: () => {}},
|
|
||||||
onTileError: {type: 'function', value: () => {}},
|
|
||||||
getFeatureColor: {type: 'function', value: null, optional: true} as any,
|
|
||||||
_getMeshColor: {type: 'function', value: () => [255, 255, 255]},
|
|
||||||
updateTrigger: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drop-in replacement for deck.gl's Tile3DLayer with per-feature GPU picking
|
|
||||||
* and per-feature mesh coloring. onClick/onHover receive featureId and
|
|
||||||
* featureProperties in addition to the tile object.
|
|
||||||
*/
|
|
||||||
export default class Tile3DLayer2<
|
|
||||||
DataT = any,
|
|
||||||
ExtraPropsT extends {} = {},
|
|
||||||
> extends CompositeLayer<ExtraPropsT & Required<_Tile3DLayer2Props<DataT>>> {
|
|
||||||
static defaultProps = defaultProps;
|
|
||||||
static layerName = 'Tile3DLayer2';
|
|
||||||
|
|
||||||
declare state: {
|
|
||||||
activeViewports: Record<string, Viewport>;
|
|
||||||
frameNumber?: number;
|
|
||||||
lastUpdatedViewports: Record<string, Viewport> | null;
|
|
||||||
layerMap: Record<string, {tile: Tile3D; layer?: Layer | null; needsUpdate?: boolean}>;
|
|
||||||
tileset3d: Tileset3D | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
initializeState(): void {
|
|
||||||
if ('onTileLoadFail' in this.props) {
|
|
||||||
log.removed('onTileLoadFail', 'onTileError')();
|
|
||||||
}
|
|
||||||
this.state = {
|
|
||||||
layerMap: {},
|
|
||||||
tileset3d: null,
|
|
||||||
activeViewports: {},
|
|
||||||
lastUpdatedViewports: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get isLoaded(): boolean {
|
|
||||||
return Boolean(this.state?.tileset3d?.isLoaded() && super.isLoaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldUpdateState({changeFlags}: UpdateParameters<this>): boolean {
|
|
||||||
return changeFlags.somethingChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState({props, oldProps, changeFlags}: UpdateParameters<this>): void {
|
|
||||||
if (props.data && props.data !== oldProps.data) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this._loadTileset(props.data);
|
|
||||||
}
|
|
||||||
if (changeFlags.viewportChanged) {
|
|
||||||
const {activeViewports} = this.state;
|
|
||||||
if (Object.keys(activeViewports).length) {
|
|
||||||
this._updateTileset(activeViewports);
|
|
||||||
this.state.lastUpdatedViewports = activeViewports;
|
|
||||||
this.state.activeViewports = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changeFlags.propsChanged) {
|
|
||||||
for (const key in this.state.layerMap) {
|
|
||||||
this.state.layerMap[key].needsUpdate = true;
|
|
||||||
}
|
|
||||||
this.setNeedsUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activateViewport(viewport: Viewport): void {
|
|
||||||
const {activeViewports, lastUpdatedViewports} = this.state;
|
|
||||||
this.internalState!.viewport = viewport;
|
|
||||||
activeViewports[viewport.id] = viewport;
|
|
||||||
const lastViewport = lastUpdatedViewports?.[viewport.id];
|
|
||||||
if (!lastViewport || !viewport.equals(lastViewport)) {
|
|
||||||
this.setChangeFlags({viewportChanged: true});
|
|
||||||
this.setNeedsUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPickingInfo({info, sourceLayer}: GetPickingInfoParams): Enhanced3DTilePickingInfo {
|
|
||||||
const sourceTile: Tile3D | null = sourceLayer
|
|
||||||
? (sourceLayer.props as any).tile ?? null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const result = info as Enhanced3DTilePickingInfo;
|
|
||||||
result.sourceTile = sourceTile;
|
|
||||||
|
|
||||||
if (info.picked && sourceTile) {
|
|
||||||
result.object = sourceTile;
|
|
||||||
const featureIds: ArrayLike<number> | undefined = sourceTile.content?.featureIds;
|
|
||||||
if (featureIds && featureIds.length > 0) {
|
|
||||||
const featureId = info.index;
|
|
||||||
if (featureId != null && featureId >= 0) {
|
|
||||||
result.featureId = featureId;
|
|
||||||
result.featureProperties = resolveFeatureProperties(sourceTile, featureId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterSubLayer({layer, viewport}: FilterContext): boolean {
|
|
||||||
const {tile} = layer.props as unknown as {tile: Tile3D};
|
|
||||||
return tile.selected && tile.viewportIds.includes(viewport.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _updateAutoHighlight(info: PickingInfo): void {
|
|
||||||
const sourceTile = (info as Enhanced3DTilePickingInfo).sourceTile;
|
|
||||||
const layerCache = sourceTile ? this.state.layerMap[sourceTile.id] : null;
|
|
||||||
if (layerCache?.layer) {
|
|
||||||
(layerCache.layer as any).updateAutoHighlight(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _loadTileset(tilesetUrl: string): Promise<void> {
|
|
||||||
const {loadOptions = {}} = this.props;
|
|
||||||
// @ts-ignore — support both deprecated `loader` and `loaders`
|
|
||||||
const loaders = this.props.loader || this.props.loaders;
|
|
||||||
const loader = Array.isArray(loaders) ? loaders[0] : loaders;
|
|
||||||
|
|
||||||
const options: any = {loadOptions: {...loadOptions}};
|
|
||||||
let actualTilesetUrl = tilesetUrl;
|
|
||||||
|
|
||||||
if (loader?.preload) {
|
|
||||||
const preloadOptions = await loader.preload(tilesetUrl, loadOptions);
|
|
||||||
if (preloadOptions.url) actualTilesetUrl = preloadOptions.url;
|
|
||||||
if (preloadOptions.headers) {
|
|
||||||
options.loadOptions.fetch = {
|
|
||||||
...options.loadOptions.fetch,
|
|
||||||
headers: preloadOptions.headers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Object.assign(options, preloadOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tilesetJson = await load(actualTilesetUrl, loader, options.loadOptions);
|
|
||||||
const tileset3d = new Tileset3D(tilesetJson, {
|
|
||||||
onTileLoad: this._onTileLoad.bind(this),
|
|
||||||
onTileUnload: this._onTileUnload.bind(this),
|
|
||||||
onTileError: this.props.onTileError,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({tileset3d, layerMap: {}});
|
|
||||||
this._updateTileset(this.state.activeViewports);
|
|
||||||
this.props.onTilesetLoad(tileset3d);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onTileLoad(tileHeader: Tile3D): void {
|
|
||||||
this._convertScenegraphToMesh(tileHeader);
|
|
||||||
this.props.onTileLoad(tileHeader);
|
|
||||||
this._updateTileset(this.state.lastUpdatedViewports);
|
|
||||||
this.setNeedsUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onTileUnload(tileHeader: Tile3D): void {
|
|
||||||
delete this.state.layerMap[tileHeader.id];
|
|
||||||
this.props.onTileUnload(tileHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateTileset(viewports: Record<string, Viewport> | null): void {
|
|
||||||
if (!viewports) return;
|
|
||||||
const {tileset3d} = this.state;
|
|
||||||
const {timeline} = this.context;
|
|
||||||
if (!timeline || !Object.keys(viewports).length || !tileset3d) return;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
tileset3d.selectTiles(Object.values(viewports)).then((frameNumber) => {
|
|
||||||
if (this.state.frameNumber !== frameNumber) {
|
|
||||||
this.setState({frameNumber});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Converts glTF SCENEGRAPH tiles to MESH in-place to enable per-feature picking and coloring. */
|
|
||||||
private _convertScenegraphToMesh(tileHeader: Tile3D): void {
|
|
||||||
const content = tileHeader.content;
|
|
||||||
if (!content?.gltf || content.attributes) return;
|
|
||||||
|
|
||||||
const gltf = content.gltf;
|
|
||||||
const prim = gltf.meshes?.[0]?.primitives?.[0];
|
|
||||||
if (!prim?.attributes?.POSITION) return;
|
|
||||||
|
|
||||||
const posAttr = prim.attributes.POSITION;
|
|
||||||
const normAttr = prim.attributes.NORMAL;
|
|
||||||
const featureIdRaw =
|
|
||||||
prim.attributes._FEATURE_ID_0 ||
|
|
||||||
prim.attributes._BATCHID ||
|
|
||||||
prim.attributes.BATCHID;
|
|
||||||
|
|
||||||
const rawPositions: Float32Array = posAttr.value || posAttr;
|
|
||||||
const rawNormals: Float32Array | undefined = normAttr?.value || normAttr;
|
|
||||||
const indices = prim.indices?.value || prim.indices;
|
|
||||||
|
|
||||||
const node = gltf.nodes?.find((n: any) => n.mesh != null);
|
|
||||||
const nodeMatrix: number[] | undefined = node?.matrix;
|
|
||||||
const positions = new Float32Array(rawPositions.length);
|
|
||||||
const normals = rawNormals ? new Float32Array(rawNormals.length) : undefined;
|
|
||||||
|
|
||||||
if (nodeMatrix && !isIdentityMatrix(nodeMatrix)) {
|
|
||||||
// Apply glTF node transform (affine: M * [x, y, z, 1])
|
|
||||||
for (let i = 0; i < rawPositions.length; i += 3) {
|
|
||||||
const x = rawPositions[i], y = rawPositions[i + 1], z = rawPositions[i + 2];
|
|
||||||
positions[i] = nodeMatrix[0] * x + nodeMatrix[4] * y + nodeMatrix[8] * z + nodeMatrix[12];
|
|
||||||
positions[i + 1] = nodeMatrix[1] * x + nodeMatrix[5] * y + nodeMatrix[9] * z + nodeMatrix[13];
|
|
||||||
positions[i + 2] = nodeMatrix[2] * x + nodeMatrix[6] * y + nodeMatrix[10] * z + nodeMatrix[14];
|
|
||||||
}
|
|
||||||
// Apply upper-3x3 to normals (no translation)
|
|
||||||
if (rawNormals && normals) {
|
|
||||||
for (let i = 0; i < rawNormals.length; i += 3) {
|
|
||||||
const x = rawNormals[i], y = rawNormals[i + 1], z = rawNormals[i + 2];
|
|
||||||
normals[i] = nodeMatrix[0] * x + nodeMatrix[4] * y + nodeMatrix[8] * z;
|
|
||||||
normals[i + 1] = nodeMatrix[1] * x + nodeMatrix[5] * y + nodeMatrix[9] * z;
|
|
||||||
normals[i + 2] = nodeMatrix[2] * x + nodeMatrix[6] * y + nodeMatrix[10] * z;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
positions.set(rawPositions);
|
|
||||||
if (rawNormals && normals) normals.set(rawNormals);
|
|
||||||
}
|
|
||||||
|
|
||||||
content.attributes = {
|
|
||||||
positions: {size: 3, value: positions},
|
|
||||||
...(normals ? {normals: {size: 3, value: normals}} : {}),
|
|
||||||
};
|
|
||||||
content.indices = indices || undefined;
|
|
||||||
if (prim.material) content.material = prim.material;
|
|
||||||
|
|
||||||
if (featureIdRaw) {
|
|
||||||
const rawValues = featureIdRaw.value || featureIdRaw;
|
|
||||||
const featureIds = new Uint32Array(rawValues.length);
|
|
||||||
for (let i = 0; i < rawValues.length; i++) featureIds[i] = rawValues[i];
|
|
||||||
content.featureIds = featureIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build per-feature property table from EXT_structural_metadata.
|
|
||||||
// loaders.gl resolves the binary arrays into typed JS arrays at load time.
|
|
||||||
const pt = gltf.extensions?.EXT_structural_metadata?.propertyTables?.[0];
|
|
||||||
if (pt) {
|
|
||||||
content.propertyTable = Array.from({length: pt.count || 0}, (_, i) => {
|
|
||||||
const row: Record<string, unknown> = {};
|
|
||||||
for (const [key, prop] of Object.entries(pt.properties || {})) {
|
|
||||||
const data = (prop as any).data;
|
|
||||||
if (Array.isArray(data)) row[key] = data[i];
|
|
||||||
}
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
(tileHeader as any).type = TILE_TYPE.MESH;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getSubLayer(
|
|
||||||
tileHeader: Tile3D,
|
|
||||||
oldLayer?: Layer,
|
|
||||||
): MeshLayer<DataT> | PointCloudLayer<DataT> | ScenegraphLayer<DataT> | null {
|
|
||||||
if (!tileHeader.content) return null;
|
|
||||||
switch (tileHeader.type as TILE_TYPE) {
|
|
||||||
case TILE_TYPE.POINTCLOUD:
|
|
||||||
return this._makePointCloudLayer(tileHeader, oldLayer as PointCloudLayer<DataT>);
|
|
||||||
case TILE_TYPE.SCENEGRAPH:
|
|
||||||
return this._make3DModelLayer(tileHeader);
|
|
||||||
case TILE_TYPE.MESH:
|
|
||||||
return this._makeSimpleMeshLayer(tileHeader, oldLayer as MeshLayer<DataT>);
|
|
||||||
default:
|
|
||||||
throw new Error(`Tile3DLayer2: unsupported tile type ${tileHeader.content.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _makePointCloudLayer(
|
|
||||||
tileHeader: Tile3D,
|
|
||||||
oldLayer?: PointCloudLayer<DataT>,
|
|
||||||
): PointCloudLayer<DataT> | null {
|
|
||||||
const {attributes, pointCount, constantRGBA, cartographicOrigin, modelMatrix} =
|
|
||||||
tileHeader.content;
|
|
||||||
const {positions, normals, colors} = attributes;
|
|
||||||
if (!positions) return null;
|
|
||||||
|
|
||||||
const data = (oldLayer && oldLayer.props.data) || {
|
|
||||||
header: {vertexCount: pointCount},
|
|
||||||
attributes: {POSITION: positions, NORMAL: normals, COLOR_0: colors},
|
|
||||||
};
|
|
||||||
|
|
||||||
const {pointSize, getPointColor} = this.props;
|
|
||||||
const SubLayerClass = this.getSubLayerClass('pointcloud', PointCloudLayer);
|
|
||||||
return new SubLayerClass(
|
|
||||||
{pointSize},
|
|
||||||
this.getSubLayerProps({id: 'pointcloud'}),
|
|
||||||
{
|
|
||||||
id: `${this.id}-pointcloud-${tileHeader.id}`,
|
|
||||||
tile: tileHeader,
|
|
||||||
data,
|
|
||||||
coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS,
|
|
||||||
coordinateOrigin: cartographicOrigin,
|
|
||||||
modelMatrix,
|
|
||||||
getColor: constantRGBA || getPointColor,
|
|
||||||
_offset: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _make3DModelLayer(tileHeader: Tile3D): ScenegraphLayer<DataT> {
|
|
||||||
const {gltf, instances, cartographicOrigin, modelMatrix} = tileHeader.content;
|
|
||||||
const SubLayerClass = this.getSubLayerClass('scenegraph', ScenegraphLayer);
|
|
||||||
return new SubLayerClass(
|
|
||||||
{_lighting: 'pbr'},
|
|
||||||
this.getSubLayerProps({id: 'scenegraph'}),
|
|
||||||
{
|
|
||||||
id: `${this.id}-scenegraph-${tileHeader.id}`,
|
|
||||||
tile: tileHeader,
|
|
||||||
data: instances || SINGLE_DATA,
|
|
||||||
scenegraph: gltf,
|
|
||||||
coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS,
|
|
||||||
coordinateOrigin: cartographicOrigin,
|
|
||||||
modelMatrix,
|
|
||||||
getTransformMatrix: (instance: any) => instance.modelMatrix,
|
|
||||||
getPosition: [0, 0, 0],
|
|
||||||
_offset: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _makeSimpleMeshLayer(
|
|
||||||
tileHeader: Tile3D,
|
|
||||||
oldLayer?: MeshLayer<DataT>,
|
|
||||||
): MeshLayer<DataT> {
|
|
||||||
const content = tileHeader.content;
|
|
||||||
const {
|
|
||||||
attributes,
|
|
||||||
indices,
|
|
||||||
modelMatrix,
|
|
||||||
cartographicOrigin,
|
|
||||||
coordinateSystem = COORDINATE_SYSTEM.METER_OFFSETS,
|
|
||||||
material,
|
|
||||||
featureIds,
|
|
||||||
} = content;
|
|
||||||
|
|
||||||
const {getFeatureColor, _getMeshColor} = this.props;
|
|
||||||
const mergedAttributes = buildMeshGeometry(attributes, featureIds, getFeatureColor, tileHeader);
|
|
||||||
const geometry =
|
|
||||||
(!getFeatureColor && oldLayer && oldLayer.props.mesh) ||
|
|
||||||
new Geometry({topology: 'triangle-list', attributes: mergedAttributes, indices});
|
|
||||||
|
|
||||||
const SubLayerClass = this.getSubLayerClass('mesh', MeshLayer);
|
|
||||||
return new SubLayerClass(
|
|
||||||
this.getSubLayerProps({id: 'mesh'}),
|
|
||||||
{
|
|
||||||
id: `${this.id}-mesh-${tileHeader.id}`,
|
|
||||||
tile: tileHeader,
|
|
||||||
mesh: geometry,
|
|
||||||
data: SINGLE_DATA,
|
|
||||||
getColor: getFeatureColor != null ? [255, 255, 255, 255] : _getMeshColor(tileHeader),
|
|
||||||
// Spread material into a new object so MeshLayer.updateState always calls
|
|
||||||
// updatePbrMaterialUniforms() after a geometry rebuild (binds GPU sampler to new model).
|
|
||||||
pbrMaterial: {...(material || {})},
|
|
||||||
modelMatrix,
|
|
||||||
coordinateOrigin: cartographicOrigin,
|
|
||||||
coordinateSystem,
|
|
||||||
featureIds,
|
|
||||||
_offset: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLayers(): Layer | null | LayersList {
|
|
||||||
const {tileset3d, layerMap} = this.state;
|
|
||||||
if (!tileset3d) return null;
|
|
||||||
|
|
||||||
return (tileset3d.tiles as Tile3D[])
|
|
||||||
.map((tile) => {
|
|
||||||
const layerCache = (layerMap[tile.id] = layerMap[tile.id] || {tile});
|
|
||||||
let {layer} = layerCache;
|
|
||||||
if (tile.selected) {
|
|
||||||
if (!layer) {
|
|
||||||
layer = this._getSubLayer(tile);
|
|
||||||
} else if (layerCache.needsUpdate) {
|
|
||||||
layer = this._getSubLayer(tile, layer as Layer);
|
|
||||||
layerCache.needsUpdate = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
layerCache.layer = layer;
|
|
||||||
return layer;
|
|
||||||
})
|
|
||||||
.filter(Boolean) as LayersList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveFeatureProperties(
|
|
||||||
tile: Tile3D,
|
|
||||||
featureId: number,
|
|
||||||
): Record<string, unknown> | undefined {
|
|
||||||
const content = tile.content;
|
|
||||||
if (!content) return undefined;
|
|
||||||
|
|
||||||
// 3D Tiles 1.1 — propertyTable built from EXT_structural_metadata
|
|
||||||
const propertyTable: Record<string, unknown>[] | undefined = content.propertyTable;
|
|
||||||
if (propertyTable) return propertyTable[featureId] as Record<string, unknown> | undefined;
|
|
||||||
|
|
||||||
// 3D Tiles 1.0 — batchTableJson (column-oriented arrays indexed by featureId)
|
|
||||||
const batchTableJson: Record<string, ArrayLike<unknown>> | undefined = content.batchTableJson;
|
|
||||||
if (batchTableJson) {
|
|
||||||
const properties: Record<string, unknown> = {};
|
|
||||||
for (const key of Object.keys(batchTableJson)) {
|
|
||||||
const col = batchTableJson[key];
|
|
||||||
if (Array.isArray(col) || ArrayBuffer.isView(col)) {
|
|
||||||
properties[key] = (col as any)[featureId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMeshGeometry(
|
|
||||||
contentAttributes: MeshAttributes,
|
|
||||||
featureIds: ArrayLike<number> | undefined,
|
|
||||||
getFeatureColor: ((featureId: number, tile: Tile3D) => Color) | null | undefined,
|
|
||||||
tileHeader: Tile3D,
|
|
||||||
): MeshAttributes {
|
|
||||||
const attributes: MeshAttributes = {
|
|
||||||
positions: {
|
|
||||||
...contentAttributes.positions,
|
|
||||||
// Always copy into a fresh array — geometry creation may mutate the buffer
|
|
||||||
value: new Float32Array(contentAttributes.positions.value as ArrayLike<number>),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (contentAttributes.normals) attributes.normals = contentAttributes.normals;
|
|
||||||
if (contentAttributes.texCoords) attributes.texCoords = contentAttributes.texCoords;
|
|
||||||
if (contentAttributes.uvRegions) attributes.uvRegions = contentAttributes.uvRegions;
|
|
||||||
|
|
||||||
if (getFeatureColor && featureIds && featureIds.length > 0) {
|
|
||||||
const colorBuffer = new Uint8Array(featureIds.length * 4);
|
|
||||||
for (let i = 0; i < featureIds.length; i++) {
|
|
||||||
const color = getFeatureColor(featureIds[i], tileHeader);
|
|
||||||
colorBuffer[i * 4] = color[0];
|
|
||||||
colorBuffer[i * 4 + 1] = color[1];
|
|
||||||
colorBuffer[i * 4 + 2] = color[2];
|
|
||||||
colorBuffer[i * 4 + 3] = color[3] ?? 255;
|
|
||||||
}
|
|
||||||
attributes.colors = {size: 4, value: colorBuffer, normalized: true};
|
|
||||||
} else if (getFeatureColor) {
|
|
||||||
// No featureIds — color all vertices uniformly via featureId 0
|
|
||||||
const vertexCount =
|
|
||||||
(contentAttributes.positions.value as ArrayLike<number>).length /
|
|
||||||
(contentAttributes.positions.size || 3);
|
|
||||||
const color = getFeatureColor(0, tileHeader);
|
|
||||||
const colorBuffer = new Uint8Array(vertexCount * 4);
|
|
||||||
for (let i = 0; i < vertexCount; i++) {
|
|
||||||
colorBuffer[i * 4] = color[0];
|
|
||||||
colorBuffer[i * 4 + 1] = color[1];
|
|
||||||
colorBuffer[i * 4 + 2] = color[2];
|
|
||||||
colorBuffer[i * 4 + 3] = color[3] ?? 255;
|
|
||||||
}
|
|
||||||
attributes.colors = {size: 4, value: colorBuffer, normalized: true};
|
|
||||||
} else if (contentAttributes.colors) {
|
|
||||||
attributes.colors = contentAttributes.colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const IDENTITY_4X4 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
||||||
|
|
||||||
function isIdentityMatrix(m: ArrayLike<number>): boolean {
|
|
||||||
if (m.length !== 16) return false;
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
if (Math.abs(m[i] - IDENTITY_4X4[i]) > 1e-6) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
16
src/main.tsx
16
src/main.tsx
@@ -1,16 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
import { MapComponentsProvider } from "@mapcomponents/react-maplibre";
|
|
||||||
import App from "./App";
|
|
||||||
import "./index.css";
|
|
||||||
import { DeckGlContextProvider } from "@mapcomponents/deck-gl";
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<MapComponentsProvider>
|
|
||||||
<DeckGlContextProvider>
|
|
||||||
<App />
|
|
||||||
</DeckGlContextProvider>
|
|
||||||
</MapComponentsProvider>
|
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
|
||||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2023",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noImplicitAny": false,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user