-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore(examples): Example with loading of slpk in browser implementation #2904
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
{ | ||
"name": "slpk-in-browser1", | ||
"version": "0.1.0", | ||
"private": true, | ||
"dependencies": { | ||
"@deck.gl/core": "^8.9.28", | ||
"@deck.gl/extensions": "^8.9.28", | ||
"@deck.gl/geo-layers": "^8.9.28", | ||
"@deck.gl/layers": "^8.9.28", | ||
"@deck.gl/mesh-layers": "^8.9.28", | ||
"@deck.gl/react": "^8.9.34", | ||
"@loaders.gl/core": "^4.0.0", | ||
"@loaders.gl/schema": "^4.0.0", | ||
"@loaders.gl/i3s": "^4.0.0", | ||
"@loaders.gl/loader-utils": "^4.0.0", | ||
"@loaders.gl/mvt": "^4.0.0", | ||
"@loaders.gl/terrain": "^4.0.0", | ||
"@testing-library/jest-dom": "^5.17.0", | ||
"@testing-library/react": "^13.4.0", | ||
"@testing-library/user-event": "^13.5.0", | ||
"@types/jest": "^27.5.2", | ||
"@types/node": "^16.18.83", | ||
"@types/react": "^18.2.58", | ||
"@types/react-dom": "^18.2.19", | ||
"maplibre-gl": "^2.4.0", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"react-scripts": "5.0.1", | ||
"typescript": "^4.9.5", | ||
"web-vitals": "^2.1.4" | ||
}, | ||
"scripts": { | ||
"start": "react-scripts start", | ||
"build": "react-scripts build", | ||
"eject": "react-scripts eject" | ||
}, | ||
"eslintConfig": { | ||
"extends": [ | ||
"react-app", | ||
"react-app/jest" | ||
] | ||
}, | ||
"browserslist": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need these? |
||
"production": [ | ||
">0.2%", | ||
"not dead", | ||
"not op_mini all" | ||
], | ||
"development": [ | ||
"last 1 chrome version", | ||
"last 1 firefox version", | ||
"last 1 safari version" | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<meta name="theme-color" content="#000000" /> | ||
<meta | ||
name="description" | ||
content="Web site created using create-react-app" | ||
/> | ||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> | ||
<!-- | ||
manifest.json provides metadata used when your web app is installed on a | ||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ | ||
--> | ||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> | ||
<!-- | ||
Notice the use of %PUBLIC_URL% in the tags above. | ||
It will be replaced with the URL of the `public` folder during the build. | ||
Only files inside the `public` folder can be referenced from the HTML. | ||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will | ||
work correctly both with client-side routing and a non-root public URL. | ||
Learn how to configure a non-root public URL by running `npm run build`. | ||
--> | ||
<title>React App</title> | ||
</head> | ||
<body> | ||
<noscript>You need to enable JavaScript to run this app.</noscript> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like some really old boilerplate... What browser doesn't have JS enabled these days? |
||
<div id="root"></div> | ||
<!-- | ||
This HTML file is a template. | ||
If you open it directly in the browser, you will see an empty page. | ||
You can add webfonts, meta tags, or analytics to this file. | ||
The build step will place the bundled scripts into the <body> tag. | ||
To begin the development, run `npm start` or `yarn start`. | ||
To create a production bundle, use `npm run build` or `yarn build`. | ||
--> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import DeckGL from '@deck.gl/react/typed'; | ||
|
||
import {TerrainLayer} from '@deck.gl/geo-layers/typed'; | ||
import {TerrainLoader} from '@loaders.gl/terrain'; | ||
import {parseSLPKArchive} from '@loaders.gl/i3s'; | ||
import {BrowserFile} from './browser-file'; | ||
|
||
|
||
const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line | ||
|
||
const INITIAL_VIEW_STATE = { | ||
latitude: 46.24, | ||
longitude: -122.18, | ||
zoom: 11.5, | ||
bearing: 140, | ||
pitch: 60, | ||
maxPitch: 89 | ||
}; | ||
|
||
const TERRAIN_IMAGE = `https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.png?access_token=${MAPBOX_TOKEN}`; | ||
const SURFACE_IMAGE = `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.png?access_token=${MAPBOX_TOKEN}`; | ||
|
||
// https://docs.mapbox.com/help/troubleshooting/access-elevation-data/#mapbox-terrain-rgb | ||
// Note - the elevation rendered by this example is greatly exagerated! | ||
const ELEVATION_DECODER = { | ||
rScaler: 6553.6, | ||
gScaler: 25.6, | ||
bScaler: 0.1, | ||
offset: -10000 | ||
}; | ||
|
||
function App({ | ||
texture = SURFACE_IMAGE, | ||
wireframe = false, | ||
initialViewState = INITIAL_VIEW_STATE | ||
}) { | ||
const [fileList, setFileList] = useState<FileList | null>(null) | ||
const [fetchObject, setFetchObject] = useState<({fetch: (path: string) => Promise<ArrayBuffer>}) | undefined>(undefined) | ||
|
||
useEffect(() => { | ||
if (!fileList) { | ||
return | ||
} | ||
const provider = new BrowserFile(fileList[0]) | ||
const setFetchAsync = async () => { | ||
const slpkFile = await parseSLPKArchive(provider) | ||
const fetch = async (path: string) => { | ||
return await slpkFile.getFile(path, "http") | ||
} | ||
|
||
//simple test, should show root json in console | ||
console.log(new TextDecoder().decode(await fetch(''))) | ||
|
||
setFetchObject({fetch}) | ||
} | ||
|
||
setFetchAsync().catch(console.error); | ||
|
||
}, [fileList]) | ||
|
||
const layer = new TerrainLayer({ | ||
id: 'layer', | ||
minZoom: 0, | ||
maxZoom: 23, | ||
strategy: 'no-overlap', | ||
elevationDecoder: ELEVATION_DECODER, | ||
elevationData: TERRAIN_IMAGE, | ||
texture, | ||
wireframe, | ||
color: [255, 255, 255], | ||
loadOptions: { | ||
terrain: { | ||
skirtHeight: 50 | ||
} | ||
}, | ||
loaders: [TerrainLoader] | ||
}); | ||
|
||
if (fileList) { | ||
if (fetchObject) { | ||
return <DeckGL initialViewState={initialViewState} controller={true} layers={[layer]} /> | ||
} else { | ||
return <div>Loading...</div> | ||
} | ||
} else { | ||
return <input type="file" onChange={(ev) => setFileList(ev.target.files)}/> | ||
} | ||
} | ||
|
||
export default App; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// loaders.gl | ||
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
import {FileProvider} from '@loaders.gl/loader-utils'; | ||
|
||
/** | ||
* Provides file data using node fs library | ||
* @deprecated - will be replaced with ReadableFile | ||
*/ | ||
export class BrowserFile implements FileProvider { | ||
/** The File object from which data is provided */ | ||
private file: File; | ||
|
||
/** Create a new BrowserFile */ | ||
constructor(file: File) { | ||
this.file = file; | ||
} | ||
/** | ||
* returns an ArrayBuffer whose contents are a copy of this file bytes from startOffset, inclusive, up to endOffset, exclusive. | ||
* @param start The offset, in byte, from the start of the file where to start reading the data. | ||
* @param lenght Length of read data | ||
*/ | ||
private async getBytesFromFile(start: number, lenght: number): Promise<ArrayBuffer> { | ||
let reader = new FileReader(); | ||
reader.readAsArrayBuffer(this.file.slice(start, start + lenght)); | ||
return new Promise<ArrayBuffer>((res, rej) => { | ||
reader.onload = function() { | ||
const arrayBuffer = reader.result | ||
if (!arrayBuffer || typeof arrayBuffer === 'string') { | ||
rej(new Error('something went wrong')); | ||
} else { | ||
res(arrayBuffer) | ||
} | ||
} | ||
}) | ||
} | ||
|
||
/** | ||
* Truncates the file descriptor. | ||
* @param length desired file lenght | ||
*/ | ||
async truncate(length: number): Promise<void> { | ||
throw new Error("file loaded in browser cannot be changed"); | ||
} | ||
|
||
/** | ||
* Append data to a file. | ||
* @param buffer data to append | ||
*/ | ||
async append(buffer: Uint8Array): Promise<void> { | ||
throw new Error("file loaded in browser cannot be changed"); | ||
} | ||
|
||
/** Close file */ | ||
async destroy(): Promise<void> { | ||
throw new Error("file loaded in browser cannot be changed"); | ||
} | ||
|
||
/** | ||
* Gets an unsigned 8-bit integer at the specified byte offset from the start of the file. | ||
* @param offset The offset, in bytes, from the start of the file where to read the data. | ||
*/ | ||
async getUint8(offset: number | bigint): Promise<number> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel you should separate this out of your File implementation. You read If you want a help class to wrap DataView that is fine, but I would suggest keeping it separate from |
||
const arrayBuffer = await this.getBytesFromFile(Number(offset), 1); | ||
const val = new Uint8Array(arrayBuffer).at(0); | ||
if (val === undefined) { | ||
throw new Error('something went wrong'); | ||
} | ||
return val; | ||
} | ||
|
||
/** | ||
* Gets an unsigned 16-bit integer at the specified byte offset from the start of the file. | ||
* @param offset The offset, in bytes, from the start of the file where to read the data. | ||
*/ | ||
async getUint16(offset: number | bigint): Promise<number> { | ||
const arrayBuffer = await this.getBytesFromFile(Number(offset), 2); | ||
const val = new Uint16Array(arrayBuffer).at(0); | ||
if (val === undefined) { | ||
throw new Error('something went wrong'); | ||
} | ||
return val; | ||
} | ||
|
||
/** | ||
* Gets an unsigned 32-bit integer at the specified byte offset from the start of the file. | ||
* @param offset The offset, in bytes, from the start of the file where to read the data. | ||
*/ | ||
async getUint32(offset: number | bigint): Promise<number> { | ||
const arrayBuffer = await this.getBytesFromFile(Number(offset), 4); | ||
const val = new Uint32Array(arrayBuffer).at(0); | ||
if (val === undefined) { | ||
throw new Error('something went wrong'); | ||
} | ||
return val; | ||
} | ||
|
||
/** | ||
* Gets an unsigned 32-bit integer at the specified byte offset from the start of the file. | ||
* @param offset The offset, in bytes, from the start of the file where to read the data. | ||
*/ | ||
async getBigUint64(offset: number | bigint): Promise<bigint> { | ||
const arrayBuffer = await this.getBytesFromFile(Number(offset), 8); | ||
const val = new BigInt64Array(arrayBuffer).at(0); | ||
if (val === undefined) { | ||
throw new Error('something went wrong'); | ||
} | ||
return val; | ||
} | ||
|
||
/** | ||
* returns an ArrayBuffer whose contents are a copy of this file bytes from startOffset, inclusive, up to endOffset, exclusive. | ||
* @param startOffset The offset, in byte, from the start of the file where to start reading the data. | ||
* @param endOffset The offset, in bytes, from the start of the file where to end reading the data. | ||
*/ | ||
async slice(startOffset: bigint, endOffset: bigint): Promise<ArrayBuffer> { | ||
const bigLength = endOffset - startOffset; | ||
if (bigLength > Number.MAX_SAFE_INTEGER) { | ||
throw new Error('too big slice'); | ||
} | ||
const length = Number(bigLength); | ||
|
||
return await this.getBytesFromFile(Number(startOffset), length); | ||
} | ||
|
||
/** | ||
* the length (in bytes) of the data. | ||
*/ | ||
get length(): bigint { | ||
return BigInt(this.file.size); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom/client'; | ||
import App from './App'; | ||
|
||
const root = ReactDOM.createRoot( | ||
document.getElementById('root') as HTMLElement | ||
); | ||
root.render( | ||
<React.StrictMode> | ||
<App /> | ||
</React.StrictMode> | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "es2020", | ||
"lib": [ | ||
"dom", | ||
"dom.iterable", | ||
"esnext" | ||
], | ||
"allowJs": true, | ||
"checkJs": false, | ||
"skipLibCheck": true, | ||
"esModuleInterop": true, | ||
"allowSyntheticDefaultImports": true, | ||
"strict": true, | ||
"forceConsistentCasingInFileNames": true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you really need all these options? Simpler is better. |
||
"noFallthroughCasesInSwitch": true, | ||
"module": "esnext", | ||
"moduleResolution": "node", | ||
"resolveJsonModule": true, | ||
"isolatedModules": true, | ||
"noEmit": true, | ||
"jsx": "react-jsx" | ||
}, | ||
"include": [ | ||
"src" | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a LOT of dependencies. Do we really need the test libraries for an example? Can they go into
devDependencies
?