├── test ├── __init__.py ├── python │ ├── __init__.py │ ├── test_laspointcloud.py │ ├── test_geomap.py │ ├── utils.py │ ├── test_raster_features.py │ ├── test_geojson_features.py │ ├── test_scenevalidator.py │ ├── test_basic_features.py │ └── test_pointcloud_features.py ├── data │ ├── utm.tif │ ├── polygons.shp │ ├── polygons.shx │ ├── test1_4.las │ ├── 100-points.las │ ├── polygons.dbf │ ├── rasterwithpalette.png │ ├── rasterwithpalette.tif │ └── polygons.prj ├── models │ ├── basic_model.json │ ├── geojson_model.json │ ├── shpfile_model.json │ ├── basic-features_model.json │ ├── raster-rgb_model.json │ ├── laspointcloud_100.json │ └── pointcloud-100_model.json ├── jasmine │ ├── jasmine.json │ ├── run.js │ └── geojsbuilder.spec.js ├── las │ ├── webpack.config.js │ └── las.pug ├── browser │ ├── README.md │ ├── rollup.config.js │ └── browser.pug └── README.md ├── requirements.txt ├── docs └── BasicScreenshot.png ├── src ├── declarations.d.ts ├── index.ts ├── pointcloud │ ├── index.d.ts │ ├── index.js │ ├── particlesystem.js │ └── laslaz.js ├── geojsextension.ts ├── pointcloudextension.ts └── geojsbuilder.ts ├── jupyterlab_geojs ├── __init__.py ├── geojslayer.py ├── types.py ├── geojsosmlayer.py ├── gdalutils.py ├── laspointcloud.py ├── geojsfeaturelayer.py ├── geojsonfeature.py ├── scenevalidator.py ├── geojsfeature.py ├── scene.py ├── pointcloudfeature.py ├── rasterfeature.py └── lasutils.py ├── tsconfig.json ├── project ├── schema │ ├── example_geojson.json │ ├── osmLayer.schema.yml │ ├── README.md │ ├── geojsonFeature.schema.yml │ ├── model.schema.yml │ ├── binaryDataFeature.schema.yml │ ├── combine_schemas.js │ ├── featureLayer.schema.yml │ └── model.schema.json └── docker │ └── thirdparty │ └── Dockerfile ├── .gitignore ├── style └── index.css ├── Dockerfile ├── setup.py ├── README.md ├── webpack.config.js ├── notebooks ├── ny-points.geojson ├── basic_map.ipynb ├── pointcloud.ipynb ├── geojson_data.ipynb └── basic_features.ipynb └── package.json /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/python/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | GDAL==2.2.2 2 | jsonschema==2.6.0 3 | matplotlib=2.2.2 4 | -------------------------------------------------------------------------------- /test/data/utm.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeoscience/jupyterlab_geojs/HEAD/test/data/utm.tif -------------------------------------------------------------------------------- /test/data/polygons.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeoscience/jupyterlab_geojs/HEAD/test/data/polygons.shp -------------------------------------------------------------------------------- /test/data/polygons.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeoscience/jupyterlab_geojs/HEAD/test/data/polygons.shx -------------------------------------------------------------------------------- /test/data/test1_4.las: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeoscience/jupyterlab_geojs/HEAD/test/data/test1_4.las -------------------------------------------------------------------------------- /docs/BasicScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeoscience/jupyterlab_geojs/HEAD/docs/BasicScreenshot.png -------------------------------------------------------------------------------- /test/data/100-points.las: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeoscience/jupyterlab_geojs/HEAD/test/data/100-points.las -------------------------------------------------------------------------------- /test/data/polygons.dbf: -------------------------------------------------------------------------------- 1 | _A WFIDN 0 1 2 -------------------------------------------------------------------------------- /test/data/rasterwithpalette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeoscience/jupyterlab_geojs/HEAD/test/data/rasterwithpalette.png -------------------------------------------------------------------------------- /test/data/rasterwithpalette.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeoscience/jupyterlab_geojs/HEAD/test/data/rasterwithpalette.tif -------------------------------------------------------------------------------- /test/data/polygons.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | // Placeholder for geojs declarations, per blog post https://bit.ly/2Jy6G7f 2 | declare module 'geojs' { 3 | var geo: any; 4 | export = geo 5 | } 6 | -------------------------------------------------------------------------------- /jupyterlab_geojs/__init__.py: -------------------------------------------------------------------------------- 1 | version_info = (0, 3, 3) 2 | __version__ = ".".join(map(str, version_info)) 3 | 4 | from .scene import Scene 5 | from .types import LayerType, FeatureType 6 | -------------------------------------------------------------------------------- /test/models/basic_model.json: -------------------------------------------------------------------------------- 1 | {"layers": [{"layerType": "osm", "options": {}}, {"features": [], "layerType": "feature", "options": {}}], "options": {"center": {"x": -73, "y": 42.5}, "zoom": 10}, "viewpoint": null} -------------------------------------------------------------------------------- /test/jasmine/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "test/jasmine", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; 2 | 3 | import { GeoJSExtension } from './geojsextension'; 4 | import { PointCloudExtension } from './pointcloudextension'; 5 | 6 | const extensions: Array = [GeoJSExtension, PointCloudExtension]; 7 | export default extensions; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "noImplicitAny": true, 5 | "noEmitOnError": true, 6 | "noUnusedLocals": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "target": "es2015", 10 | "outDir": "./lib", 11 | "lib": ["es2015", "dom"], 12 | "jsx": "react", 13 | "types": [] 14 | }, 15 | "include": ["src/*"] 16 | } 17 | -------------------------------------------------------------------------------- /test/las/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.v2.rules; 4 | 5 | module.exports = { 6 | entry: path.join(__dirname, '../../src/pointcloud'), 7 | output: { 8 | path: __dirname, 9 | filename: 'pointcloud.bundle.js', 10 | libraryTarget: 'umd', 11 | }, 12 | module: { 13 | rules: vtkRules, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/pointcloud/index.d.ts: -------------------------------------------------------------------------------- 1 | export class LASPointCloud { 2 | constructor(); 3 | dispose(): void; 4 | loadBuffers(arraybuffers: ArrayBuffer[]): Promise; 5 | loadFiles(files: Blob[]): Promise; 6 | bounds(): number[]; 7 | pointCount(): number; 8 | render(elem: HTMLElement): void; 9 | 10 | // For advanced users: 11 | static getLASHeader(arraybuffer: ArrayBuffer): Promise; 12 | setZRange(zmin: number, zmax: number): void; 13 | } 14 | -------------------------------------------------------------------------------- /project/schema/example_geojson.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers": [ 3 | { 4 | "layerType": "feature", 5 | "featureTypes": ["point", "quad"], 6 | "features": [ 7 | { 8 | "featureType": "geojson", 9 | "url": "http://someserver/somefile.geojson" 10 | } 11 | ] 12 | } 13 | ], 14 | "options": { 15 | "center": { 16 | "x": -75.0, 17 | "y": 40.0 18 | } 19 | }, 20 | "viewpoint": null 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | *.egg-info/ 4 | 5 | # Compiled javascript 6 | jupyterlab_geojs/__pycache__/ 7 | jupyterlab_geojs/labextension/ 8 | jupyterlab_geojs/nbextension/ 9 | lib/ 10 | 11 | test/browser/browser.bundle.js 12 | test/browser/browser.html 13 | test/las/las.html 14 | test/las/pointcloud.bundle.js 15 | 16 | # Runtime files 17 | *checkpoint.* 18 | jupyterlab_geojs/__logs__/*.log 19 | 20 | # Python distribution 21 | dist/ 22 | 23 | # OS X 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /jupyterlab_geojs/geojslayer.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class GeoJSLayer: 4 | '''Incomplete class listing common options for all layers. 5 | 6 | This is NOT a base class for concrete layer classes. 7 | The OptionNames list in this class should be included in all concrete layer classes 8 | ''' 9 | OptionNames = [ 10 | 'active', 11 | 'attribution', 12 | 'name', 13 | 'opacity', 14 | 'rendererName', 15 | 'visible', 16 | 'zIndex' 17 | ] 18 | -------------------------------------------------------------------------------- /test/browser/README.md: -------------------------------------------------------------------------------- 1 | # Browser Testbed 2 | 3 | This folder is used to create a browser.html file for testing data 4 | models passed from kernel code to the lab extension widget. 5 | 6 | To build the testbed: 7 | * Compile the browser.pug file 8 | 9 | To test the latest source code changes: 10 | * Compile typescript files by running ```jlpm build``` 11 | * Build a browser.bundle.js file by executing ```npm run build:browser```. 12 | * Load the resulting browser.html file into, yes, a web browser. 13 | -------------------------------------------------------------------------------- /project/schema/osmLayer.schema.yml: -------------------------------------------------------------------------------- 1 | #$schema: http://json-schema.org/draft-07/schema# 2 | title: Map OSM Layer (GeoJS Jupyter Model), 3 | description: An OSM layer contained inside the GeoJS Jupyter Model 4 | type: object 5 | properties: 6 | layerType: 7 | description: A literal identifying the layer type 8 | enum: 9 | - osm 10 | options: 11 | description: The options object passed in to the layer constructor 12 | type: object 13 | 14 | required: 15 | - layerType 16 | 17 | additionalProperties: false 18 | -------------------------------------------------------------------------------- /test/browser/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs'; 2 | import nodeResolve from 'rollup-plugin-node-resolve'; 3 | 4 | export default { 5 | input: 'lib/geojsbuilder.js', 6 | output: { 7 | file: 'test/browser/browser.bundle.js', 8 | format: 'iife', 9 | exports: 'named', 10 | name: 'jupyterlab_geojs', 11 | strict: false, 12 | }, 13 | plugins: [ 14 | nodeResolve({ 15 | jsnext: false, 16 | main: true 17 | }), 18 | commonjs({}) 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /project/docker/thirdparty/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image for installing thirdparty software 2 | # docker build -t jupyterlab_geojs/thirdparty . 3 | 4 | # Per jupyter docker-stacks: 5 | # https://github.com/jupyter/docker-stacks/tree/master/base-notebook 6 | # http://jupyter-docker-stacks.readthedocs.io/en/latest/index.html 7 | FROM jupyter/base-notebook 8 | 9 | # Install jupyterlab widget manager (needed for custom widgets) 10 | RUN jupyter labextension install @jupyter-widgets/jupyterlab-manager 11 | 12 | # Install python requirements (GDAL et al) 13 | RUN conda install --yes GDAL 14 | -------------------------------------------------------------------------------- /jupyterlab_geojs/types.py: -------------------------------------------------------------------------------- 1 | # Enumerated classes which can be used to specify layer or feature types. 2 | # These classes are for convenience only (you can always use a string). 3 | 4 | class LayerType: 5 | '''For Scene.create_layer()''' 6 | OSM = 'osm' 7 | FEATURE = 'feature' 8 | #UI = 'ui' # (future) 9 | 10 | 11 | class FeatureType: 12 | '''For GeoJSFeatureLayer.create_feature()''' 13 | GEOJSON = 'geojson' 14 | POINT = 'point' 15 | POINTCLOUD = 'pointcloud' 16 | POLYGON = 'polygon' 17 | QUAD = 'quad' 18 | RASTER = 'raster' 19 | -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | .jp-OutputWidgetGeoJS { 2 | min-height: 500px; 3 | overflow: auto; 4 | resize: vertical; 5 | width: 100%; 6 | } 7 | 8 | div.output_subarea.output_GeoJS { 9 | padding: 0.4em 0; 10 | max-width: 100%; 11 | } 12 | 13 | 14 | .jp-OutputWidgetLAS { 15 | height: 480px !important; 16 | overflow: auto; 17 | width: 100%; 18 | } 19 | 20 | div.output_subarea.output_LAS { 21 | padding: 0.4em 0; 22 | max-width: 100%; 23 | } 24 | 25 | .jp-TooltipGeoJS { 26 | background: lightblue; 27 | border-radius: 4px; 28 | opacity: 0.9; 29 | /*padding-right: 10px;*/ 30 | } 31 | -------------------------------------------------------------------------------- /test/models/geojson_model.json: -------------------------------------------------------------------------------- 1 | {"layers": [{"layerType": "osm", "options": {"renderer": "canvas"}}, {"features": [{"data": {"geometry": {"coordinates": [[[-78.878369, 42.886447], [-76.147424, 43.048122], [-75.910756, 43.974784], [-73.756232, 42.652579], [-75.917974, 42.098687], [-78.429927, 42.083639], [-78.878369, 42.886447]]], "type": "Polygon"}, "properties": {"author": "Kitware", "cities": ["Buffalo", "Syracuse", "Watertown", "Albany", "Binghamton", "Olean"]}, "type": "Feature"}, "featureType": "geojson", "options": {}}], "layerType": "feature", "options": {"features": ["point", "line", "polygon"]}}], "options": {"center": {"x": -76.5, "y": 43.0}, "zoom": 7}, "viewpoint": null} -------------------------------------------------------------------------------- /project/schema/README.md: -------------------------------------------------------------------------------- 1 | ## Data model for jupyterlab_geojs 2 | 3 | This folder contains the files associated with the internal 4 | representation used to send data from the notebook kernel to the GeoJS 5 | web client. Because GeoJS has a large API, a formal schema is used to 6 | specify the interface. 7 | 8 | The "source" files are authored in YAML, using the .yml extension. 9 | The entry point for the source is file model.schema.yml. 10 | 11 | For development, the node script combine_schemas.js is used to load the 12 | .yml files and generate a single json-schema file, model.schema.json. 13 | This file is used in conjunction with testing the notebook (python) 14 | code that generates data to send to the web client. 15 | -------------------------------------------------------------------------------- /project/schema/geojsonFeature.schema.yml: -------------------------------------------------------------------------------- 1 | #"$schema": "http://json-schema.org/draft-07/schema#" 2 | title: GeoJSON Feature (GeoJS Jupyter Model) 3 | description: A GeoJSON feature 4 | type: object 5 | properties: 6 | featureType: 7 | description: A literal indicating the feature type 8 | enum: 9 | - geojson 10 | options: 11 | description: Options passed in to createFeature() method 12 | type: object 13 | # Content represented either as data (object) or url 14 | data: 15 | description: This is the geojson object (not validated) 16 | type: object 17 | url: 18 | description: The url to a geojson file 19 | type: string 20 | 21 | additionalProperties: false 22 | required: 23 | - featureType 24 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Testing jupyterlab_geojs 2 | 3 | Tests are written for both kernel side (python) and client (javascript) code. 4 | To the extent practical: 5 | * Python tests generate data-model files. 6 | * Data-model files are validated using the model.schema.json file in the models folder. 7 | * Javascript tests are written to read the data-model files generated by the python tests. 8 | 9 | To run all tests, use this order 10 | 11 | ``` 12 | npm install # loads modules use for testing 13 | 14 | npm run build:schema # updates model.schema.json 15 | python -m unittest # tests python code, producing and validating data-model files 16 | jlpm build # generates js files in lib/ 17 | npm run test # tests javascript code (in lib/), using the data-model files 18 | ``` 19 | -------------------------------------------------------------------------------- /project/schema/model.schema.yml: -------------------------------------------------------------------------------- 1 | #$schema: http://json-schema.org/draft-07/schema# 2 | title: GoeJS Jupyter Model 3 | description: The data model passed from Jupyter kernel to server 4 | type: object 5 | properties: 6 | layers: 7 | description: The list of layers contained in the map 8 | type: array 9 | items: 10 | description: One layer in the map 11 | oneOf: 12 | - $ref: featureLayer.schema.yml 13 | - $ref: osmLayer.schema.yml 14 | # - $ref: uiLayer.schema.yml 15 | options: 16 | description: The options object passed in to the map constructor 17 | type: object 18 | viewpoint: 19 | description: The viewpoint specification (optional) 20 | type: 21 | - 'null' # (MUST BE IN QUOTES!) 22 | - object 23 | additionalProperties: false 24 | -------------------------------------------------------------------------------- /project/schema/binaryDataFeature.schema.yml: -------------------------------------------------------------------------------- 1 | #"$schema": "http://json-schema.org/draft-07/schema#" 2 | title: Binary Data Feature Type (GeoJS Jupyter Model) 3 | description: Any feature that is input as binary data 4 | type: object 5 | properties: 6 | featureType: 7 | description: A literal indicating the feature type 8 | enum: 9 | - pointcloud 10 | # - raster 11 | options: 12 | description: Options passed to createFeature() method 13 | type: object 14 | # Content represented either as data (uuencoded string) or url 15 | data: 16 | description: A uuencoded copy of the file contents 17 | type: [string, array] 18 | url: 19 | description: The url for downloading the data 20 | type: string 21 | 22 | additionalProperties: false 23 | required: 24 | - featureType 25 | -------------------------------------------------------------------------------- /test/models/shpfile_model.json: -------------------------------------------------------------------------------- 1 | {"layers": [{"layerType": "osm", "options": {}}, {"features": [{"data": {"features": [{"geometry": {"coordinates": [[[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]]], "type": "Polygon"}, "id": 0, "properties": {"FID": 0}, "type": "Feature"}, {"geometry": {"coordinates": [[[0.0, 0.0], [0.0, 4.0], [4.0, 4.0], [4.0, 0.0], [0.0, 0.0]], [[1.0, 1.0], [3.0, 1.0], [3.0, 3.0], [1.0, 3.0], [1.0, 1.0]]], "type": "Polygon"}, "id": 1, "properties": {"FID": 1}, "type": "Feature"}, {"geometry": {"coordinates": [[[[2.0, 2.0], [2.0, 3.0], [3.0, 3.0], [3.0, 2.0], [2.0, 2.0]]], [[[4.0, 4.0], [4.0, 5.0], [5.0, 5.0], [5.0, 4.0], [4.0, 4.0]]]], "type": "MultiPolygon"}, "id": 2, "properties": {"FID": 2}, "type": "Feature"}], "type": "FeatureCollection"}, "featureType": "geojson", "options": {}}], "layerType": "feature", "options": {"features": ["polygon"]}}], "options": {}, "viewpoint": null} -------------------------------------------------------------------------------- /test/python/test_laspointcloud.py: -------------------------------------------------------------------------------- 1 | """ 2 | DEPRECATED 3 | * Was used for developing standalone pointcloud display 4 | * Replaced with pointcloud feature in Scene objects 5 | """ 6 | 7 | 8 | import os 9 | import unittest 10 | 11 | from . import utils 12 | from jupyterlab_geojs import laspointcloud 13 | 14 | @unittest.skip('Deprecated LASPointCloud') 15 | class TestLASPointCloud(unittest.TestCase): 16 | '''Use unit test to generate test data for LASPointCloud 17 | 18 | ''' 19 | 20 | def test_las_100points(self): 21 | '''Test creating pointcloud feature''' 22 | filename = os.path.join(utils.data_folder, '100-points.las') 23 | 24 | las = laspointcloud.LASPointCloud(filename) 25 | self.assertEqual(las._feature.get_point_count(), 100) 26 | self.assertIsNone(las._feature.get_wkt_string()) 27 | 28 | display_model = las._feature._build_display_model() 29 | 30 | utils.write_model(display_model, 'laspointcloud_100.json') 31 | -------------------------------------------------------------------------------- /test/python/test_geomap.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | logging.basicConfig(level=logging.DEBUG) 5 | 6 | from . import utils 7 | from jupyterlab_geojs import Scene 8 | 9 | 10 | class TestGeoMap(unittest.TestCase): 11 | 12 | def test_basic_model(self): 13 | '''Test creating simple map with osm and feature layer''' 14 | scene = Scene(zoom=10) # pass in option to constructor 15 | scene.center = {'x': -73, 'y': 42.5} # set option as public member 16 | osm_layer = scene.create_layer('osm') 17 | feature_layer = scene.create_layer('feature') 18 | display_model = scene._build_display_model() 19 | #print(display_model) 20 | 21 | utils.validate_model(display_model) 22 | 23 | self.assertIsInstance(display_model, dict) 24 | self.assertTrue('options' in display_model) 25 | self.assertTrue('layers' in display_model) 26 | 27 | utils.write_model(display_model, 'basic_model.json') 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # docker build -t jupyterlab_geojs . 2 | # docker run -it --rm -p 7227:7227 --hostname localhost jupyterlab_geojs 3 | # Find the URL in the console and open browser to that url 4 | 5 | # You must first build the thirdparty image, which is at 6 | # project/docker/thirdparty/Dockerfile 7 | # (docker build -t jupyterlab_geojs/thirdparty project/docker/thirdparty) 8 | FROM jupyter/base-notebook 9 | 10 | # Install jupyterlab widget manager (needed for custom widgets) 11 | RUN jupyter labextension install @jupyter-widgets/jupyterlab-manager 12 | 13 | # Install python requirements (GDAL et al) 14 | USER root 15 | RUN conda install --yes GDAL 16 | 17 | # Copy source files 18 | ADD ./ /home/$NB_USER/jupyterlab_geojs 19 | RUN chown -R ${NB_UID}:${NB_UID} ${HOME} 20 | USER ${NB_USER} 21 | 22 | # Install JupyterLab extension 23 | WORKDIR /home/$NB_USER/jupyterlab_geojs 24 | RUN python setup.py install 25 | RUN jupyter labextension install . 26 | 27 | # Setup entry point 28 | WORKDIR /home/$NB_USER/jupyterlab_geojs/notebooks 29 | CMD ["jupyter", "lab", "--ip", "0.0.0.0"] 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | from jupyterlab_geojs import __version__ 4 | 5 | with open('README.md', 'r') as f: 6 | long_description = f.read() 7 | 8 | setuptools.setup( 9 | name = 'jupyterlab_geojs', 10 | version = __version__, 11 | author = 'john Tourtellott', 12 | author_email = 'john.tourtellott@kitware.com', 13 | description = 'A package for rendering GeoJS scenes in JupyterLab', 14 | long_description = long_description, 15 | long_description_content_type = 'text/markdown', 16 | url = 'https://github.com/OpenGeoscience/jupyterlab_geojs', 17 | license = 'BSD', 18 | platforms = 'Linux, Mac OS X, Windows', 19 | packages = ['jupyterlab_geojs'], 20 | classifiers = ( 21 | 'License :: OSI Approved :: BSD License', 22 | 'Programming Language :: Python :: 3' 23 | ), 24 | keywords = [ 25 | 'jupyter', 26 | 'jupyterlab', 27 | 'extension', 28 | 'geojs' 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /jupyterlab_geojs/geojsosmlayer.py: -------------------------------------------------------------------------------- 1 | from .geojslayer import GeoJSLayer 2 | 3 | class GeoJSOSMLayer: 4 | """A notebook class for representing OSM layers in GeoJS visualizations. 5 | 6 | """ 7 | OptionNames = GeoJSLayer.OptionNames 8 | 9 | def __init__(self, **kwargs): 10 | '''''' 11 | # Public members 12 | for name in self.__class__.OptionNames: 13 | value = kwargs.get(name) 14 | setattr(self, name, value) 15 | # Todo create attributes for any kwargs not in MemberNames, 16 | # for forward compatibility with GeoJS 17 | 18 | # Internal members 19 | self._options = kwargs 20 | 21 | def _build_display_model(self): 22 | '''''' 23 | display_model = dict() # return value 24 | display_model['layerType'] = 'osm' 25 | 26 | # Copy options that have been set 27 | for name in self.__class__.OptionNames: 28 | value = getattr(self, name, None) 29 | if value is not None: 30 | self._options[name] = value 31 | display_model['options'] = self._options 32 | 33 | return display_model 34 | -------------------------------------------------------------------------------- /test/python/utils.py: -------------------------------------------------------------------------------- 1 | '''Common test utility functions 2 | ''' 3 | import json 4 | import os 5 | 6 | import jsonschema 7 | 8 | source_folder = os.path.abspath(os.path.dirname(__file__)) 9 | root_folder = os.path.join(source_folder, os.pardir, os.pardir) 10 | 11 | 12 | test_folder = os.path.join(root_folder, 'test') 13 | data_folder = os.path.join(test_folder, 'data') 14 | model_folder = os.path.join(test_folder, 'models') 15 | 16 | 17 | # Load schema file 18 | schema = None 19 | schema_folder = os.path.join(root_folder, 'project', 'schema') 20 | schema_file = os.path.join(schema_folder, 'model.schema.json') 21 | with open(schema_file) as f: 22 | schema_string = f.read() 23 | schema = json.loads(schema_string) 24 | 25 | 26 | def validate_model(display_model): 27 | '''Validates input display model against schema 28 | 29 | Raises exception if invalid 30 | ''' 31 | print('validating against schema:') 32 | jsonschema.validate(display_model, schema) 33 | 34 | 35 | def write_model(display_model, filename, folder=model_folder): 36 | '''Writes display model as json file 37 | 38 | ''' 39 | path = os.path.join(folder, filename) 40 | model_string = json.dumps(display_model, sort_keys=True) 41 | #data_string = json.dumps(data, sort_keys=True, indent=2) 42 | with open(path, 'w') as f: 43 | f.write(model_string) 44 | print('Wrote {}'.format(path)) 45 | -------------------------------------------------------------------------------- /test/models/basic-features_model.json: -------------------------------------------------------------------------------- 1 | {"layers": [{"layerType": "osm", "options": {}}, {"features": [{"featureType": "point", "options": {"data": [{"__i": 0, "lat": 40.7127837, "lon": -74.0059413, "name": "New York", "population": 8405837}, {"__i": 1, "lat": 34.0522342, "lon": -118.2436849, "name": "Los Angeles", "population": 3884307}, {"__i": 2, "lat": 41.8781136, "lon": -87.6297982, "name": "Chicago", "population": 2718782}, {"__i": 3, "lat": 29.7604267, "lon": -95.3698028, "name": "Houston", "population": 2195914}, {"__i": 4, "lat": 39.9525839, "lon": -75.1652215, "name": "Philadelphia", "population": 1553165}, {"__i": 5, "lat": 33.4483771, "lon": -112.0740373, "name": "Phoenix", "population": 1513367}], "enableTooltip": true, "position": [{"x": -74.0059413, "y": 40.7127837}, {"x": -118.2436849, "y": 34.0522342}, {"x": -87.6297982, "y": 41.8781136}, {"x": -95.3698028, "y": 29.7604267}, {"x": -75.1652215, "y": 39.9525839}, {"x": -112.0740373, "y": 33.4483771}], "style": {"fillColor": ["#5e4fa2", "#9e0142", "#aedea3", "#fafdb8", "#535ca8", "#e04f4a"], "radius": [18, 12, 10, 9, 8, 8], "strokeColor": "black", "strokeWidth": 2}}}, {"featureType": "quad", "options": {"data": [{"lr": {"x": -100.9375, "y": 29.348416310781797}, "ul": {"x": -129.0625, "y": 42.13468456089552}}], "style": {"color": "magenta", "opacity": 0.2}}}], "layerType": "feature", "options": {"features": ["point", "quad"]}}], "options": {"center": {"x": -97.67, "y": 31.8}, "zoom": 4}, "viewpoint": null} -------------------------------------------------------------------------------- /project/schema/combine_schemas.js: -------------------------------------------------------------------------------- 1 | // Merge model schema source files 2 | // * using json-schema-ref-parser to build single json tree from yml inputs 3 | // * using ajv for minimal sanity check 4 | 5 | var fs = require('fs'); 6 | var Ajv = require('ajv'); 7 | var refParser = require('json-schema-ref-parser'); 8 | 9 | var refSchema = { 10 | "$schema": "http://json-schema.org/draft-07/schema#", 11 | "$ref": __dirname + "/model.schema.yml" 12 | }; 13 | 14 | refParser.dereference(refSchema) 15 | .then(function(schema) { 16 | var outString = JSON.stringify(schema, null, 2) + '\n'; 17 | //console.log(outString); 18 | var outFilename = __dirname + '/model.schema.json'; 19 | fs.writeFileSync(outFilename, outString); 20 | console.log(`Wrote ${outFilename}`); 21 | 22 | // Load test file 23 | var testFilename = __dirname + '/example_geojson.json'; 24 | var testText = fs.readFileSync(testFilename); 25 | var testJson = JSON.parse(testText); 26 | 27 | // Validate test file 28 | var ajv = new Ajv(); // options can be passed, e.g. {allErrors: true} 29 | var validate = ajv.compile(schema); 30 | //console.log(`schema: ${JSON.stringify(validate.schema, null, 2)}`); 31 | 32 | // if (!validate) { 33 | // console.error(validate.errors); 34 | // } 35 | var valid = validate(testJson); 36 | if (valid) { 37 | console.log('Test file is valid'); 38 | } 39 | else { 40 | console.log(validate.errors); 41 | } 42 | }) 43 | .catch(function(err) { 44 | console.error(err); 45 | }); 46 | -------------------------------------------------------------------------------- /test/jasmine/run.js: -------------------------------------------------------------------------------- 1 | // Run jasmine tests 2 | // We are using this script with babel-node in order to handle ES6 code 3 | // As described at https://gist.github.com/mauvm/172878a9646095d03fd7 4 | 5 | import { JSDOM } from 'jsdom'; 6 | import Jasmine from 'jasmine'; 7 | 8 | // Create jasmine instance 9 | var jasmine = new Jasmine(); 10 | // FYI the config file is NOT in the default path 11 | jasmine.loadConfigFile('test/jasmine/jasmine.json'); 12 | 13 | // Create dom instance and mocks required for testing geojs 14 | const dom = new JSDOM('
'); 15 | global.window = dom.window; 16 | global.document = dom.window.document; 17 | global.Image = dom.window.Image; // needed for 'osm' layer 18 | global.File = dom.window.File; // needed for geoFileReader 19 | 20 | // For some reason, jsdom not creating userAgent, so do it manually 21 | let userAgent = `Mozilla/5.0 (${process.platform}) AppleWebKit/537.36 (KHTML, like Gecko) jsdom`; 22 | global.navigator = { userAgent: userAgent }; 23 | 24 | // Fix strange problem detecting styles (setting variable isOldIE) 25 | global.self = { 26 | navigator: global.navigator 27 | } 28 | 29 | // This fixes nonfatal errors when testing raster features 30 | global.HTMLCanvasElement = window.HTMLCanvasElement; 31 | 32 | // For some reason, the inherit() needs to be mocked 33 | function newfunc() { 34 | return function() {}; 35 | } 36 | 37 | global.inherit = function(C, P) { 38 | var F = newfunc(); 39 | F.prototype = P.prototype; 40 | C.prototype = new F(); 41 | C.prototype.constructor = C; 42 | } 43 | 44 | // Run the tests 45 | jasmine.execute(); 46 | -------------------------------------------------------------------------------- /jupyterlab_geojs/gdalutils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A set of GDAL related utils 3 | ''' 4 | import pkg_resources 5 | 6 | try: 7 | pkg_resources.get_distribution('gdal') 8 | except pkg_resources.DistributionNotFound: 9 | HAS_GDAL = False 10 | else: 11 | HAS_GDAL = True 12 | from osgeo import gdal, osr 13 | 14 | 15 | def is_gdal_loaded(): 16 | return HAS_GDAL 17 | 18 | 19 | def convert_points_to_lonlat(points, projection_wkt): 20 | '''Converts an array of points to lonlat coordinates 21 | 22 | ''' 23 | if not is_gdal_loaded(): 24 | raise Exception('Cannot convert points because GDAL not loaded') 25 | 26 | from_spatial_ref = osr.SpatialReference() 27 | from_spatial_ref.ImportFromWkt(projection_wkt) 28 | 29 | lonlat_ref = osr.SpatialReference() 30 | lonlat_ref .ImportFromEPSG(4326) 31 | ref_transform = osr.CoordinateTransformation(from_spatial_ref, lonlat_ref) 32 | 33 | n = len(points) 34 | lonlat_points = [None] * n 35 | for i,point in enumerate(points): 36 | input_x = point[0] 37 | input_y = point[1] 38 | x,y,z = ref_transform.TransformPoint(input_x, input_y) 39 | lonlat_points[i] = [x, y] 40 | return lonlat_points 41 | 42 | def convert_wkt_to_proj(projection_wkt): 43 | '''Converts projection WKT string to Proj4 string 44 | 45 | ''' 46 | if not is_gdal_loaded(): 47 | raise Exception('Cannot convert projection WKT because GDAL not loaded') 48 | 49 | if projection_wkt is None: 50 | return None 51 | 52 | ref = osr.SpatialReference() 53 | ref.ImportFromWkt(wkt) 54 | proj4_string = ref.ExportToProj4() 55 | return proj4_string 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupyterlab_geojs 2 | 3 | [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/OpenGeoscience/jupyterlab_geojs/master) 4 | 5 | A JupyterLab notebook extension for rendering geospatial 6 | data using the GeoJS front end library 7 | 8 | ![Example Screenshot](./docs/BasicScreenshot.png) 9 | 10 | ## Prerequisites 11 | 12 | * JupyterLab ^0.32.1 and Notebook >=5.5.0 13 | 14 | ## Usage 15 | 16 | To render GeoJS output in JupyterLab: 17 | 18 | ```python 19 | from jupyterlab_geojs import Scene 20 | scene = Scene() 21 | osm_layer = scene.create_layer('osm') 22 | scene 23 | 24 | ``` 25 | 26 | The notebooks folder contains examples. 27 | 28 | 29 | ## Install 30 | 31 | ```bash 32 | # Install this lab extension 33 | jupyter labextension install @johnkit/jupyterlab_geojs 34 | 35 | # Also need to install the widget-manager extension 36 | jupyter labextension install @jupyter-widgets/jupyterlab-manager 37 | 38 | # Install the python package 39 | pip install jupyterlab_geojs 40 | 41 | ``` 42 | 43 | ## Development 44 | 45 | ```bash 46 | # Install python package 47 | pip install -e . 48 | 49 | # Install widget-manager extension 50 | jupyter labextension install @jupyter-widgets/jupyterlab-manager 51 | 52 | 53 | # Install js dependencies 54 | npm install 55 | # Build Typescript source 56 | jlpm build 57 | # Link your development version of the extension with JupyterLab 58 | jupyter labextension link . 59 | # Run 60 | jupyter lab 61 | 62 | 63 | # Rebuild Typescript source after making changes 64 | jlpm build 65 | # Rebuild JupyterLab after making any changes 66 | jupyter lab build 67 | ``` 68 | 69 | For testing, see README.md in test/ folder. 70 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | var vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.v2.rules; 5 | 6 | /* 7 | "JUPYTERLAB_FILE_LOADER_" is a special file prefix that indicates to 8 | jupyterlab to use its file loader to load the file, instead of 9 | bundling it with the rest of the extension files at build time. 10 | This allows the vtk.js code to be packaged separately, because it 11 | requries some webpack loaders that are not available using jlpm. 12 | 13 | This config generates two files in the lib folder: 14 | 1. JUPYTERLAB_FILE_LOADER_pointcloud.bundle.js, which is the 15 | point cloud implemenation. 16 | 2. JUPYTERLAB_FILE_LOADER_pointcloud.bundle.d.ts, which is a 17 | copy of the src/pointcloude/index.d.ts file. This file has the 18 | minimal definitions needed to successfully compile the 19 | extension ("jlpm build"). 20 | */ 21 | module.exports = { 22 | entry: path.join(__dirname, './src/pointcloud/index.js'), 23 | output: { 24 | path: path.join(__dirname, 'lib'), 25 | filename: 'JUPYTERLAB_FILE_LOADER_pointcloud.bundle.js', 26 | libraryTarget: 'umd', 27 | }, 28 | mode: 'development', 29 | devtool: 'source-map', 30 | module: { 31 | rules: vtkRules, 32 | }, 33 | plugins: [ 34 | new CopyWebpackPlugin([ 35 | { 36 | // See not above re JUPYTER_FILE_LOADER_ prefix 37 | from: './src/pointcloud/index.d.ts', 38 | to: 'JUPYTERLAB_FILE_LOADER_pointcloud.bundle.d.ts', 39 | toType: 'file' 40 | }, 41 | ]) // new CopyWebpackPlugin() 42 | ] // plugins 43 | }; 44 | -------------------------------------------------------------------------------- /test/models/raster-rgb_model.json: -------------------------------------------------------------------------------- 1 | {"layers": [{"layerType": "osm", "options": {}}, {"features": [{"featureType": "quad", "options": {"data": [{"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAADAFBMVEUAAAAA+QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABHa6DR3fkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADdycnYk4LtAACqAAAAAAAAAAAAAAAAAAAAAAAAAACyraP5+fkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABoqmMcYzC1yY4AAAAAAAAAAAAAAAAAAAAAAAAAAACljDDMunwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADi4sHJyXeZwUd3rZMAAAAAAAAAAAAAAAAAAAAAAADb2D2qcCgAAAAAAAAAAAAAAAAAAAAAAAAAAAC62Oq10+W10+W10+W10+Vwo7oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwN5zCAAAARUlEQVRoge3NwQAAIABFsW+RQ1L5W8Tw7pvAdrOXTSKRSCQSiUQikUgkEolEIpHE5GTrJBKJRCKRSCQSiUQikUgkEknzAXesGtv2dTlxAAAAAElFTkSuQmCC", "ll": {"x": -73.758345, "y": 41.849604}, "lr": {"x": -72.758345, "y": 41.849604}, "ul": {"x": -73.758345, "y": 42.849604}, "ur": {"x": -72.758345, "y": 42.849604}}], "style": {"opacity": 0.5}}}], "layerType": "feature", "options": {"features": ["quad.image"]}}], "options": {}, "viewpoint": {"bounds": {"bottom": 41.849604, "left": -73.758345, "right": -72.758345, "top": 42.849604}, "mode": "bounds"}} -------------------------------------------------------------------------------- /test/python/test_raster_features.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from . import utils 5 | from jupyterlab_geojs import Scene 6 | 7 | 8 | class TestRasterFeatures(unittest.TestCase): 9 | 10 | def test_rgb_image(self): 11 | '''Test creating raster feature''' 12 | filename = os.path.join(utils.data_folder, 'rasterwithpalette.tif') 13 | 14 | scene = Scene() 15 | # scene.center = {'x': -76.5, 'y': 43.0}; 16 | # scene.zoom = 7; 17 | scene.create_layer('osm'); 18 | feature_layer = scene.create_layer('feature', features=['quad.image']) 19 | quad = feature_layer.create_feature('raster', data=filename) 20 | quad.style = { 21 | 'opacity': 0.5 22 | } 23 | 24 | corners = quad.get_corner_points() 25 | scene.set_zoom_and_center(corners=corners) 26 | 27 | display_model = scene._build_display_model() 28 | #print(display_model) 29 | 30 | utils.validate_model(display_model) 31 | utils.write_model(display_model, 'raster-rgb_model.json') 32 | 33 | def test_utm_image(self): 34 | filename = os.path.join(utils.data_folder, 'utm.tif') 35 | 36 | scene = Scene() 37 | scene.center = {'x': -74.5, 'y': 6.0}; 38 | scene.zoom = 10; 39 | scene.create_layer('osm'); 40 | feature_layer = scene.create_layer('feature', features=['quad.image']) 41 | quad = feature_layer.create_feature('raster', data=filename) 42 | quad.style = { 43 | 'opacity': 0.8 44 | } 45 | 46 | display_model = scene._build_display_model() 47 | #print(display_model) 48 | 49 | # Write display model (don't need to validate again) 50 | utils.write_model(display_model, 'raster-utm_model.json') 51 | 52 | if __name__ == '__main__': 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /project/schema/featureLayer.schema.yml: -------------------------------------------------------------------------------- 1 | #$schema: http://json-schema.org/draft-07/schema# 2 | title: Map Feature Layer (GeoJS Jupyter Model), 3 | description: A feature layer contained inside the GeoJS Jupyter Model, 4 | type: object 5 | properties: 6 | layerType: 7 | description: A literal identifying the layer type 8 | enum: 9 | - feature 10 | 11 | options: 12 | description: The options object passed in to the layer constructor 13 | type: object 14 | 15 | featureTypes: 16 | description: A list of the feature types to use in this layer 17 | type: array 18 | items: 19 | anyOf: 20 | - enum: 21 | # - choropleth 22 | # - contour 23 | # - heatmap 24 | # - line 25 | # - pixelmap 26 | - point 27 | # - polygon 28 | - quad 29 | minItems: 1 30 | uniqueItems: true 31 | 32 | features: 33 | description: The list of map features 34 | type: array 35 | items: 36 | description: One map feature (which may contain multiple entities) 37 | type: object 38 | oneOf: 39 | - description: Generic geojs feature 40 | type: object 41 | properties: 42 | featureType: 43 | description: A literal indicating the feature type 44 | enum: 45 | - line 46 | - point 47 | - polygon 48 | - quad 49 | options: 50 | description: Options passed in to createFeature() method 51 | type: object 52 | required: 53 | - featureType 54 | 55 | - $ref: ./binaryDataFeature.schema.yml 56 | - $ref: ./geojsonFeature.schema.yml 57 | # $ref: ./choroplethFeature.schema 58 | # $ref: ./contourFeature.schema 59 | # $ref: ./heatmapFeature.schema 60 | # $ref: ./lineFeature.schema 61 | # $ref: ./pixelMapFeature.schema 62 | # $ref: ./pointFeature.schema 63 | # $ref: ./polygonFeature.schema 64 | 65 | required: 66 | - layerType 67 | 68 | additionalProperties: false 69 | -------------------------------------------------------------------------------- /notebooks/ny-points.geojson: -------------------------------------------------------------------------------- 1 | { "type": "FeatureCollection", 2 | "features": [ 3 | { 4 | "type": "Feature", 5 | "geometry": { 6 | "type": "Point", 7 | "coordinates": [-73.756232, 42.652579] 8 | }, 9 | "id": null, 10 | "properties": { 11 | "name": "Albany", 12 | "zipcode": 12201 13 | } 14 | }, 15 | 16 | { 17 | "type": "Feature", 18 | "geometry": { 19 | "type": "Point", 20 | "coordinates": [-75.917974, 42.098687] 21 | }, 22 | "id": null, 23 | "properties": { 24 | "name": "Binghamton", 25 | "zipcode": 13901 26 | } 27 | }, 28 | 29 | { 30 | "type": "Feature", 31 | "geometry": { 32 | "type": "Point", 33 | "coordinates": [-78.878369, 42.886447] 34 | }, 35 | "id": null, 36 | "properties": { 37 | "name": "Buffalo", 38 | "zipcode": 14201 39 | } 40 | }, 41 | 42 | { 43 | "type": "Feature", 44 | "geometry": { 45 | "type": "Point", 46 | "coordinates": [-74.005941, 40.712784] 47 | }, 48 | "id": null, 49 | "properties": { 50 | "name": "New York City", 51 | "zipcode": 10001 52 | } 53 | }, 54 | 55 | { 56 | "type": "Feature", 57 | "geometry": { 58 | "type": "Point", 59 | "coordinates": [-78.429927, 42.083639] 60 | }, 61 | "id": null, 62 | "properties": { 63 | "name": "Olean", 64 | "zipcode": 14760 65 | } 66 | }, 67 | { 68 | "type": "Feature", 69 | "geometry": { 70 | "type": "Point", 71 | "coordinates": [-75.063775, 42.452857] 72 | }, 73 | "id": null, 74 | "properties": { 75 | "name": "Oneonto", 76 | "zipcode": 13820 77 | } 78 | }, 79 | { 80 | "type": "Feature", 81 | "geometry": { 82 | "type": "Point", 83 | "coordinates": [-76.147424, 43.048122] 84 | }, 85 | "id": null, 86 | "properties": { 87 | "name": "Syracuse", 88 | "zipcode": 13201 89 | } 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /test/python/test_geojson_features.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import unittest 4 | 5 | logging.basicConfig(level=logging.DEBUG) 6 | 7 | from . import utils 8 | from jupyterlab_geojs import Scene 9 | 10 | ny_polygons = { "type": "Feature", 11 | "geometry": { 12 | "type": "Polygon", 13 | "coordinates": [[ 14 | [-78.878369, 42.886447], 15 | [-76.147424, 43.048122], 16 | [-75.910756, 43.974784], 17 | [-73.756232, 42.652579], 18 | [-75.917974, 42.098687], 19 | [-78.429927, 42.083639], 20 | [-78.878369, 42.886447] 21 | ]] 22 | }, 23 | "properties": { 24 | "author": "Kitware", 25 | "cities": ["Buffalo", "Syracuse", "Watertown", "Albany", "Binghamton", "Olean"] 26 | } 27 | } 28 | 29 | class TestGeoJSONFeatures(unittest.TestCase): 30 | 31 | def test_geojson_features(self): 32 | '''Test creating geojson features''' 33 | scene = Scene() 34 | scene.center = {'x': -76.5, 'y': 43.0}; 35 | scene.zoom = 7; 36 | scene.create_layer('osm', renderer='canvas'); 37 | feature_layer = scene.create_layer('feature', features=['point', 'line', 'polygon']) 38 | feature_layer.create_feature('geojson', data=ny_polygons) 39 | 40 | display_model = scene._build_display_model() 41 | #print(data) 42 | 43 | # Validate display model against schema 44 | utils.validate_model(display_model) 45 | 46 | # Optionally write result to model file 47 | utils.write_model(display_model, 'geojson_model.json') 48 | 49 | def test_shpfile_features(self): 50 | '''Test creating geojson feature from shp file''' 51 | scene = Scene() 52 | osm_layer = scene.create_layer('osm'); 53 | feature_layer = scene.create_layer('feature', features=['polygon']) 54 | 55 | filename = os.path.join(utils.data_folder, 'polygons.shp') 56 | feature = feature_layer.create_feature('polygon', filename) 57 | display_model = scene._build_display_model() 58 | utils.validate_model(display_model) 59 | utils.write_model(display_model, 'shpfile_model.json') 60 | 61 | if __name__ == '__main__': 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /jupyterlab_geojs/laspointcloud.py: -------------------------------------------------------------------------------- 1 | """ 2 | DEPRECATED 3 | * Was used for developing standalone pointcloud display 4 | * Replaced with pointcloud feature in Scene objects 5 | """ 6 | 7 | 8 | import logging 9 | 10 | from IPython.display import display, JSON 11 | 12 | from .pointcloudfeature import PointCloudFeature 13 | 14 | # An interim display class for point cloud data, 15 | # essentially a wrapper for PointCloudFeature with new mime type 16 | # Usage 17 | # from jupyterlab_geojs import LASPointCloud 18 | # LASPointCloud(LASFilename) 19 | 20 | MIME_TYPE = 'application/las+json' 21 | 22 | 23 | class LASPointCloud(JSON): 24 | def __init__(self, filename, **kwargs): 25 | """A display class for displaying pointcloud data in JupyterLab notebooks 26 | """ 27 | super(LASPointCloud, self).__init__() 28 | self._feature = PointCloudFeature(filename=filename) 29 | self._logger = None 30 | 31 | 32 | def create_logger(self, folder, filename='laspointcloud.log'): 33 | '''Initialize logger with file handler 34 | 35 | @param folder (string) directory to store logfile 36 | ''' 37 | os.makedirs(folder, exist_ok=True) # create folder if needed 38 | 39 | log_name, ext = os.path.splitext(filename) 40 | self._logger = logging.getLogger(log_name) 41 | self._logger.setLevel(logging.INFO) # default 42 | 43 | log_path = os.path.join(folder, filename) 44 | fh = logging.FileHandler(log_path, 'w') 45 | self._logger.addHandler(fh) 46 | return self._logger 47 | 48 | 49 | def _ipython_display_(self): 50 | '''''' 51 | if self._logger is not None: 52 | self._logger.debug('Enter LASPointCloud._ipython_display_()') 53 | 54 | display_model = self._feature._build_display_model() 55 | bundle = { 56 | MIME_TYPE: display_model, 57 | 'text/plain': '' 58 | } 59 | metadata = { 60 | MIME_TYPE: self.metadata 61 | } 62 | if self._logger is not None: 63 | self._logger.debug('display bundle: {}'.format(bundle)) 64 | self._logger.debug('metadata: {}'.format(metadata)) 65 | display(bundle, metadata=metadata, raw=True) 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@johnkit/jupyterlab_geojs", 3 | "version": "0.3.3", 4 | "description": "A package for rendering GeoJS scenes in JupyterLab", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/*.d.ts", 9 | "lib/*.js", 10 | "style/*.*" 11 | ], 12 | "directories": { 13 | "lib": "lib/" 14 | }, 15 | "keywords": [ 16 | "jupyter", 17 | "jupyterlab", 18 | "jupyterlab-extension" 19 | ], 20 | "jupyterlab": { 21 | "mimeExtension": true 22 | }, 23 | "dependencies": { 24 | "@jupyter-widgets/base": "^1.2.1", 25 | "@jupyterlab/application": "^0.17.2", 26 | "@jupyterlab/docregistry": "^0.17.2", 27 | "@jupyterlab/notebook": "^0.17.2", 28 | "@jupyterlab/rendermime-interfaces": "^1.1.2", 29 | "@phosphor/disposable": "^1.1.2", 30 | "@phosphor/messaging": "^1.2.2", 31 | "@phosphor/widgets": "^1.5.0", 32 | "@types/base64-arraybuffer": "^0.1.0", 33 | "@types/react-dom": "^16.0.6", 34 | "babel-cli": "^6.26.0", 35 | "base64-arraybuffer": "^0.1.5", 36 | "colorkit": "^1.2.0", 37 | "geojs": "^0.17.0", 38 | "react": "^16.4.1", 39 | "vtk.js": "^7.4.10" 40 | }, 41 | "devDependencies": { 42 | "ajv": "^6.5.2", 43 | "babel-preset-env": "^1.7.0", 44 | "canvas-prebuilt": "^1.6.5-prerelease.1", 45 | "copy-webpack-plugin": "^4.5.2", 46 | "jasmine": "^3.1.0", 47 | "jsdom": "^11.12.0", 48 | "json-schema-ref-parser": "^5.1.2", 49 | "kw-web-suite": "^6.2.0", 50 | "pug-cli": "^1.0.0-alpha6", 51 | "rimraf": "~2.6.2", 52 | "rollup": "^0.63.5", 53 | "rollup-plugin-commonjs": "^9.1.4", 54 | "rollup-plugin-node-resolve": "^3.3.0", 55 | "typescript": "~2.6.2" 56 | }, 57 | "scripts": { 58 | "build": "webpack && tsc", 59 | "build:browser": "tsc && node node_modules/rollup/bin/rollup --config test/browser/rollup.config.js", 60 | "build:extension": "tsc", 61 | "build:las-test": "webpack --config test/las/webpack.config.js", 62 | "build:schema": "node ./project/schema/combine_schemas.js", 63 | "clean": "rimraf lib", 64 | "prepack": "npm run clean && npm run build", 65 | "test": "node node_modules/babel-cli/bin/babel-node test/jasmine/run.js", 66 | "watch": "tsc -w" 67 | }, 68 | "babel": { 69 | "presets": [ 70 | "env" 71 | ] 72 | }, 73 | "author": "Kitware", 74 | "license": "BSD-3-Clause", 75 | "repository": "https://github.com/OpenGeoscience/jupyterlab_geojs" 76 | } 77 | -------------------------------------------------------------------------------- /test/python/test_scenevalidator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import unittest 4 | 5 | from . import utils 6 | 7 | logging.basicConfig(level=logging.DEBUG) 8 | 9 | from jupyterlab_geojs import Scene 10 | 11 | 12 | class TestSceneValidator(unittest.TestCase): 13 | 14 | def test_pointcloud_osm(self): 15 | '''pointcloud cannot be used in scene with osm layer''' 16 | scene = Scene() 17 | scene.create_layer('osm') 18 | feature_layer = scene.create_layer('feature') 19 | filename = os.path.join(utils.data_folder, '100-points.las') 20 | #feature_layer.create_feature('pointcloud', data=filename) 21 | self.assertRaises(Exception, feature_layer.create_feature, 'pointcloud', data=filename) 22 | 23 | def test_osm_pointcloud(self): 24 | '''osm layer cannot be added to scene with pointcloud feature''' 25 | scene = Scene() 26 | feature_layer = scene.create_layer('feature') 27 | filename = os.path.join(utils.data_folder, '100-points.las') 28 | feature_layer.create_feature('pointcloud', data=filename) 29 | self.assertRaises(Exception, scene.create_layer, 'osm') 30 | 31 | def test_pointcloud_point(self): 32 | '''pointcloud cannot be used in scene with point feature''' 33 | scene = Scene() 34 | feature_layer = scene.create_layer('feature') 35 | feature_layer.create_feature('point', data=[]) 36 | filename = os.path.join(utils.data_folder, '100-points.las') 37 | #feature_layer.create_feature('pointcloud', data=filename) 38 | self.assertRaises( 39 | Exception, feature_layer.create_feature, 'pointcloud', data=filename) 40 | 41 | def test_point_pointcloud(self): 42 | '''pointcloud feature cannot be added to scene with point feature''' 43 | scene = Scene() 44 | feature_layer = scene.create_layer('feature') 45 | filename = os.path.join(utils.data_folder, '100-points.las') 46 | feature_layer.create_feature('pointcloud', data=filename) 47 | #feature_layer.create_feature('point') 48 | self.assertRaises(Exception, feature_layer.create_feature, 'point', data=[]) 49 | 50 | def test_pointcloud_pointcloud(self): 51 | '''only one pointcloud feature cannot be added to scene''' 52 | scene = Scene() 53 | feature_layer = scene.create_layer('feature') 54 | filename = os.path.join(utils.data_folder, '100-points.las') 55 | feature_layer.create_feature('pointcloud', data=filename) 56 | 57 | #feature_layer.create_feature('pointcloud', data=filename) 58 | self.assertRaises( 59 | Exception, feature_layer.create_feature, 'pointcloud', data=filename) 60 | 61 | 62 | if __name__ == '__main__': 63 | unittest.main() 64 | -------------------------------------------------------------------------------- /jupyterlab_geojs/geojsfeaturelayer.py: -------------------------------------------------------------------------------- 1 | from .geojsfeature import GeoJSFeature 2 | from .geojslayer import GeoJSLayer 3 | from .geojsonfeature import GeoJSONFeature 4 | from .pointcloudfeature import PointCloudFeature 5 | from .rasterfeature import RasterFeature 6 | from .scenevalidator import SceneValidator 7 | from .types import FeatureType 8 | 9 | 10 | class GeoJSFeatureLayer: 11 | """A notebook class for representing feature layers in GeoJS visualizations. 12 | 13 | """ 14 | OptionNames = GeoJSLayer.OptionNames + ['selectionAPI'] 15 | 16 | def __init__(self, **kwargs): 17 | # Public members 18 | for name in self.__class__.OptionNames: 19 | value = kwargs.get(name) 20 | setattr(self, name, value) 21 | # Todo create attributes for any kwargs not in MemberNames, 22 | # for forward compatibility with GeoJS 23 | 24 | # Internal members 25 | self._options = kwargs 26 | self._features = list() 27 | self._validator = SceneValidator() 28 | 29 | def create_feature(self, feature_type, data, **kwargs): 30 | '''API method to add features to this layer''' 31 | self._validator.adding_feature(self, feature_type) 32 | 33 | # Handle special cases first 34 | if feature_type == FeatureType.GEOJSON: 35 | feature = GeoJSONFeature(data, **kwargs) 36 | elif feature_type == FeatureType.POINTCLOUD: 37 | feature = PointCloudFeature(data, **kwargs) 38 | elif feature_type == FeatureType.POLYGON and isinstance(data, str): 39 | # Special case: polygon with string data represents shp file 40 | # Load shp file as geojson feature 41 | feature = GeoJSONFeature(data, **kwargs) 42 | elif feature_type == FeatureType.RASTER: 43 | feature = RasterFeature(data, **kwargs) 44 | else: 45 | feature = GeoJSFeature(feature_type, data=data, **kwargs) 46 | 47 | self._features.append(feature) 48 | return feature 49 | 50 | def clear(self): 51 | '''Removes all features from this layer 52 | 53 | ''' 54 | self._validator.clearing_layer(self) 55 | del self._features[:] 56 | 57 | 58 | def _build_display_model(self): 59 | '''''' 60 | display_model = dict() # return value 61 | display_model['layerType'] = 'feature' 62 | 63 | # Copy options that have been set 64 | for name in self.__class__.OptionNames: 65 | value = getattr(self, name, None) 66 | if value is not None: 67 | self._options[name] = value 68 | display_model['options'] = self._options 69 | 70 | feature_data = list() 71 | for feature in self._features: 72 | feature_data.append(feature._build_display_model()) 73 | display_model['features'] = feature_data 74 | 75 | return display_model 76 | -------------------------------------------------------------------------------- /jupyterlab_geojs/geojsonfeature.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from urllib.parse import urlparse 4 | 5 | from . import gdalutils 6 | from .geojsfeature import GeoJSFeature 7 | 8 | class GeoJSONFeature(GeoJSFeature): 9 | '''''' 10 | def __init__(self, data, **kwargs): 11 | super(GeoJSONFeature, self).__init__('geojson', config_options=False, **kwargs) 12 | self._json_data = None 13 | self._uri = None 14 | 15 | # Determine if input data is filename, uri, or raw data 16 | filename = None 17 | if isinstance(data, str): 18 | parsed_url = urlparse(data) 19 | if not bool(parsed_url.scheme): 20 | # If not scheme, then we presume it is a file/path 21 | filename = data 22 | elif parsed_url.scheme == 'file': 23 | # If scheme is a file, strip the scheme and use the rest as file/path 24 | n = len('file://') 25 | filename = data[n:] 26 | else: 27 | # Otherwise infer it is a network url 28 | self._url = data 29 | elif isinstance(data, dict): 30 | self._json_data = data 31 | else: 32 | raise Exception('Unrecognized input data not a string or dict: {}'.format(data)) 33 | 34 | if filename is not None: 35 | # Load data here, because javascript cannot load from 36 | # filesystem due to browser security restriction. 37 | if not os.path.exists(filename): 38 | raise Exception('Cannot find file {}'.format(filename)) 39 | 40 | root, ext = os.path.splitext(filename) 41 | if ext == '.shp': 42 | # Convert shape files to geojson format 43 | self._json_data = self._convert_shp(filename) 44 | else: 45 | # Logic for standard geojson files 46 | with open(filename) as f: 47 | text = f.read() 48 | self._json_data = json.loads(text) 49 | 50 | 51 | def _convert_shp(self, filename): 52 | '''Convert shp files to geojson data 53 | 54 | Returns feature collection object 55 | ''' 56 | if not gdalutils.is_gdal_loaded(): 57 | raise Exception('Cannot process .shp files because GDAL is not loaded') 58 | 59 | from osgeo import gdal, ogr 60 | # Create GeoJSON feature collection 61 | fc = { 62 | 'type': 'FeatureCollection', 63 | 'features': [] 64 | } 65 | dataset = ogr.Open(filename, gdal.GA_ReadOnly) 66 | layer = dataset.GetLayer() 67 | for feature in layer: 68 | fc['features'].append(feature.ExportToJson(as_object=True)) 69 | return fc 70 | 71 | def _build_display_model(self): 72 | display_model = super(GeoJSONFeature, self)._build_display_model() 73 | if self._json_data is not None: 74 | display_model['data'] = self._json_data 75 | elif self._url is not None: 76 | display_model['url'] = self._url 77 | return display_model 78 | -------------------------------------------------------------------------------- /test/browser/browser.pug: -------------------------------------------------------------------------------- 1 | html(lang="en") 2 | head 3 | meta(charset="utf-8") 4 | title Lab/GeoJS 5 | script(src="vue.min.js") 6 | style. 7 | body {font-family: Verdana;} 8 | button, input, textarea { 9 | font-size: 12px; 10 | } 11 | #map { 12 | border: 1px solid #333; 13 | min-height: 400px; 14 | overflow: auto; 15 | resize: vertical; 16 | } 17 | .model-string { 18 | height: 100px; 19 | resize: vertical; 20 | width: 100%; 21 | } 22 | .jp-TooltipGeoJS { 23 | background: lightblue; 24 | border-radius: 4px; 25 | opacity: 0.9; 26 | padding-right: 10px; /* causes artifacts */ 27 | } 28 | body 29 | div 30 | h3 jupyterlab_geojs model browser 31 | div#vue 32 | p 33 | label(for="modelFile") 34 | input(type="file" name="modelFile" v-model="modelFileName" @change="onFileChange") 35 | p 36 | label(for="modelString") 37 | textarea.model-string(name="modelString", v-model="modelString" placeholder="Load data model as string") 38 | p 39 | button(@click="draw") Draw 40 | button(@click="resize") Resize 41 | div#map 42 | 43 | script(src="browser.bundle.js") 44 | script. 45 | new Vue({ 46 | el: '#vue', 47 | data: function() { 48 | return { 49 | builder: null, 50 | modelFileName: '', 51 | modelString: '', 52 | } 53 | }, // data 54 | methods: { 55 | draw: function() { 56 | this.builder.clear(); 57 | let model = JSON.parse(this.modelString); 58 | //console.dir(model); 59 | let node = document.getElementById('map'); 60 | this.builder.generate(node, model) 61 | .then((geomap) => geomap.draw()); 62 | }, // draw() 63 | onFileChange: function(evt) { 64 | //console.dir(evt.target); 65 | let files = evt.target.files; 66 | if (files.length === 0) { 67 | this.modelString = ''; 68 | return; 69 | } 70 | 71 | let f = files[0]; 72 | //console.dir(f); 73 | 74 | // Only process json files. 75 | if (!f.type.match('application/json')) { 76 | alert(`Cannot load ${f.name} -- not json`); 77 | return; 78 | } 79 | 80 | // Read contents 81 | let reader = new FileReader(); 82 | reader.onload = evt => { 83 | //- console.log(`result: ${evt}`); 84 | //- console.dir(evt); 85 | this.modelString = evt.target.result; 86 | //- console.log(vm.modelString); 87 | }; 88 | reader.onerror = err => { 89 | alert(err); 90 | } 91 | reader.readAsText(f); 92 | }, // onFileChange() 93 | resize () { 94 | window.dispatchEvent(new Event('resize')); 95 | }, // resize() 96 | }, // methods 97 | mounted: function() { 98 | // Set default model string 99 | this.modelString = '{"options": {"zoom": 10, "center": {"x": -73, "y": 42.5}}, "layers": [{"layerType": "osm", "options": {}}, {"layerType": "feature", "options": {}, "features": []}]}'; 100 | // Instantiate GeoJSBuilder 101 | this.builder = new jupyterlab_geojs.GeoJSBuilder(); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /test/python/test_basic_features.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | import unittest 4 | 5 | try: 6 | import matplotlib as mpl 7 | import matplotlib.cm 8 | MPL_LOADED = True 9 | except ImportError: 10 | MPL_LOADED = False 11 | print('matplotlib loaded? {}'.format(MPL_LOADED)) 12 | 13 | logging.basicConfig(level=logging.DEBUG) 14 | 15 | from . import utils 16 | from jupyterlab_geojs import Scene, LayerType, FeatureType 17 | 18 | 19 | class TestBasicFeatures(unittest.TestCase): 20 | 21 | def test_quad_feature(self): 22 | '''Test creating simple map with osm and feature layer''' 23 | scene = Scene(zoom=10) # pass in option to constructor 24 | scene.center = {'x': -97.67, 'y': 31.80} # set option as public member 25 | scene.zoom = 4 26 | osm_layer = scene.create_layer(LayerType.OSM) 27 | feature_layer = scene.create_layer( 28 | LayerType.FEATURE, features=[FeatureType.POINT, FeatureType.QUAD]) 29 | 30 | # Point data 31 | cities = [ 32 | {'lon': -74.0059413, 'lat': 40.7127837, 'name': "New York", 'population': 8405837}, 33 | {'lon': -118.2436849, 'lat': 34.0522342, 'name': "Los Angeles", 'population': 3884307}, 34 | {'lon': -87.6297982, 'lat': 41.8781136, 'name': "Chicago", 'population': 2718782}, 35 | {'lon': -95.3698028, 'lat': 29.7604267, 'name': "Houston", 'population': 2195914}, 36 | {'lon': -75.1652215, 'lat': 39.9525839, 'name': "Philadelphia", 'population': 1553165}, 37 | {'lon': -112.0740373, 'lat': 33.4483771, 'name': "Phoenix", 'population': 1513367} 38 | ] 39 | # Use lambda function to set point positions 40 | position = lambda city: {'x':city['lon'], 'y':city['lat']} 41 | 42 | style = {'strokeColor': 'black', 'strokeWidth': 2} 43 | 44 | # Use lambda function to set point radius 45 | populations = [city['population'] for city in cities] 46 | pmin = min(populations) 47 | # Scale area proportional to population 48 | rmin = 8 # minimum radius 49 | style['radius'] = lambda city: int(math.sqrt(rmin*rmin*city['population']/pmin)) 50 | 51 | point_feature = feature_layer.create_feature( 52 | FeatureType.POINT, cities, position=position, style=style) 53 | point_feature.enableTooltip = True # adds ui layer in JS but NOT in python 54 | 55 | # Apply colormap to longitude 56 | if MPL_LOADED: 57 | cmap = mpl.cm.get_cmap('Spectral') 58 | lons = [city['lon'] for city in cities] 59 | lon_norm = mpl.colors.Normalize(vmin=min(lons), vmax=max(lons)) 60 | style['fillColor'] = lambda city: cmap(lon_norm(city['lon'])) 61 | point_feature.style = style 62 | else: 63 | point_feature.colormap = { 64 | 'colorseries': 'rainbow', 65 | 'field': 'lon', 66 | 'range': [-118.2436849, -74.0059413] 67 | } 68 | 69 | 70 | # Quad data 71 | feature_data = [{ 72 | # Copied from http://opengeoscience.github.io/geojs/tutorials/video_on_map/ 73 | 'ul': {'x': -129.0625, 'y': 42.13468456089552}, 74 | 'lr': {'x': -100.9375, 'y': 29.348416310781797} 75 | }] 76 | quad = feature_layer.create_feature(FeatureType.QUAD, feature_data) 77 | quad.style = { 78 | 'color': 'magenta', 79 | 'opacity': 0.2 80 | } 81 | 82 | # Build display model 83 | display_model = scene._build_display_model() 84 | print(display_model) 85 | 86 | # Validate display model against schema 87 | utils.validate_model(display_model) 88 | 89 | # Optionally write result to model file 90 | utils.write_model(display_model, 'basic-features_model.json') 91 | 92 | if __name__ == '__main__': 93 | unittest.main() 94 | -------------------------------------------------------------------------------- /src/geojsextension.ts: -------------------------------------------------------------------------------- 1 | import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; 2 | 3 | import { Widget } from '@phosphor/widgets'; 4 | 5 | import { GeoJSBuilder } from './geojsbuilder.js'; 6 | 7 | import '../style/index.css'; 8 | 9 | 10 | /** 11 | * The default mime type for the extension. 12 | */ 13 | const MIME_TYPE = 'application/geojs+json'; 14 | 15 | 16 | /** 17 | * The class name added to the extension. 18 | */ 19 | const CLASS_NAME = 'jp-OutputWidgetGeoJS'; 20 | 21 | 22 | /** 23 | * A widget for rendering GeoJS. 24 | */ 25 | export 26 | class OutputWidget extends Widget implements IRenderMime.IRenderer { 27 | /** 28 | * Construct a new output widget. 29 | */ 30 | constructor(options: IRenderMime.IRendererOptions) { 31 | super(); 32 | this._mimeType = options.mimeType; 33 | this.addClass(CLASS_NAME); 34 | 35 | // Keep reference to geomap, for resizing and dispose 36 | this._geoMap = null; 37 | } 38 | 39 | /** 40 | * Dispose of the widget 41 | */ 42 | dispose(): void { 43 | // Dispose of the geojs map 44 | if (!!this._geoMap) { 45 | console.debug('Disposing geo.map'); 46 | this._geoMap.exit(); 47 | this._geoMap = null; 48 | } 49 | super.dispose(); 50 | } 51 | 52 | /** 53 | * Handle widget resize 54 | */ 55 | onResize(msg?: Widget.ResizeMessage): void { 56 | if (!this._geoMap) { 57 | return; 58 | } 59 | // Update map to its element size 60 | this._geoMap.size({ 61 | width: this._geoMap.node().width(), 62 | height: this._geoMap.node().height() 63 | }); 64 | } 65 | 66 | /** 67 | * Render GeoJS into this widget's node. 68 | */ 69 | renderModel(model: IRenderMime.IMimeModel): Promise { 70 | //console.log(`OutputWidget.renderModel() ${this._mimeType}`); 71 | //console.dir(model); 72 | //this.node.textContent = model.data[this._mimeType]; 73 | const data = model.data[this._mimeType] as any; 74 | //const metadata = model.metadata[this._mimeType] as any || {}; 75 | 76 | const mapModel = data; 77 | if (!mapModel) { 78 | console.error('mapModel missing'); 79 | } 80 | 81 | // Make sure any existing map is removed 82 | if (!!this._geoMap) { 83 | console.debug('Deleting geo.map instance'); 84 | this._geoMap.exit(); // safety first 85 | this._geoMap = null; 86 | } 87 | 88 | let builder = new GeoJSBuilder(); 89 | // Return promise that resolves when builder generates map 90 | return new Promise((resolve, reject) => { 91 | builder.generate(this.node, mapModel) 92 | .then((geoMap:any) => { 93 | this._geoMap = geoMap; 94 | // Need resize event to get map to fill widget 95 | this.onResize(); 96 | this._geoMap.draw(); 97 | resolve(); 98 | }, reject => console.error(reject)) 99 | .catch(error => { reject(error); }); 100 | }); 101 | } // renderModel() 102 | 103 | private _geoMap: any; 104 | private _mimeType: string; 105 | } // OutputWidget 106 | 107 | 108 | /** 109 | * A mime renderer factory for GeoJS data. 110 | */ 111 | export 112 | const rendererFactory: IRenderMime.IRendererFactory = { 113 | safe: true, 114 | mimeTypes: [MIME_TYPE], 115 | createRenderer: options => new OutputWidget(options) 116 | }; 117 | 118 | export 119 | const GeoJSExtension: IRenderMime.IExtension = { 120 | id: 'jupyterlab_geojs:factory', 121 | rendererFactory, 122 | rank: 0, 123 | dataType: 'json', 124 | documentWidgetFactoryOptions: { 125 | name: 'GeoJS', 126 | primaryFileType: 'geojson', 127 | fileTypes: ['geojson'], 128 | defaultFor: [] 129 | }, 130 | fileTypes: [ 131 | { 132 | name: 'geojs', 133 | mimeTypes: [MIME_TYPE], 134 | extensions: ['.geojs'] 135 | } 136 | ] 137 | 138 | }; 139 | -------------------------------------------------------------------------------- /test/python/test_pointcloud_features.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from . import utils 5 | from jupyterlab_geojs import Scene, gdalutils 6 | 7 | 8 | class TestPointCloudFeatures(unittest.TestCase): 9 | 10 | def test_las_100points(self): 11 | '''Test creating pointcloud feature''' 12 | filename = os.path.join(utils.data_folder, '100-points.las') 13 | 14 | scene = Scene() 15 | feature_layer = scene.create_layer('feature') 16 | pointcloud = feature_layer.create_feature('pointcloud', data=filename) 17 | self.assertEqual(pointcloud.get_point_count(), 100) 18 | self.assertEqual(pointcloud.get_point_data_record_formats(), {3: 100}) 19 | self.assertEqual(pointcloud.get_point_count_by_return(), (89, 10, 1, 0, 0)) 20 | self.assertIsNone(pointcloud.get_wkt_string()) 21 | 22 | bounds = pointcloud.get_bounds() 23 | min_x = 635717.85 24 | max_z = 530.61 25 | self.assertAlmostEqual(bounds[0], min_x) 26 | self.assertAlmostEqual(bounds[5], max_z) 27 | 28 | # atts = pointcloud.get_point_attributes() 29 | # print(atts) 30 | 31 | display_model = scene._build_display_model() 32 | 33 | utils.write_model(display_model, 'pointcloud-100_model.json') 34 | utils.validate_model(display_model) 35 | 36 | # data should contain "data" field 37 | layers = display_model.get('layers', {}) 38 | features = layers[0].get('features') 39 | feature = features[0] 40 | self.assertIsNotNone(feature) 41 | self.assertTrue('data' in feature) 42 | 43 | def test_las_v14(self): 44 | '''Test that creating LAS version 1.4 not supported''' 45 | # Load as data instead of filename 46 | filename = os.path.join(utils.data_folder, 'test1_4.las') 47 | with open(filename, 'rb') as f: 48 | data = f.read() 49 | self.assertGreater(len(data), 30000) # (sanity check) 50 | 51 | scene = Scene() 52 | feature_layer = scene.create_layer('feature') 53 | 54 | #pointcloud = feature_layer.create_feature('pointcloud', data=data) 55 | self.assertRaises(Exception, feature_layer.create_feature, 'pointcloud', data=data) 56 | 57 | # self.assertEqual(pointcloud.get_point_count(), 1000) 58 | # self.assertEqual(pointcloud.get_point_count_by_return(), (974, 23, 2, 1, 0)) 59 | 60 | # bounds = pointcloud.get_bounds() 61 | # min_x = 1694038.4456376971 62 | # max_z = 5599.069686454539 63 | # self.assertAlmostEqual(bounds[0], min_x) 64 | # self.assertAlmostEqual(bounds[5], max_z) 65 | 66 | # self.assertIsNotNone(pointcloud.get_wkt_string()) 67 | # # print(pointcloud.get_point_data_record_format()) 68 | # # atts = pointcloud.get_point_attributes() 69 | # # print(atts) 70 | 71 | # if gdalutils.is_gdal_loaded(): 72 | # lonlat_bounds = pointcloud.get_bounds(as_lonlat=True) 73 | # #print('lonlat_bounds: {}'.format(lonlat_bounds)) 74 | # min_x = -106.068729935 # (lon) 75 | # max_y = 35.992260517 # (lat) 76 | # self.assertAlmostEqual(lonlat_bounds[0], min_x, 6) # lon min 77 | # self.assertAlmostEqual(lonlat_bounds[3], max_y, 6) # lat max 78 | 79 | # data = scene._build_display_model() 80 | 81 | # #utils.write_model(data, 'test1_4.json') 82 | # utils.validate_model(data) 83 | 84 | # # data should contain "data" field 85 | # layers = data.get('layers', {}) 86 | # features = layers[0].get('features') 87 | # feature = features[0] 88 | # self.assertIsNotNone(feature) 89 | # self.assertTrue('data' in feature) 90 | 91 | # data = scene._build_display_model() 92 | #print(data) 93 | #utils.write_model(data, 'test1_4.json') 94 | 95 | 96 | if __name__ == '__main__': 97 | unittest.main() 98 | -------------------------------------------------------------------------------- /notebooks/basic_map.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "data": { 10 | "application/geojs+json": { 11 | "layers": [ 12 | { 13 | "layerType": "osm", 14 | "options": {} 15 | } 16 | ], 17 | "options": { 18 | "center": { 19 | "x": -73.756887, 20 | "y": 42.849488, 21 | "z": 0 22 | }, 23 | "node": { 24 | "jQuery331059263068042862431": { 25 | "events": { 26 | "contextmenu": [ 27 | { 28 | "guid": 249, 29 | "namespace": "geojs", 30 | "origType": "contextmenu", 31 | "type": "contextmenu" 32 | } 33 | ], 34 | "dragover": [ 35 | { 36 | "guid": 242, 37 | "namespace": "geo", 38 | "origType": "dragover", 39 | "type": "dragover" 40 | } 41 | ], 42 | "dragstart": [ 43 | { 44 | "guid": 248, 45 | "namespace": "", 46 | "origType": "dragstart", 47 | "type": "dragstart" 48 | } 49 | ], 50 | "drop": [ 51 | { 52 | "guid": 243, 53 | "namespace": "geo", 54 | "origType": "drop", 55 | "type": "drop" 56 | } 57 | ], 58 | "mousedown": [ 59 | { 60 | "guid": 246, 61 | "namespace": "geojs", 62 | "origType": "mousedown", 63 | "type": "mousedown" 64 | } 65 | ], 66 | "mousemove": [ 67 | { 68 | "guid": 245, 69 | "namespace": "geojs", 70 | "origType": "mousemove", 71 | "type": "mousemove" 72 | } 73 | ], 74 | "mouseup": [ 75 | { 76 | "guid": 247, 77 | "namespace": "geojs", 78 | "origType": "mouseup", 79 | "type": "mouseup" 80 | } 81 | ], 82 | "wheel": [ 83 | { 84 | "guid": 244, 85 | "namespace": "geojs", 86 | "origType": "wheel", 87 | "type": "wheel" 88 | } 89 | ] 90 | } 91 | }, 92 | "jQuery331059263068042862432": { 93 | "dataGeojsMap": {} 94 | } 95 | }, 96 | "zoom": 10 97 | }, 98 | "viewpoint": null 99 | }, 100 | "text/plain": [ 101 | "" 102 | ] 103 | }, 104 | "metadata": { 105 | "application/geojs+json": { 106 | "expanded": false 107 | } 108 | }, 109 | "output_type": "display_data" 110 | } 111 | ], 112 | "source": [ 113 | "from jupyterlab_geojs import Scene\n", 114 | "scene = Scene(center={'y': 42.849488, 'x': -73.756887}, zoom=10)\n", 115 | "osm_layer = scene.create_layer('osm')\n", 116 | "scene\n" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [] 125 | } 126 | ], 127 | "metadata": { 128 | "kernelspec": { 129 | "display_name": "Python 3", 130 | "language": "python", 131 | "name": "python3" 132 | }, 133 | "language_info": { 134 | "codemirror_mode": { 135 | "name": "ipython", 136 | "version": 3 137 | }, 138 | "file_extension": ".py", 139 | "mimetype": "text/x-python", 140 | "name": "python", 141 | "nbconvert_exporter": "python", 142 | "pygments_lexer": "ipython3", 143 | "version": "3.5.2" 144 | } 145 | }, 146 | "nbformat": 4, 147 | "nbformat_minor": 2 148 | } 149 | -------------------------------------------------------------------------------- /src/pointcloudextension.ts: -------------------------------------------------------------------------------- 1 | import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; 2 | 3 | import { JSONObject } from '@phosphor/coreutils'; 4 | import { Widget } from '@phosphor/widgets'; 5 | 6 | import { decode as base64Decode } from 'base64-arraybuffer'; 7 | 8 | import { LASPointCloud } from '../lib/JUPYTERLAB_FILE_LOADER_pointcloud.bundle.js'; 9 | 10 | // Local interface definitions - do these need to be exported? 11 | export interface IFeatureModel { 12 | data: string[]; 13 | featureType: string; 14 | options?: JSONObject; 15 | url?: string; 16 | } 17 | 18 | export interface ILayerModel { 19 | features?: IFeatureModel[]; 20 | layerType: string; 21 | options?: JSONObject; 22 | } 23 | 24 | export interface IMapModel { 25 | layers?: ILayerModel[]; 26 | options?: JSONObject; 27 | viewpoint?: JSONObject; 28 | } 29 | 30 | 31 | 32 | import '../style/index.css'; 33 | 34 | 35 | /** 36 | * The default mime type for the extension. 37 | */ 38 | const MIME_TYPE = 'application/las+json'; 39 | 40 | 41 | /** 42 | * The class name added to the extension. 43 | */ 44 | const CLASS_NAME = 'jp-OutputWidgetLAS'; 45 | 46 | 47 | /** 48 | * A widget for rendering pointcloud data; uses vtk.js 49 | */ 50 | export 51 | class OutputWidget extends Widget implements IRenderMime.IRenderer { 52 | /** 53 | * Construct a new output widget. 54 | */ 55 | constructor(options: IRenderMime.IRendererOptions) { 56 | super(); 57 | this._mimeType = options.mimeType; 58 | this.addClass(CLASS_NAME); 59 | } 60 | 61 | /** 62 | * Render into this widget's node. 63 | * Current code only supports ONE pointcloud 64 | */ 65 | async renderModel(model: IRenderMime.IMimeModel): Promise { 66 | //console.log(`OutputWidget.renderModel() ${this._mimeType}`); 67 | //console.dir(model); 68 | const mapModel = model.data[this._mimeType] as any; 69 | if (!mapModel) { 70 | console.error('mapModel missing'); 71 | } 72 | 73 | let pointcloud = new LASPointCloud(); 74 | let buffers: ArrayBuffer[] = []; 75 | 76 | let layerModels: ILayerModel[] = mapModel.layers || []; 77 | for (let layerModel of layerModels) { 78 | let featureModels: IFeatureModel[] = layerModel.features; 79 | for (let featureModel of featureModels) { 80 | // console.log('featureModel:') 81 | // console.dir(featureModel); 82 | const lasArray: string[] = featureModel.data; 83 | for (let lasString of lasArray) { 84 | const binaryData: ArrayBuffer = base64Decode(lasString); 85 | buffers.push(binaryData); 86 | } // lasString 87 | } // for (featureModel) 88 | } // for (layerModel) 89 | pointcloud.loadBuffers(buffers) 90 | .then(() => { 91 | if (!pointcloud.pointCount()) { 92 | // If we reach here, then no pointcloud features detected, so... 93 | //console.log('No pointcloud feature'); 94 | this.node.textContent = 'No pointcloud feature to display'; 95 | return Promise.resolve(); 96 | } 97 | // (else) 98 | return pointcloud.render(this.node); 99 | }); 100 | } // renderModel() 101 | 102 | private _mimeType: string; 103 | } // OutputWidget 104 | 105 | 106 | /** 107 | * A mime renderer factory for GeoJS data. 108 | */ 109 | export 110 | const rendererFactory: IRenderMime.IRendererFactory = { 111 | safe: true, 112 | mimeTypes: [MIME_TYPE], 113 | createRenderer: options => new OutputWidget(options) 114 | }; 115 | 116 | export 117 | const PointCloudExtension: IRenderMime.IExtension = { 118 | id: 'jupyterlab_geojs:las_factory', 119 | rendererFactory, 120 | rank: 0, 121 | dataType: 'json', 122 | documentWidgetFactoryOptions: { 123 | name: 'LASPointCloud', 124 | primaryFileType: 'las', 125 | fileTypes: ['las'], 126 | defaultFor: [] 127 | }, 128 | fileTypes: [ 129 | { 130 | name: 'las', 131 | mimeTypes: [MIME_TYPE], 132 | extensions: ['.las'] 133 | } 134 | ] 135 | 136 | }; 137 | -------------------------------------------------------------------------------- /jupyterlab_geojs/scenevalidator.py: -------------------------------------------------------------------------------- 1 | """Validates scenes before creating layers and features, 2 | 3 | Currently, this extension can render maps containing either, 4 | but not both: 5 | * Regular GeoJS layers and features 6 | * One feature layer with one point cloud feature 7 | 8 | To make things work via a single class (Scene), this class 9 | checks each new layer and feature as it is created, and raises an 10 | Exception if the current limitations are not met. The intent is to 11 | isolate as much of the validity-checking code as possible, with the 12 | expectation it will not be needed in the future, once geojs can 13 | support vtk.js layers. 14 | """ 15 | 16 | from enum import Enum 17 | 18 | class _SceneMode(Enum): 19 | '''For specifying a model on map instance; used by scene validator 20 | 21 | ''' 22 | UNASSIGNED = 'unassigned' # scene contains no osm layer or no features 23 | GEOJS = 'geojs' # scene contains osm layer or non-pointcloud features 24 | POINTCLOUD = 'pointcloud' # scen contains pointcloud feature(s) 25 | 26 | 27 | class _SceneValidator: 28 | instance = None 29 | 30 | def __init__(self): 31 | # Enforce singleton pattern 32 | if _SceneValidator.instance: 33 | raise Exception('Error creating _SceneValidator - use scene_validator instead') 34 | 35 | self._mapDict = dict() # 36 | self._layerDict = dict() # 37 | 38 | def adding_map(self, map): 39 | '''This must be called when a new Scene instance is initialized 40 | 41 | ''' 42 | assert(not map in self._mapDict) 43 | self._mapDict[map] = _SceneMode.UNASSIGNED 44 | 45 | def adding_layer(self, map, layer_type): 46 | mode = self._mapDict[map] 47 | #print('Adding layer type {} to map mode {}'.format(layer_type, mode)) 48 | if mode == _SceneMode.UNASSIGNED: 49 | if layer_type == 'osm': 50 | self._mapDict[map] = _SceneMode.GEOJS 51 | elif mode == _SceneMode.POINTCLOUD: 52 | raise Exception('Cannot add osm layer to scene containing a pointcloud object') 53 | 54 | 55 | def added_layer(self, map, layer): 56 | '''This must be called AFTER each layer is added to a map 57 | 58 | ''' 59 | self._layerDict[layer] = map 60 | 61 | def adding_feature(self, layer, feature_type): 62 | '''This must be called BEFORE adding a feature 63 | 64 | ''' 65 | map = self._layerDict[layer] 66 | mode = self._mapDict[map] 67 | #print('Adding feature type {} to map mode {}'.format(feature_type, mode)) 68 | if mode == _SceneMode.UNASSIGNED: 69 | if feature_type == 'pointcloud': 70 | self._mapDict[map] = _SceneMode.POINTCLOUD 71 | else: 72 | self._mapDict[map] = _SceneMode.GEOJS 73 | elif mode == _SceneMode.GEOJS: 74 | if feature_type == 'pointcloud': 75 | raise Exception('Cannot add pointcloud to scene with osm layer or geojs features') 76 | elif mode == _SceneMode.POINTCLOUD: 77 | if feature_type == 'pointcloud': 78 | raise Exception('Cannot use multiple pointcloud features in one scene') 79 | else: 80 | raise Exception('Cannot add non-pointcloud feature to scene containing pointcloud features') 81 | 82 | def clearing_layer(self, layer): 83 | '''Checks if layer has pointcloud feature, and resets if applicable 84 | 85 | ''' 86 | map = self._layerDict[layer] 87 | mode = self._mapDict[map] 88 | if mode != _SceneMode.POINTCLOUD: 89 | return 90 | 91 | # Rest mode if this layer contains a (pointcloud) feature 92 | if len(layer._features) == 1: 93 | self._mapDict[map] = _SceneMode.UNASSIGNED 94 | 95 | def is_pointcloud(self, map): 96 | '''Returns boolean indicating whether specific map is pointcloud type 97 | 98 | ''' 99 | return self._mapDict.get(map) == _SceneMode.POINTCLOUD 100 | 101 | 102 | def SceneValidator(): 103 | if _SceneValidator.instance is None: 104 | _SceneValidator.instance = _SceneValidator() 105 | return _SceneValidator.instance 106 | -------------------------------------------------------------------------------- /jupyterlab_geojs/geojsfeature.py: -------------------------------------------------------------------------------- 1 | class GeoJSFeature: 2 | '''Generic/base class for GeoJS features 3 | ''' 4 | 5 | # List of options that are common to all GoeJS features 6 | CommonOptionNames = [ 7 | 'bin', 8 | 'gcs', 9 | 'selectionAPI', 10 | 'style', 11 | 'visible' 12 | ] 13 | 14 | # Table of options specific to each feature type 15 | OptionNameTable = { 16 | 'point': ['colormap', 'enableTooltip', 'position'], 17 | 'quad': ['image', 'imageCrop', 'imageFixedScale'] # omit 'color' 'canvas', 'video' 18 | } 19 | 20 | def __init__(self, feature_type, config_options=True, **kwargs): 21 | # Public members 22 | option_names = [] 23 | if config_options: 24 | option_names = self.__class__.CommonOptionNames + \ 25 | self.__class__.OptionNameTable.get(feature_type, []) 26 | for name in option_names: 27 | value = kwargs.get(name) 28 | setattr(self, name, value) 29 | # Todo create attributes for any kwargs not in MemberNames, 30 | # for forward compatibility with GeoJS 31 | 32 | # Internal members 33 | self._feature_type = feature_type 34 | self._options = kwargs 35 | self._option_names = option_names 36 | 37 | def _build_display_model(self): 38 | '''''' 39 | display_model = dict() 40 | display_model['featureType'] = self._feature_type 41 | 42 | # Add array index to point features, because it: 43 | # - provides a selection workaround for points 44 | # - is used in client for options specified by function 45 | # Should this index be added to all feature types? 46 | if self._feature_type == 'point': 47 | point_data = self._options.get('data') 48 | if isinstance(point_data, list) \ 49 | and len(point_data) > 0 \ 50 | and not '__i' in point_data[0]: 51 | 52 | for i in range(len(point_data)): 53 | point_data[i]['__i'] = i 54 | self._options['data'] = point_data 55 | 56 | # Copy options that have been set 57 | for name in self._option_names: 58 | value = getattr(self, name, None) 59 | if value is not None: 60 | self._options[name] = value 61 | 62 | # Apply any lambda functions 63 | data = self._options.get('data') 64 | 65 | # Check position 66 | position = self._options.get('position') 67 | if position is not None and callable(position): 68 | position_coords = [position(item) for item in data] 69 | self._options['position'] = position_coords 70 | 71 | # Check style components 72 | style = self._options.get('style', {}) 73 | for key,val in style.items(): 74 | if callable(val): 75 | item_vals = [val(item) for item in data] 76 | 77 | # Check format for styles that set color 78 | if key in ['backgroundColor', 'color', 'fillColor', 'strokeColor']: 79 | item_vals = self._format_colors(item_vals) 80 | style[key] = item_vals 81 | 82 | display_model['options'] = self._options 83 | 84 | return display_model 85 | 86 | def _format_colors(self, input_vals): 87 | '''Converts input colors to hex format. 88 | 89 | This only applies to colors produced by a callable 90 | ''' 91 | # Use first item as exemplar 92 | input0 = input_vals[0] 93 | # Abort for unexpected input 94 | if not isinstance(input0, (list,tuple)): 95 | return input_vals 96 | if len(input0) < 3 or len(input0) > 4: 97 | return input_vals 98 | if len(input0) == 4: 99 | input0 = list(input0[:3]) 100 | 101 | if max(input0) <= 1: 102 | return[self._double_to_hex(item) for item in input_vals] 103 | elif max(input0) <= 255: 104 | return [self._rgb_to_hex(item) for item in input_vals] 105 | 106 | # else: 107 | return input_vals # dont understand format 108 | 109 | 110 | def _double_to_hex(self, color): 111 | rgb = tuple(map(lambda val: int(255.0 * val), color)) 112 | return self._rgb_to_hex(rgb) 113 | 114 | def _rgb_to_hex(self, rgb): 115 | hexVal = tuple(map(lambda val: '{:02x}'.format(val), rgb[:3])) 116 | return '#' + ''.join(hexVal) 117 | -------------------------------------------------------------------------------- /test/jasmine/geojsbuilder.spec.js: -------------------------------------------------------------------------------- 1 | // Tests for GeoJSBuilder 2 | 3 | import fs from 'fs'; 4 | import geo from 'geojs'; 5 | import { GeoJSBuilder } from '../../lib/geojsbuilder.js'; 6 | 7 | describe('jasmine', () => { 8 | it('should be configured for testing', () => { 9 | expect(true).toBeTruthy(); 10 | }); 11 | }); 12 | 13 | describe('GeoJSBuilder', () => { 14 | 15 | // Use jasmine async support for convenience 16 | // (Async not technically required, but simpifies test code.) 17 | // Also disable OSM renderer so that we don't have to mock canvas 18 | // Also use mockVGLRenderer() 19 | 20 | // USE STATIC/GLOBAL geoMap VARIABLE IN ALL TESTS 21 | let geoMap = null; 22 | 23 | beforeEach(function(done) { 24 | // Wait for async tests 25 | setTimeout(function() { 26 | if (geoMap) { 27 | geoMap.exit(); 28 | geoMap = null; 29 | } 30 | geo.util.mockVGLRenderer(); 31 | done(); 32 | }, 1); 33 | }); 34 | 35 | afterEach(() => { 36 | geo.util.restoreVGLRenderer(); 37 | }); 38 | 39 | // Helper method to load model 40 | // @param modelFile: string relative path to input model file (optional) 41 | async function initGeoMap(modelFile) { 42 | let node = document.querySelector('#map'); 43 | let modelString = fs.readFileSync(__dirname + '/' + modelFile); 44 | let model = JSON.parse(modelString); 45 | let builder = new GeoJSBuilder(); 46 | return builder.generate(node, model); 47 | }; 48 | 49 | it('should be able to initialize a geo.map instance', async () => { 50 | let node = document.querySelector('#map'); 51 | expect(node).toBeDefined(); 52 | let builder = new GeoJSBuilder(); 53 | geoMap = await builder.generate(node); 54 | expect(geoMap).toBeDefined(); 55 | expect(geoMap.layers().length).toBe(0); 56 | }); 57 | 58 | it('should instantiate a simple model', async () => { 59 | // Init model locally so that we can query its contents 60 | let modelString = fs.readFileSync(__dirname + '/../models/basic_model.json'); 61 | let model = JSON.parse(modelString); 62 | let node = document.querySelector('#map'); 63 | let builder = new GeoJSBuilder(); 64 | geoMap = await builder.generate(node, model); 65 | 66 | expect(geoMap.layers().length).toBe(2); 67 | let center = geoMap.center(); 68 | expect(center.x).toBeCloseTo(model.options.center.x); 69 | expect(center.y).toBeCloseTo(model.options.center.y); 70 | let zoom = geoMap.zoom(); 71 | expect(zoom).toBe(10); 72 | }); 73 | 74 | it('should load a geojson object', async () => { 75 | geoMap = await initGeoMap('../models/geojson_model.json'); 76 | 77 | let layers = geoMap.layers() 78 | expect(layers.length).toBe(2); 79 | let layer1 = layers[1]; 80 | expect(layer1.features().length).toBe(2) // 1 polygon with 1 edge 81 | }); 82 | 83 | it('should load basic features', async () => { 84 | geoMap = await initGeoMap('../models/basic-features_model.json'); 85 | 86 | let layers = geoMap.layers() 87 | // Expect 3 layers: osm, features, tooltip 88 | expect(layers.length).toBe(3); 89 | let layer1 = layers[1]; 90 | // Expect 2 features: point feature, quad feature 91 | expect(layer1.features().length).toBe(2) 92 | }); 93 | 94 | it('should load raster features', async () => { 95 | geoMap = await initGeoMap('../models/raster-rgb_model.json') 96 | 97 | let layers = geoMap.layers() 98 | expect(layers.length).toBe(2); 99 | let layer1 = layers[1]; 100 | expect(layer1.features().length).toBe(1) // 1 quad feature 101 | }); 102 | 103 | it('should load raster features without GeoJSBuilder', () => { 104 | let node = document.querySelector('#map'); 105 | geoMap = geo.map({node: node}); 106 | 107 | let featureLayer = geoMap.createLayer('feature', {features: ['quad.image']}); 108 | let featureData = { 109 | image: '../data/rasterwithpalette.png', 110 | ll: { x: -73.758345, y: 41.849604 }, 111 | lr: { x: -72.758345, y: 41.849604 }, 112 | ul: { x: -73.758345, y: 42.849604 }, 113 | ur: { x: -72.758345, y: 42.849604 } 114 | }; 115 | let feature = featureLayer.createFeature('quad').data([featureData]); 116 | 117 | const bounds = {"bottom": 41.849604, "left": -73.758345, "right": -72.758345, "top": 42.849604} 118 | let spec = geoMap.zoomAndCenterFromBounds(bounds); 119 | // console.log('Computed viewpoint spec:'); 120 | // console.dir(spec); 121 | geoMap.center(spec.center); 122 | geoMap.zoom(spec.zoom / 2); 123 | //console.dir(node); 124 | }); 125 | 126 | }); 127 | -------------------------------------------------------------------------------- /test/models/laspointcloud_100.json: -------------------------------------------------------------------------------- 1 | {"data": ["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"], "featureType": "pointcloud", "options": {}} -------------------------------------------------------------------------------- /test/models/pointcloud-100_model.json: -------------------------------------------------------------------------------- 1 | {"layers": [{"features": [{"data": ["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"], "featureType": "pointcloud", "options": {}}], "layerType": "feature", "options": {}}], "options": {}, "viewpoint": null} -------------------------------------------------------------------------------- /jupyterlab_geojs/scene.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from IPython.display import display, JSON 5 | from .geojsfeaturelayer import GeoJSFeatureLayer 6 | from .geojsosmlayer import GeoJSOSMLayer 7 | from .scenevalidator import SceneValidator 8 | from .types import LayerType 9 | 10 | # A display class that can be used in Jupyter notebooks: 11 | # from jupyterlab_geojs import Scene 12 | # Scene() 13 | 14 | MIME_TYPE = 'application/geojs+json' 15 | 16 | 17 | class Scene(JSON): 18 | """A display class for displaying GeoJS visualizations in the Jupyter Notebook and IPython kernel. 19 | 20 | Scene expects a JSON-able dict, not serialized JSON strings. 21 | 22 | Scalar types (None, number, string) are not allowed, only dict containers. 23 | """ 24 | 25 | # List of options (names) to be added as a public member of each instance. 26 | # No error checking is done in this class. 27 | OptionNames = [ 28 | 'allowRotation', 29 | 'center', 30 | 'clampBoundsX', 31 | 'clampBoundsY', 32 | 'clampZoom', 33 | 'discreteZoom', 34 | 'gcs', 35 | 'ingcs', 36 | 'maxBounds', 37 | 'minZoom', 38 | 'maxZoom', 39 | 'rotation', 40 | 'unitsPerPixel', 41 | 'zoom' 42 | ] 43 | 44 | def __init__(self, **kwargs): 45 | ''' 46 | ''' 47 | super(Scene, self).__init__() 48 | # Public members 49 | for name in self.__class__.OptionNames: 50 | value = kwargs.get(name) 51 | setattr(self, name, value) 52 | # Todo create attributes for any kwargs not in MemberNames, 53 | # for forward compatibility with GeoJS 54 | self._validator = SceneValidator() 55 | self._validator.adding_map(self) 56 | 57 | # Internal members 58 | self._options = kwargs 59 | self._layers = list() 60 | self._layer_lookup = dict() # 61 | self._logger = None 62 | # Tracks the zoom & center coordinates in different representations 63 | self._viewpoint = type('ViewPoint', (object,), dict()) 64 | self._viewpoint.mode = None 65 | self._viewpoint.bounds = { 66 | 'left': None, 67 | 'top': None, 68 | 'right': None, 69 | 'bottom': None 70 | } 71 | 72 | def create_layer(self, layer_type, **kwargs): 73 | self._validator.adding_layer(self, layer_type) 74 | if False: pass 75 | # elif layer_type == 'annotation': 76 | # layer = GeoJSAnnotationLayer(**kwargs) 77 | elif layer_type == LayerType.FEATURE: 78 | layer = GeoJSFeatureLayer(**kwargs) 79 | elif layer_type == LayerType.OSM: 80 | layer = GeoJSOSMLayer(**kwargs) 81 | # elif layer_type == 'ui': 82 | # layer = GeoJSUILayer(**kwargs) 83 | else: 84 | raise Exception('Unrecognized layer type \"{}\"'.format(layerType)) 85 | 86 | self._layers.append(layer) 87 | self._validator.added_layer(self, layer) 88 | return layer 89 | 90 | def create_logger(self, folder, filename='scene.log'): 91 | '''Initialize logger with file handler 92 | 93 | @param folder (string) directory to store logfile 94 | ''' 95 | os.makedirs(folder, exist_ok=True) # create folder if needed 96 | 97 | log_name, ext = os.path.splitext(filename) 98 | self._logger = logging.getLogger(log_name) 99 | self._logger.setLevel(logging.INFO) # default 100 | 101 | log_path = os.path.join(folder, filename) 102 | fh = logging.FileHandler(log_path, 'w') 103 | self._logger.addHandler(fh) 104 | return self._logger 105 | 106 | def set_zoom_and_center(self, enable=True, corners=None): 107 | '''Sets map zoom and center based on input args 108 | 109 | @param enable: (boolean) command geojs to set map's zoom & center coords 110 | @param corners: (list of [x,y]) bounding box specified by 4 corner points 111 | ''' 112 | if not enable or corners is None: 113 | self._viewpoint.mode = None 114 | elif corners is not None: 115 | self._viewpoint.mode = 'bounds' 116 | x_coords,y_coords = zip(*corners) 117 | self._viewpoint.bounds['left'] = min(x_coords) 118 | self._viewpoint.bounds['right'] = max(x_coords) 119 | self._viewpoint.bounds['top'] = max(y_coords) 120 | self._viewpoint.bounds['bottom'] = min(y_coords) 121 | 122 | def _build_display_model(self): 123 | data = dict() # return value 124 | 125 | # Copy options that have been set 126 | for name in self.__class__.OptionNames: 127 | value = getattr(self, name, None) 128 | if value is not None: 129 | self._options[name] = value 130 | data['options'] = self._options 131 | 132 | if self._viewpoint.mode is None: 133 | data['viewpoint'] = None 134 | else: 135 | data['viewpoint'] = {'mode': self._viewpoint.mode} 136 | if 'bounds' == self._viewpoint.mode: 137 | data['viewpoint']['bounds'] = self._viewpoint.bounds 138 | 139 | layer_list = list() 140 | for layer in self._layers: 141 | layer_list.append(layer._build_display_model()) 142 | data['layers'] = layer_list 143 | return data 144 | 145 | 146 | def _ipython_display_(self): 147 | if self._logger is not None: 148 | self._logger.debug('Enter Scene._ipython_display_()') 149 | display_model = self._build_display_model() 150 | 151 | # Change mime type for "pointcloud mode" 152 | mimetype = 'application/las+json' if self._validator.is_pointcloud(self) else MIME_TYPE 153 | 154 | bundle = { 155 | mimetype: display_model, 156 | 'text/plain': '' 157 | } 158 | metadata = { 159 | mimetype: self.metadata 160 | } 161 | if self._logger is not None: 162 | self._logger.debug('display bundle: {}'.format(bundle)) 163 | self._logger.debug('metadata: {}'.format(metadata)) 164 | display(bundle, metadata=metadata, raw=True) 165 | -------------------------------------------------------------------------------- /notebooks/pointcloud.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from jupyterlab_geojs import Scene, LayerType, FeatureType\n", 10 | "scene = Scene()\n", 11 | "layer = scene.create_layer(LayerType.FEATURE)" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 3, 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "data": { 21 | "application/las+json": { 22 | "layers": [ 23 | { 24 | "features": [ 25 | { 26 | "data": [ 27 | "TEFTRgAAAAAAAAAAAAAAAAAAAAAAAAAAAQJQREFMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBEQUwgMS41LjAgKDI4NGFmOCkAAAAAAAAAAAAAAAAAcgDhB+MA4wAAAAAAAAADIgBkAAAAWQAAAAoAAAABAAAAAAAAAAAAAAB7FK5H4XqEP3sUrkfheoQ/exSuR+F6hD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABnZmbmwX8jQTMzM7OLZiNBmpmZmdYLKkGuR+F6c+gpQXsUrkfhlIBA16NwPQqTeUAYp8sDfokPBZGmAACdAEkC+HyfHAh9h9ChCg5BjABoAIQAtx7MA45mDwUhqAAAlwAJAfSAnxymPLh8rgoOQYwAcACOAKXlywMfKBEFxsIAAAUAEQH/hqAc+mPPYDMXDkFmAGUAfAC6d8sDRcYQBfqlAADpAAkBAoigHBkGhMZBFw5BXABkAG4AN+fKA4z7EAV8tQAAfwBSAQB+oBwwZXjhUBcOQWIAYQBoAGBpygPM5hAFebQAAAwAEgEAgqAcGBS/CmEXDkE1AEAARAD7RcoDIKERBZGmAACAAAkB/HyhHHBThRpFLA5BhQBoAIIALMzKA/AvEQUbyAAACQASAfN+oRxIeYWhXywOQTgAQwBGAH58ywPuHBEF/7UAAG0ASQH1fKEcZ+zZBngsDkFOAEEAVgDvQ8wDyBYRBfCnAABoAEkB9oKhHAQ4mvGNLA5BNABAAEIATgHOA3jCDwXTpwAAqQBJAfR+nhz9cVoa6fMNQXYAegCVAPJ+zQNMkQ8FTKcAAAQASQH7gJ4cudpN0PnzDUE5AFEASwDTl8wDZgwRBUynAABbAFIBC36fHB0u+6OxCg5BbAByAH4Alq/MAwPfDwWfoAAACwAJAvt8nxy86vgGvwoOQU8AVwBXAKkYzQPqUxAFxacAALQACQEAgJ8cfsao9swKDkFmAE8AXwBHfM0DzDgQBTGsAAAuAFEBAIKfHC0T/f7aCg5BVABYAGoAysjNA//eDwVFzwAABgBZAQGEnxy0OOYF5goOQVAAWgBgAChAzgMkhRAFBKMAABwACQEMg58cbv7MbvEKDkFSAGEAXwAkyc4DHE0QBf6iAAAVAAkCBYCfHI00NpoCCw5BOABQAEcALUvOAwqrEAX6ogAACwAJAQF+oBwQha9g4RYOQVMAXgBeAD7ezQNDVxAFnq0AAAcASQEEgKAcFj2p4e8WDkE6AE4ATAAzQM0DpW0RBVmmAABxAEkC/X6gHEvUr/b7Fg5BZgBoAHwA3jPNA7sgEAU4zwAAIAAZAQ+CoBzJXNOsBxcOQUoAVgBYAB4AzQPVPhAFjqYAACoACQELgqAc5ICSFxQXDkFkAGYAdAB/Y8wD4I0RBZTOAAALABEB+4KgHJU2eiYhFw5BQABNAFAAk4XMA28ZEQWMtwAAIQBRAfJ+oRzd6xQTlCwOQUUASwBTAAlXzQNfDxEFGKQAAFAAUgH6fqEcnuEFgq8sDkF9AH4AkgD9P84DIkcRBfOvAAAtAFoB9oKhHPNTg2zHLA5BiQB+AJgAIgjMA5L2EQWlpgAAvgAJAfaAoBxUrCD2LBcOQX4AcACSAIlYygO36hEFVqYAABUASQLyeqAcC6CNHF8XDkGLAHoAhQBsWsoDd18SBWqlAAAMAFICBHyhHDQ941hBLA5BdABsAH4As8rKA9MdEgU8pgAAIgBJAgB8oRxN0ftuUSwOQaAAhgCXAPocywOfXBIFOKsAABUACQEDfaEclEWzNmAsDkFAAEEASAA/qssDZpASBcyoAABTAAkBA3qhHLoNkcJwLA5BogByAJEAwjTMA4KXEgXqpAAAfAAJAgt8oRwOPyL/giwOQaUAeACTAET+ywMGRRMFMqYAAGIACQL9fKIcM5LneHg4DkHlANsA4wA+tcsDNcsSBaSqAAABAFEBAHyiHKhlVZWDOA5BagBiAHoASgvLAxn0EwX+oQAARABJAvJ8ohw/rT1IjjgOQUAATABLAHniygN2HxMFw6MAAFYACQEBfKIcuorlppg4DkGiAHoAngAPYMoDI9ITBbaiAACLAEkB94CiHJ05XCGjOA5BagBkAHgASQfKA3apEwWeowAACwAbAvl8ohz7VDsMrTgOQUgAUgBWAIouygMruBQFYKIAAHAASQIGeqMcDDDf0LZODkGYAHoAkAA2aMoDYnQUBZWiAADMAEkCBHyjHNX9MsXATg5BZABkAHsAiNHKAw3eFAWipAAAGgAJAg56oxxp4erYy04OQaMAiQCbAKMQywPkhBQFOaMAAHAACQEIeqMc3a3mldZODkFZAFwAdQDghMsDQt0UBfqkAABrAEkBCn6jHK8EtLvhTg5BrwCXAKYAPZrLA67qEwXEtgAAEABJAfx8oxyxZCq+7E4OQYgAhgCMAN7aywOQYRMFJaUAABMASQH3fKMcinPQw/dODkGWAIUAjgC8UswDgJsUBQumAAAYAEkBAX6kHKoXLKE8Ww5BmACGAJAAfuHLAxtGFAWhqAAAsABJAQt8pBxUzEhFTFsOQbEAqQC0AKyAywMnLxQFfLQAAEsASQEJfKQcOJuH1VpbDkHQAMQAzgA59coDEAMVBfSjAABcAEkBAHikHOHVFnRoWw5BigBwAI4ASJ7KA3I7FAU/ogAAdABJAgt7pByHj04Id1sOQV0AYQB4ANUzygO5IhQFsqIAAB0ASQILfqQcwf5CN4RbDkGaAIMAkAA3vsoDFRgVBeejAABmAAkC9XilHAJnD6JvcQ5BiABsAIYAOcfLA0JBFQWvpAAAlAAJAvN6pRxfbeNbjXEOQXgAaQCJAAc7zgPVxhEFh6YAAB0ASQH0fqAcBeAth94WDkGmAH4AmwADzMwDrK0RBa+lAACLAEkB+n6gHFDfvdQIFw5BTQBLAE8AiHPMA0r0EQXUtgAABwARAQGAoRwOjkGDjCwOQVwAWgBsAD08zQPh4xIFYKQAACQACQEKgKEcWtstMZ0sDkGsAIsAowAkkc0DLKASBW2kAAAYAAkBDoShHIKgapmtLA5BtQCXAKoAuCHOAzz6EQUepgAAeQBJAf5+oRyGoUtRvywOQcUAlACsANylzgPPkBIFEaUAAC0ASQELfqEcXCn7us8sDkGmAIwAngBE2s4DERgTBVuzAAAhABoB/oeiHL6wPxcnOA5BaABvAH4AQ5zOAzhpEgUEowAAWAAJAgV+ohx/NwceMDgOQUIAVABSAOsuzgOhABIF9KUAABYACQIKfqIcl6iX/Do4DkHNAJkAtACktc0D/oUSBaikAAA8AEkBBYKiHH1Elr5GOA5BrwCWAKQA0krNAwLEEgXtowAAFAAJAgGIohwkLeUlUjgOQbQAkwCqAOjMzAMF9hMFIqMAACMASQH2fKIcIOfxn1w4DkGYAHwAnAAZzswDFgoSBde+AAAPAFEBC36iHBc2xWdnOA5BaABaAHQA4qfMA56XFAXSrAAAvgAJAQh+oxxGgUoZBE8OQW0AaACAAHvZzAO/4RMF+qIAABwASQECeqMci3+rrw9PDkGoAJgArgCw98wDI/0SBVClAAAQAAkB83yjHO4gSdIaTw5BqgCVAKQA+VXNA9AxEwU1pQAAMQBJAfiGoxxbCMQyJk8OQaQAiACcAPHJzQM9sxMF2aUAAEgASQH6kaMcPDp0UTJPDkG7AJgAqgCvVs4DaDYUBdOkAAA7AEkCAIijHFYBb/o9Tw5BkgCEAJAAvKDOAy7qEwVdpQAAvwAJAQCEoxym3ztQSE8OQaoAhACgAHzyzgNmUBQFIqYAAFUASQIGgqMc9AoZm09PDkFHAFEAVwDf884DWQ0UBaulAAAxAAkBDYekHH8pAtfqWg5BmACKAJIA51nOAyMQFQWopAAAbgBJAf9+pBzhxnNY91oOQboAiACkAO/fzQNzHRUF8KQAADwACQH8gqQc+XlS6QZbDkGzAJkApwAacs0Drg0UBTimAAAlAEkBCoGkHPpoD1IYWw5BnwCKAJQAqbfMA/dhFQWLpQAASwAJAfp8pBxA59u2J1sOQW4AagB2AN6JzANOfRQFDLsAAMsASQEFfKQc+yiFHTdbDkFIAFcAUgACes0DqekUBa+lAAAkAEkC8nylHF38M7K3cQ5BqQCUAKAAuKjOA/zfFAVGpQAArABJAvOApRzhjLCQ2XEOQUgAWABSAKGsywMEghUFK6UAABEACQH7eqQcW6ucqEpbDkF6AG8AfACXvMoDlO4VBWCiAAAXAEkB9HqkHDYlh4BpWw5BRABSAE4AFzHKA86RFQXvsQAADgARAfd4pRzEvWPFXHEOQUsAWABXAGUtywM09BUFzKMAAKkASQEAe6UcJTO7R3ZxDkGBAHIAiACXFcwD6k8WBSKkAABGABIBAnylHJ2G+aGPcQ5BbgBmAHQAmb/LA1/4FQWLpAAAIgBJAQp8phzh4XwzhX0OQagAoQCmAC3DygNuKxYFBaAAAGgASQIDfqYcsJ5Q/Kd9DkFpAGkAewAofs4DQLoVBfeiAAB+AEkB94CkHE2We/zrWg5BdQB1AIgA6o3NA5a1FQW8pQAAoQBJAfd8pBxp62YXDVsOQbgAhgCcAFeQzAO1NRYF158AAAsACQIBeqUca99z1JhxDkFkAGIAdABXo80DrR8WBYiiAABFAEkBA3qlHI6N0DG2cQ5BgAB0AJIAEIfOA9SdFQXotgAAAQBRAft+pRwNTCyJ0nEOQXwAdgCQAI5WzgOtHRYFLKIAAGUASQEBg6YcCc4spzV9DkFPAFsAYgDDHM0D8BUWBXqkAABpABIBB3ymHLBEEQRafQ5BYwBnAHQA" 28 | ], 29 | "featureType": "pointcloud", 30 | "options": {} 31 | } 32 | ], 33 | "layerType": "feature", 34 | "options": {} 35 | } 36 | ], 37 | "options": {}, 38 | "viewpoint": null 39 | }, 40 | "text/plain": [ 41 | "" 42 | ] 43 | }, 44 | "metadata": { 45 | "application/las+json": { 46 | "expanded": false 47 | } 48 | }, 49 | "output_type": "display_data" 50 | } 51 | ], 52 | "source": [ 53 | "filename = '../test/data/100-points.las'\n", 54 | "#filename = '../../PDAL/test/data/las/autzen_trim.las'\n", 55 | "layer.clear()\n", 56 | "feature = layer.create_feature(FeatureType.POINTCLOUD, data=filename)\n", 57 | "scene" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [] 66 | } 67 | ], 68 | "metadata": { 69 | "kernelspec": { 70 | "display_name": "Python 3", 71 | "language": "python", 72 | "name": "python3" 73 | }, 74 | "language_info": { 75 | "codemirror_mode": { 76 | "name": "ipython", 77 | "version": 3 78 | }, 79 | "file_extension": ".py", 80 | "mimetype": "text/x-python", 81 | "name": "python", 82 | "nbconvert_exporter": "python", 83 | "pygments_lexer": "ipython3", 84 | "version": "3.5.2" 85 | } 86 | }, 87 | "nbformat": 4, 88 | "nbformat_minor": 2 89 | } 90 | -------------------------------------------------------------------------------- /test/las/las.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(http-equiv="Content-type" content="text/html; charset=utf-8") 5 | meta(name="viewport" content="width=device-width, initial-scale=1") 6 | title LAS Test 7 | script(src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js") 8 | style. 9 | body {font-family: "Verdana, sans-serif";} 10 | button, input { 11 | min-width: 80px; 12 | } 13 | span.comment { 14 | margin-left: 8px; 15 | } 16 | #vtk { 17 | height: 480px !important; 18 | } 19 | h3.notify { 20 | color: red; 21 | } 22 | body 23 | div#vue 24 | //- Html here references vue members - messy yes, but I don't have to 25 | set up a build env for vue 26 | div(v-if="!isDone") 27 | h3 LAS test 28 | p 29 | input(:disabled="isInput" type="file" multiple name="lasFile" @change="onFileChange") 30 | div(v-if="isInput") 31 | table 32 | tr 33 | td Input point Count 34 | td {{input.pointCount}} 35 | tr 36 | td Input bounds 37 | td {{input.bounds}} 38 | p 39 | button(:disabled="isDisplayed" @click="onDisplayClick") Load & Display 40 | span.comment(v-if="pointCount > 0") Loaded binary data ({{pointCount}} points) 41 | 42 | div(v-if="isDisplayed") 43 | p 44 | button(:disabled="!lasPointCloud" @click="onDeleteClick") Delete 45 | 46 | div(v-if="isDone") 47 | h3.notify Reload the page to start again. 48 | div#vtk 49 | 50 | script(src="./pointcloud.bundle.js") 51 | script. 52 | console.log('Page loaded') 53 | new Vue({ 54 | el: '#vue', 55 | data: function() { 56 | return { 57 | // Store metadata from the user-specified files (before loading data) 58 | input: { 59 | files: null, // from 60 | bounds: null, // [xmin,xmax, ymin,ymax, zmin,zmax] 61 | formats: {}, // 62 | pointCount: 0, // overall point count 63 | }, 64 | 65 | // Tracks state from input to display to done 66 | isInput: false, // app has read LAS headers 67 | isDisplayed: false, // app has loaded and rendered data 68 | isDone: false, // deprecated? 69 | lasPointCloud: null, 70 | pointCount: 0, 71 | } 72 | }, // data 73 | methods: { 74 | onDeleteClick: function() { 75 | this.lasPointCloud.dispose(); 76 | 77 | let vtkElement = document.querySelector('#vtk'); 78 | vtkElement.parentNode.removeChild(vtkElement); 79 | 80 | this.isDisplayed = null; 81 | this.isDone = true; 82 | this.lasPointCloud = null; 83 | this.pointCount = 0; 84 | console.log('Data should be deleted'); 85 | }, // deleteLASData() 86 | onDisplayClick: function() { 87 | // Load input files 88 | this.lasPointCloud = new LASPointCloud(); 89 | this.lasPointCloud.loadFiles(this.input.files) 90 | .then(() => { 91 | let elem = document.querySelector('#vtk'); 92 | this.lasPointCloud.render(elem); 93 | this.isDisplayed = true; 94 | }) 95 | }, // onDisplayClick() 96 | onFileChange: function(evt) { 97 | // Parse input file headers to update this.input 98 | // We can do these concurrently, because (I just know) 99 | // lasPointCloud.getLASHeader() is a static method. 100 | let promiseList = []; 101 | this.input.files = evt.target.files; 102 | for (let f of this.input.files) { 103 | let p = new Promise((resolve, reject) => { 104 | let fileReader = new FileReader(); 105 | fileReader.onload = evt => { 106 | let buffer = evt.target.result; 107 | //console.log(`Input content length ${buffer.byteLength}`); 108 | LASPointCloud.getLASHeader(buffer) 109 | .then(header => resolve(header)) 110 | .catch(err => alert(err)); 111 | } 112 | // Read enough bytes to parse the LAS public header 113 | let blob = f.slice(0, 350); 114 | fileReader.readAsArrayBuffer(blob); 115 | }); // new Promise() 116 | promiseList.push(p); 117 | } // for (f) 118 | 119 | // Compute input metadata once all headers have been read 120 | Promise.all(promiseList) 121 | .then(headers => { 122 | for (let header of headers) { 123 | // Total point count 124 | this.input.pointCount += header.pointsCount; 125 | 126 | // Point count by point record format 127 | const format = header.pointsFormatId; 128 | let newCount = header.pointsCount; 129 | if (format in this.input.formats) { 130 | newCount += this.input.formats[format]; 131 | } 132 | this.input.formats[format] = newCount; 133 | 134 | // And the bounds 135 | const bounds = [ 136 | header.mins[0], header.maxs[0], // xmin, xmax 137 | header.mins[1], header.maxs[1], // ymin, ymax 138 | header.mins[2], header.maxs[2], // zmin, zmax 139 | ] 140 | this.updateBounds(bounds); 141 | } 142 | //- console.log(`Input metadata: ${this.input}`); 143 | //- console.dir(this.input); 144 | this.isInput = true; 145 | }) 146 | .catch(err => alert(err)); 147 | 148 | 149 | }, // onFileChange() 150 | updateBounds: function(newBounds) { 151 | if (!this.input.bounds) { 152 | this.input.bounds = newBounds; 153 | return; 154 | } 155 | // (else) 156 | for (let i=0; i<3; ++i) { 157 | let index = 2*i; 158 | if (newBounds[index] < this.input.bounds[index]) { 159 | this.input.bounds[index] = newBounds[index]; 160 | } 161 | 162 | ++index; 163 | if (newBounds[index] > this.input.bounds[index]) { 164 | this.input.bounds[index] = newBounds[index]; 165 | } 166 | } // for (i) 167 | }, // updateBounds() 168 | }, // methods 169 | computed: { 170 | isLoaded: function() { 171 | return this.pointCount > 0; 172 | } 173 | }, // computed 174 | }); // Vue() 175 | -------------------------------------------------------------------------------- /src/pointcloud/index.js: -------------------------------------------------------------------------------- 1 | import { LASFile } from './laslaz' 2 | import { ParticleSystem } from './particlesystem' 3 | 4 | 5 | // Function to run (map) in sequence an array of 6 | // calls to a promise-returning function 7 | // https://stackoverflow.com/a/41608207 8 | Promise.each = async function(dataArray, fn) { 9 | for (const item of dataArray) await fn(item); 10 | } 11 | 12 | 13 | class LASPointCloud { 14 | constructor() { 15 | this.particleSystem = new ParticleSystem(); 16 | this.input = { 17 | bounds: null, // [xmin,xmax, ymin,ymax, zmin,zmax] 18 | formats: {}, // 19 | pointCount: 0, // overall point count 20 | } 21 | } // constructor() 22 | 23 | // Deletes resources 24 | dispose() { 25 | this.particleSystem.destroy(); 26 | this.particleSystem = null; 27 | } 28 | 29 | // Parses LAS public header from input arraybuffer 30 | // Input arraybuffer must be long enough to include header 31 | // For LAS 1.4 and lower, 350 bytes is sufficient. 32 | // Returns Promise
33 | static 34 | getLASHeader(arraybuffer) { 35 | return new Promise((resolve, reject) => { 36 | let lasFile = new LASFile(arraybuffer); 37 | //console.log(`lasFile ${lasFile}`); 38 | lasFile.open() 39 | .then(() => lasFile.getHeader()) 40 | .then(header => resolve(header)) 41 | .catch(err => reject(err)); 42 | }); // newPromise 43 | } // getLASHeader() 44 | 45 | loadFiles(files) { 46 | return new Promise((resolve, reject) => { 47 | // Read files into arraybuffers 48 | //let buffers = []; 49 | let promiseList = []; 50 | for (let f of files) { 51 | let p = new Promise((resolve, reject) => { 52 | let fileReader = new FileReader(); 53 | fileReader.onload = evt => { 54 | let buffer = evt.target.result; 55 | //buffers.push(buffer); 56 | resolve(buffer); 57 | } 58 | // Read files into array buffer 59 | fileReader.readAsArrayBuffer(f); 60 | }); // new Promise() 61 | promiseList.push(p); 62 | } // for (f) 63 | 64 | // Load arraybuffers into particle system 65 | Promise.all(promiseList) 66 | .then(buffers => this.loadBuffers(buffers)) 67 | .then(() => resolve()); 68 | }); // new Promise() 69 | } // loadFiles() 70 | 71 | loadBuffers(arraybuffers) { 72 | return new Promise((resolve, reject) => { 73 | // Parse headers first to compute overall bounds 74 | // We can do these concurrently, because 75 | // lasPointCloud.getLASHeader() is a static method. 76 | let promiseList = []; 77 | for (let buffer of arraybuffers) { 78 | let p = this.constructor.getLASHeader(buffer); // calling static method 79 | promiseList.push(p); 80 | } 81 | Promise.all(promiseList) 82 | .then(headers => { 83 | for (let header of headers) { 84 | // Total point count 85 | this.input.pointCount += header.pointsCount; 86 | 87 | // Point count by point record format 88 | const format = header.pointsFormatId; 89 | let newCount = header.pointsCount; 90 | if (format in this.input.formats) { 91 | newCount += this.input.formats[format]; 92 | } 93 | this.input.formats[format] = newCount; 94 | 95 | // And the bounds 96 | const bounds = [ 97 | header.mins[0], header.maxs[0], // xmin, xmax 98 | header.mins[1], header.maxs[1], // ymin, ymax 99 | header.mins[2], header.maxs[2], // zmin, zmax 100 | ] 101 | this.updateBounds(bounds); 102 | } // for (header) 103 | }) // then (headers) 104 | .then(() => { 105 | this.setZRange(this.input.bounds[4], this.input.bounds[5]); 106 | //console.log(`bounds ${this.input.bounds}`); 107 | // Load each buffer in sequence 108 | return Promise.each(arraybuffers, buffer => this.loadData(buffer)); 109 | }) 110 | .then(() => resolve()) 111 | .catch(err => { 112 | alert(err); 113 | reject(err); 114 | }); 115 | }); // new Promise() 116 | 117 | } // loadBuffers() 118 | 119 | // Returns promise 120 | loadData(arraybuffer) { 121 | return new Promise((resolve, reject) => { 122 | let lasFile = new LASFile(arraybuffer); 123 | let lasHeader = null; 124 | // Load the LAS file 125 | lasFile.open() 126 | .then(() => lasFile.getHeader()) 127 | .then(header => { 128 | lasHeader = header; 129 | //console.log('LAS header:'); 130 | //console.dir(header); 131 | let count = header.pointsCount 132 | console.log(`Input point count ${count}`); 133 | 134 | return lasFile.readData(count, 0, 0); 135 | }) 136 | .then(data => { 137 | //- console.log('readData result:'); 138 | //- console.dir(data); 139 | let Unpacker = lasFile.getUnpacker(); 140 | this.particleSystem.push(new Unpacker( 141 | data.buffer, data.count, lasHeader)); 142 | }) 143 | .then(() => { 144 | lasFile.close(); 145 | console.log(`Loaded point count ${this.particleSystem.pointsSoFar}`); 146 | resolve(); 147 | }) 148 | .then(() => { 149 | // Poor man's way to release resources 150 | delete lasFile.arraybuffer; 151 | lasFile.arraybuffer = null; 152 | lasHeader = null; 153 | lasFile = null; 154 | }) 155 | .catch(err => { 156 | lasFile.close(); alert(err); 157 | reject() 158 | }) 159 | }); // new Promise() 160 | } // loadData() 161 | 162 | bounds() { 163 | return this.input.bounds; 164 | } 165 | 166 | pointCount() { 167 | return this.particleSystem.pointsSoFar; 168 | } 169 | 170 | render(elem) { 171 | if (!this.pointCount()) { 172 | console.error('No LAS data -- nothing to render'); 173 | return; 174 | } 175 | 176 | this.particleSystem.init(elem); 177 | this.particleSystem.render(true); 178 | this.particleSystem.resize(elem); 179 | } 180 | 181 | setZRange(zmin, zmax) { 182 | this.particleSystem.setZRange(zmin, zmax); 183 | } 184 | 185 | updateBounds(newBounds) { 186 | if (!this.input.bounds) { 187 | this.input.bounds = newBounds; 188 | return; 189 | } 190 | // (else) 191 | for (let i=0; i<3; ++i) { 192 | let index = 2*i; 193 | if (newBounds[index] < this.input.bounds[index]) { 194 | this.input.bounds[index] = newBounds[index]; 195 | } 196 | 197 | ++index; 198 | if (newBounds[index] > this.input.bounds[index]) { 199 | this.input.bounds[index] = newBounds[index]; 200 | } 201 | } // for (i) 202 | } // updateBounds() 203 | 204 | } // LASPointCloud 205 | 206 | export { LASPointCloud } 207 | -------------------------------------------------------------------------------- /project/schema/model.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "GoeJS Jupyter Model", 4 | "description": "The data model passed from Jupyter kernel to server", 5 | "type": "object", 6 | "properties": { 7 | "layers": { 8 | "description": "The list of layers contained in the map", 9 | "type": "array", 10 | "items": { 11 | "description": "One layer in the map", 12 | "oneOf": [ 13 | { 14 | "title": "Map Feature Layer (GeoJS Jupyter Model),", 15 | "description": "A feature layer contained inside the GeoJS Jupyter Model,", 16 | "type": "object", 17 | "properties": { 18 | "layerType": { 19 | "description": "A literal identifying the layer type", 20 | "enum": [ 21 | "feature" 22 | ] 23 | }, 24 | "options": { 25 | "description": "The options object passed in to the layer constructor", 26 | "type": "object" 27 | }, 28 | "featureTypes": { 29 | "description": "A list of the feature types to use in this layer", 30 | "type": "array", 31 | "items": { 32 | "anyOf": [ 33 | { 34 | "enum": [ 35 | "point", 36 | "quad" 37 | ] 38 | } 39 | ] 40 | }, 41 | "minItems": 1, 42 | "uniqueItems": true 43 | }, 44 | "features": { 45 | "description": "The list of map features", 46 | "type": "array", 47 | "items": { 48 | "description": "One map feature (which may contain multiple entities)", 49 | "type": "object", 50 | "oneOf": [ 51 | { 52 | "description": "Generic geojs feature", 53 | "type": "object", 54 | "properties": { 55 | "featureType": { 56 | "description": "A literal indicating the feature type", 57 | "enum": [ 58 | "line", 59 | "point", 60 | "polygon", 61 | "quad" 62 | ] 63 | }, 64 | "options": { 65 | "description": "Options passed in to createFeature() method", 66 | "type": "object" 67 | } 68 | }, 69 | "required": [ 70 | "featureType" 71 | ] 72 | }, 73 | { 74 | "title": "Binary Data Feature Type (GeoJS Jupyter Model)", 75 | "description": "Any feature that is input as binary data", 76 | "type": "object", 77 | "properties": { 78 | "featureType": { 79 | "description": "A literal indicating the feature type", 80 | "enum": [ 81 | "pointcloud" 82 | ] 83 | }, 84 | "options": { 85 | "description": "Options passed to createFeature() method", 86 | "type": "object" 87 | }, 88 | "data": { 89 | "description": "A uuencoded copy of the file contents", 90 | "type": [ 91 | "string", 92 | "array" 93 | ] 94 | }, 95 | "url": { 96 | "description": "The url for downloading the data", 97 | "type": "string" 98 | } 99 | }, 100 | "additionalProperties": false, 101 | "required": [ 102 | "featureType" 103 | ] 104 | }, 105 | { 106 | "title": "GeoJSON Feature (GeoJS Jupyter Model)", 107 | "description": "A GeoJSON feature", 108 | "type": "object", 109 | "properties": { 110 | "featureType": { 111 | "description": "A literal indicating the feature type", 112 | "enum": [ 113 | "geojson" 114 | ] 115 | }, 116 | "options": { 117 | "description": "Options passed in to createFeature() method", 118 | "type": "object" 119 | }, 120 | "data": { 121 | "description": "This is the geojson object (not validated)", 122 | "type": "object" 123 | }, 124 | "url": { 125 | "description": "The url to a geojson file", 126 | "type": "string" 127 | } 128 | }, 129 | "additionalProperties": false, 130 | "required": [ 131 | "featureType" 132 | ] 133 | } 134 | ] 135 | } 136 | } 137 | }, 138 | "required": [ 139 | "layerType" 140 | ], 141 | "additionalProperties": false 142 | }, 143 | { 144 | "title": "Map OSM Layer (GeoJS Jupyter Model),", 145 | "description": "An OSM layer contained inside the GeoJS Jupyter Model", 146 | "type": "object", 147 | "properties": { 148 | "layerType": { 149 | "description": "A literal identifying the layer type", 150 | "enum": [ 151 | "osm" 152 | ] 153 | }, 154 | "options": { 155 | "description": "The options object passed in to the layer constructor", 156 | "type": "object" 157 | } 158 | }, 159 | "required": [ 160 | "layerType" 161 | ], 162 | "additionalProperties": false 163 | } 164 | ] 165 | } 166 | }, 167 | "options": { 168 | "description": "The options object passed in to the map constructor", 169 | "type": "object" 170 | }, 171 | "viewpoint": { 172 | "description": "The viewpoint specification (optional)", 173 | "type": [ 174 | "null", 175 | "object" 176 | ] 177 | } 178 | }, 179 | "additionalProperties": false 180 | } 181 | -------------------------------------------------------------------------------- /src/pointcloud/particlesystem.js: -------------------------------------------------------------------------------- 1 | // VTK JS import 2 | import vtkjsPackage from 'vtk.js/package.json'; 3 | console.log(`Using vtk.js version ${vtkjsPackage.version}`); 4 | 5 | import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; 6 | import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; 7 | import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; 8 | import vtkPoints from 'vtk.js/Sources/Common/Core/Points'; 9 | import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; 10 | import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; 11 | import vtkOpenGLRenderWindow from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow'; 12 | import vtkRenderWindowInteractor from 'vtk.js/Sources/Rendering/Core/RenderWindowInteractor'; 13 | import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; 14 | import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; 15 | 16 | /** 17 | * An object that manages a bunch of particle systems 18 | */ 19 | var ParticleSystem = function() { 20 | this.pss = []; // particle systems in use 21 | 22 | this.mx = null; 23 | this.mn = null; 24 | this.cg = null; 25 | this.cn = null; 26 | this.cx = null; 27 | this.in_x = null; 28 | this.in_y = null; 29 | this.klass = null; 30 | this.pointsSoFar = 0; 31 | this.zrange = null; 32 | 33 | this.renderer = null; 34 | this.renderWindow = null; 35 | }; 36 | 37 | /** 38 | * 39 | */ 40 | ParticleSystem.prototype.push = function(lasBuffer) { 41 | var count = lasBuffer.pointsCount, 42 | p, z, 43 | cg = null, 44 | mx = null, 45 | mn = null, 46 | cn = null, 47 | cx = null, 48 | in_x = null, 49 | in_n = null, 50 | klass = null; 51 | 52 | var pointBuffer = new Float32Array(count * 3); 53 | var cellBuffer = new Int32Array(count+1); 54 | var scalarBuffer = new Float32Array(count); 55 | cellBuffer[0] = count; 56 | var maxz, minz; 57 | 58 | if (this.zrange) { 59 | // Offset values by same amount as point conversion 60 | minz = this.zrange[0] - lasBuffer.mins[2]; 61 | maxz = this.zrange[1] - lasBuffer.mins[2]; 62 | } 63 | else { 64 | // Autoscale 65 | for ( var i = 0; i < count; i ++) { 66 | p = lasBuffer.getPoint(i); 67 | z = p.position[2] * lasBuffer.scale[2] + 68 | (lasBuffer.offset[2] - lasBuffer.mins[2]); 69 | if (maxz === undefined) { 70 | maxz = z; 71 | minz = z; 72 | } else { 73 | if (z > maxz) { 74 | maxz = z; 75 | } 76 | if (z < minz) { 77 | minz = z; 78 | } 79 | } // (else) 80 | } // for (i) 81 | } 82 | 83 | 84 | for ( var i = 0; i < count; i ++) { 85 | var p = lasBuffer.getPoint(i); 86 | 87 | pointBuffer[i*3] = p.position[0] * lasBuffer.scale[0] + 88 | (lasBuffer.offset[0] - lasBuffer.mins[0]); 89 | pointBuffer[i*3+1] = p.position[1] * lasBuffer.scale[1] + 90 | (lasBuffer.offset[1] - lasBuffer.mins[1]); 91 | pointBuffer[i*3+2] = p.position[2] * lasBuffer.scale[2] + 92 | (lasBuffer.offset[2] - lasBuffer.mins[2]); 93 | 94 | cellBuffer[i+1] = i; 95 | scalarBuffer[i] = (pointBuffer[i*3+2] - minz)/(maxz - minz); 96 | } 97 | 98 | // Dump first and last points: 99 | // let n = 0; 100 | // console.log(`pointBuffer[${n}]`); 101 | // let coords_list = []; 102 | // for (let i=0; i<3; ++i) { 103 | // let s = Number(pointBuffer[n+i]).toFixed(2); 104 | // coords_list.push(s); 105 | // } 106 | // let coords_string = coords_list.join(','); 107 | // console.log(`Point ${n}: (${coords_string})`); 108 | 109 | // coords_list = []; 110 | // n = 3*(count - 1); 111 | // for (let i=0; i<3; ++i) { 112 | // let s = Number(pointBuffer[n+i]).toFixed(2); 113 | // coords_list.push(s); 114 | // } 115 | // coords_string = coords_list.join(','); 116 | // console.log(`Point ${count-1}: (${coords_string})`); 117 | 118 | const polydata = vtkPolyData.newInstance(); 119 | polydata.getPoints().setData(pointBuffer, 3); 120 | polydata.getVerts().setData(cellBuffer); 121 | 122 | const dataarray = vtkDataArray.newInstance({values:scalarBuffer, name: 'data'}); 123 | polydata.getPointData().setScalars(dataarray); 124 | 125 | this.pss.push(polydata); 126 | 127 | this.pointsSoFar += count; 128 | //console.log(`Loaded ${count} points into the particle system`); 129 | }; 130 | 131 | /** 132 | * 133 | */ 134 | ParticleSystem.prototype.normalizePositionsWithOffset = function(offset) { 135 | var _this = this; 136 | 137 | var off = offset.clone(); 138 | 139 | this.correctiveOffset = off.clone().sub(_this.corrective); 140 | this.cg.sub(off); 141 | this.mn.sub(off); 142 | this.mx.sub(off); 143 | }; 144 | 145 | /** 146 | * 147 | */ 148 | ParticleSystem.prototype.init = function(elem) { 149 | if (!this.renderer) { 150 | this.renderer = vtkRenderer.newInstance({ background: [0.1, 0.1, 0.1] });; 151 | this.openglRenderWindow = vtkOpenGLRenderWindow.newInstance(); 152 | this.renderWindow = vtkRenderWindow.newInstance(); 153 | this.renderWindow.addRenderer(this.renderer); 154 | this.renderWindow.addView(this.openglRenderWindow); 155 | 156 | this.openglRenderWindow.setContainer(elem); 157 | 158 | const interactor = vtkRenderWindowInteractor.newInstance(); 159 | interactor.setView(this.openglRenderWindow); 160 | interactor.initialize(); 161 | interactor.bindEvents(elem); 162 | } 163 | } 164 | 165 | /** 166 | * Render particle system using vtk.js 167 | */ 168 | ParticleSystem.prototype.setZRange = function(zmin, zmax) { 169 | if (!this.zrange) { 170 | this.zrange = new Array(2); 171 | } 172 | this.zrange[0] = zmin; 173 | this.zrange[1] = zmax; 174 | } 175 | 176 | /** 177 | * Render particle system using vtk.js 178 | */ 179 | ParticleSystem.prototype.render = function(firstTime) { 180 | if (firstTime) { 181 | for (var i = 0; i < this.pss.length; ++i) { 182 | const actor = vtkActor.newInstance(); 183 | actor.getProperty().setPointSize(5); 184 | const mapper = vtkMapper.newInstance(); 185 | mapper.setInputData(this.pss[i]); 186 | actor.setMapper(mapper); 187 | this.renderer.addActor(actor); 188 | } 189 | 190 | this.renderer.resetCamera(); 191 | } 192 | 193 | this.renderWindow.render(); 194 | } 195 | 196 | /** 197 | * Handle window resize 198 | */ 199 | ParticleSystem.prototype.resize = function(elem) { 200 | const dims = elem.getBoundingClientRect(); 201 | const windowWidth = Math.floor(dims.width); 202 | const windowHeight = Math.floor(dims.height); 203 | this.openglRenderWindow.setSize(windowWidth, windowHeight); 204 | this.render(); 205 | } 206 | 207 | ParticleSystem.prototype.destroy = function() { 208 | if (!this.renderer) { // never initialized 209 | return; 210 | } 211 | 212 | // Delete actors 213 | let actors = this.renderer.getActors(); 214 | for (let actor of actors) { 215 | this.renderer.removeActor(actor); 216 | actor.delete(); 217 | } 218 | 219 | this.renderWindow.delete(); 220 | this.renderer.delete(); 221 | } 222 | 223 | export { ParticleSystem }; 224 | -------------------------------------------------------------------------------- /jupyterlab_geojs/pointcloudfeature.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from operator import add 3 | import os 4 | 5 | from . import gdalutils 6 | from .geojsfeature import GeoJSFeature 7 | from .lasutils import LASMetadata, LASParser, LASPointAttributes 8 | 9 | class PointCloudFeature(GeoJSFeature): 10 | '''''' 11 | def __init__(self, data, **kwargs): 12 | super(PointCloudFeature, self).__init__('pointcloud', config_options=False, **kwargs) 13 | 14 | # Input source 15 | self._bounds = None # [xmin,xmax, ymin,ymax, zmin,zmax] 16 | self._filenames = None 17 | self._point_formats = dict() # 18 | self._point_count = 0 19 | self._point_count_by_return = [0]*5 20 | self._projection_wkt = '' 21 | 22 | # Note: this version of the feature only supports filename inputs, 23 | # not raw data or network url's 24 | if isinstance(data, list): 25 | self._filenames = data 26 | elif isinstance(data, str): 27 | self._filenames = [data] 28 | else: 29 | raise Exception('Input data is not list or string: {}'.format(data)) 30 | # Check that files exist 31 | for f in self._filenames: 32 | if not os.path.exists(f): 33 | raise Exception('Cannot find file {}'.format(f)) 34 | 35 | # Parse headers to initialize member data 36 | parser = LASParser() 37 | for i,filename in enumerate(self._filenames): 38 | metadata = None 39 | with open(filename, 'rb') as instream: 40 | metadata = parser.parse(instream) 41 | self._check_support(metadata) 42 | 43 | h = metadata.header 44 | if h.legacy_point_count: 45 | point_count = h.legacy_point_count 46 | else: 47 | point_count = h.number_of_point_records 48 | 49 | # Update bounds 50 | bounds = [h.min_x,h.max_x, h.min_y,h.max_y, h.min_z,h.max_z] 51 | if self._bounds is None: 52 | self._bounds = bounds 53 | else: 54 | for i in [0, 2, 4]: 55 | if bounds[i] < self._bounds[i]: 56 | self._bounds[i] = bounds[i] 57 | for i in [1, 3, 5]: 58 | if bounds[i] > self._bounds[i]: 59 | self._bounds[i] = bounds[i] 60 | 61 | # Update point formats 62 | format = h.point_data_record_format 63 | format_count = self._point_formats.get(format, 0) 64 | self._point_formats[format] = format_count + point_count 65 | 66 | # Update point count 67 | self._point_count += point_count 68 | 69 | # Update point count by return 70 | if h.legacy_number_of_points_by_return: 71 | self._point_count_by_return = list(map(add, 72 | self._point_count_by_return, 73 | h.legacy_number_of_points_by_return)) 74 | else: 75 | self._point_count_by_return = list(map(add, 76 | self._point_count_by_return, 77 | h.number_of_points_by_return)) 78 | 79 | # Update/check projection wkt 80 | if i == 0: 81 | self._projection_wkt = metadata.projection_wkt 82 | elif metadata.projection_wkt != self._projection_wkt: 83 | msg = ' '.join([ 84 | 'Project mismatch between input files.' 85 | 'File {} is projection {}'.format(self._filenames[0], self._projection_wkt), 86 | 'File {} is projection {}'.format(self._filenames[i], metadata.projection_wkt) 87 | ]) 88 | raise Exception(msg) 89 | 90 | def get_bounds(self, as_lonlat=False): 91 | '''Returns tuple (xmin,xmax,ymin,ymax,zmin,zmax) 92 | 93 | ''' 94 | return tuple(self._bounds) 95 | 96 | def get_point_data_record_formats(self): 97 | '''Returns dictionary of 98 | 99 | ''' 100 | return self._point_formats 101 | 102 | def get_point_attributes(self): 103 | '''Returns tuple of strings 104 | 105 | For LAS data, return value is based on point data record format 106 | ''' 107 | format = self._point_data_record_format 108 | # Mod by 128, because LAZ headers add 128 109 | # Per http://www.cs.unc.edu/~isenburg/lastools/download/laszip.pdf 110 | format %= 128 111 | atts = LASPointAttributes.get(format) 112 | return atts 113 | 114 | def get_point_count(self): 115 | '''Returns unsigned long 116 | ''' 117 | return self._point_count 118 | 119 | def get_point_count_by_return(self): 120 | ''' Returns standard LAS 5-tuple 121 | 122 | ''' 123 | return tuple(self._point_count_by_return) 124 | 125 | def get_proj_string(self): 126 | '''Returns Proj4 string 127 | 128 | Requires gdal to be installed in the python environment 129 | ''' 130 | if self._projection_wkt is None: 131 | return None 132 | elif gdalutils.is_gdal_loaded(): 133 | from osgeo import osr 134 | proj = osr.SpatialReference() 135 | proj.ImportFromWkt(this._projection_wkt) 136 | return proj.ExportToProj4() 137 | else: 138 | raise Exception('Cannot convert projection because GDAL not installed') 139 | 140 | return None 141 | 142 | def get_wkt_string(self): 143 | '''Returns coordinate system WKT 144 | 145 | ''' 146 | return self._projection_wkt 147 | 148 | 149 | def _build_display_model(self): 150 | '''Builds data model 151 | 152 | Represents point cloud data as list of uuencoded strings 153 | ''' 154 | # Initialize output object 155 | display_model = super(PointCloudFeature, self)._build_display_model() 156 | 157 | # Build an array of base64-encoded strings, one for each LAS file 158 | las_list = list() 159 | for filename in self._filenames: 160 | with open(filename, 'rb') as f: 161 | las_data = f.read() 162 | 163 | encoded_bytes = base64.b64encode(las_data) 164 | # Have to decode as ascii so that Jupyter can jsonify 165 | encoded_string = encoded_bytes.decode('ascii') 166 | las_list.append(encoded_string) 167 | 168 | display_model['data'] = las_list 169 | #print('las_list type {}: {}'.format(type(las_list), las_list)) 170 | return display_model 171 | 172 | def _check_support(self, metadata): 173 | '''Checks las version and point record format. 174 | 175 | Our current code does not support all versions of las files: 176 | * Only supports file versions 1.0-1.3 (not 1.4) 177 | * Only supports point-record formats 0-3 (not 4-10) 178 | * Only supports uncompressed data (not laz) 179 | 180 | ''' 181 | if metadata is None: 182 | raise Exception('LAS Metadata missing') 183 | 184 | std_msg = 'Only LAS versions 1.0-1.3, point formats 0-3, no compression' 185 | h = metadata.header 186 | 187 | # Only LAS file versions 1.0 - 1.3 188 | version_string = '{}.{}'.format(h.version_major, h.version_minor) 189 | if h.version_major != 1 or h.version_minor > 3: 190 | raise Exception('INVALID: Cannot load LAS file version {}. {}'.format( 191 | version_string, std_msg)) 192 | 193 | # Only uncompressed data 194 | if h.point_data_record_format >= 128: 195 | raise Exception('INVALID: Cannot load laz/compressed data. {}'.format(std_msg)) 196 | 197 | # Only point record formats 0-3 198 | if h.point_data_record_format > 3: 199 | raise Exception('INVALID: Cannot load point record format {}. {}'.format( 200 | h.point_data_record_format, std_msg)) 201 | -------------------------------------------------------------------------------- /jupyterlab_geojs/rasterfeature.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import glob 3 | import json 4 | import os 5 | import pkg_resources 6 | import time 7 | 8 | from .geojsfeature import GeoJSFeature 9 | 10 | ''' 11 | Raster features require GDAL to be installed on the kernel. 12 | Check if GDAL is available 13 | ''' 14 | try: 15 | pkg_resources.get_distribution('gdal') 16 | except pkg_resources.DistributionNotFound: 17 | HAS_GDAL = False 18 | else: 19 | HAS_GDAL = True 20 | from osgeo import gdal, osr 21 | 22 | # Specify temp dir to use for copying gdal datasets 23 | try: 24 | pkg_resources.get_distribution('jupyter_core.paths') 25 | except pkg_resources.DistributionNotFound: 26 | pass 27 | TEMP_DIR = os.path.expanduser('~/.jupyterlab_geojs') 28 | else: 29 | import jupyter_core.paths 30 | runtime_dir = jupyter_core.paths.jupyter_runtime_dir() 31 | TEMP_DIR = os.path.join(runtime_dir, 'geojs') 32 | #print('Using temp_dir {}'.format(TEMP_DIR)) 33 | 34 | 35 | class RasterFeature(GeoJSFeature): 36 | '''Initialize raster feature 37 | 38 | @param data GDALDataset 39 | @param filename string 40 | ''' 41 | def __init__(self, data, **kwargs): 42 | '''''' 43 | self._gdal_dataset = None 44 | 45 | if not HAS_GDAL: 46 | raise Exception('Cannot create raster features -- GDAL not installed') 47 | 48 | # Model raster dataset as geojs quad feature with png image 49 | super(RasterFeature, self).__init__('quad', **kwargs) 50 | 51 | # Determine if input data is GDAL dataset or filename 52 | if isinstance(data, gdal.Dataset): 53 | self._gdal_dataset = data 54 | elif isinstance(data, str): 55 | # For now presume it is a file/path (no url support) 56 | filename = data 57 | 58 | # Load data here, because javascript cannot load from 59 | # local filesystem due to browser security restriction. 60 | if not os.path.exists(filename): 61 | raise Exception('Cannot find file {}'.format(filename)) 62 | 63 | # Load input file 64 | self._gdal_dataset = gdal.Open(filename, gdal.GA_ReadOnly) 65 | assert(self._gdal_dataset) 66 | 67 | def get_corner_points(self, as_lonlat=False): 68 | '''Returns corners points of image as list of coords: [[x0,y0], ...[x3,y3]] 69 | 70 | ''' 71 | if self._gdal_dataset is None: 72 | raise Exception('No dataset loaded') 73 | 74 | gt = self._gdal_dataset.GetGeoTransform() 75 | if gt is None: 76 | raise Exception('Cannot compute corners -- dataset has no geo transform') 77 | num_cols = self._gdal_dataset.RasterXSize 78 | num_rows = self._gdal_dataset.RasterYSize 79 | corners = list() 80 | for px in [0, num_cols]: 81 | for py in [0, num_rows]: 82 | x = gt[0] + px*gt[1] + py*gt[2] 83 | y = gt[3] + px*gt[4] + py*gt[5] 84 | corners.append([x, y]) 85 | 86 | if as_lonlat: 87 | spatial_ref = osr.SpatialReference() 88 | spatial_ref.ImportFromWkt(self.get_wkt_string()) 89 | corners = self._convert_to_lonlat(corners, spatial_ref) 90 | 91 | return corners 92 | 93 | def get_proj4_string(self): 94 | '''''' 95 | wkt = self.get_wkt_string() 96 | if not wkt: 97 | raise Exception('dataset missing projection info') 98 | ref = osr.SpatialReference() 99 | ref.ImportFromWkt(wkt) 100 | proj4_string = ref.ExportToProj4() 101 | return proj4_string 102 | 103 | def get_wkt_string(self): 104 | '''''' 105 | if self._gdal_dataset is None: 106 | raise Exception('No dataset loaded') 107 | return self._gdal_dataset.GetProjection() 108 | 109 | def _build_display_model(self): 110 | '''Builds model as quad with image data''' 111 | display_model = super(RasterFeature, self)._build_display_model() 112 | options = display_model.get('options', {}) 113 | 114 | # Set up coordinate transform to lonlat coordinates 115 | input_ref = osr.SpatialReference() 116 | input_ref.ImportFromWkt(self._gdal_dataset.GetProjection()) 117 | lonlat_ref = osr.SpatialReference() 118 | lonlat_ref .ImportFromEPSG(4326) 119 | ref_transform = osr.CoordinateTransformation(input_ref, lonlat_ref) 120 | 121 | # Compute corner points 122 | gt = self._gdal_dataset.GetGeoTransform() 123 | if gt is None: 124 | raise Exception('Cannot render raster feature -- input has no geo transform') 125 | num_cols = self._gdal_dataset.RasterXSize 126 | num_rows = self._gdal_dataset.RasterYSize 127 | corners = list() 128 | for px in [0, num_cols]: 129 | for py in [0, num_rows]: 130 | native_x = gt[0] + px*gt[1] + py*gt[2] 131 | native_y = gt[3] + px*gt[4] + py*gt[5] 132 | 133 | # Convert to lon-lat 134 | x,y,z = ref_transform.TransformPoint(native_x, native_y) 135 | corners.append([x, y]) 136 | 137 | # Feature data is array with image & corners points for each feature 138 | # We have only 1 feature 139 | # Set corner points 140 | feature_data = dict() 141 | feature_data['ul'] = {'x': corners[0][0], 'y': corners[0][1]} 142 | feature_data['ll'] = {'x': corners[1][0], 'y': corners[1][1]} 143 | feature_data['ur'] = {'x': corners[2][0], 'y': corners[2][1]} 144 | feature_data['lr'] = {'x': corners[3][0], 'y': corners[3][1]} 145 | #options['data'] = [data] 146 | 147 | # Get dataset's gcs 148 | projection = self._gdal_dataset.GetProjection() 149 | spatial_ref = osr.SpatialReference(wkt=projection) 150 | gcs_name = spatial_ref.GetAttrValue('AUTHORITY', 0) 151 | gcs_value = spatial_ref.GetAttrValue('AUTHORITY', 1) 152 | gcs_string = '{}:{}'.format(gcs_name, gcs_value) 153 | #print(gcs_string) 154 | #options['gcs'] = gcs_string 155 | 156 | # Need temp directory to create png dataset 157 | os.makedirs(TEMP_DIR, exist_ok=True) 158 | 159 | # Generate filename for interim png file 160 | # Use timestamp (presumes creating < 1 png image per second) 161 | ts = int(time.time()) 162 | png_filename = '{}.png'.format(ts) 163 | 164 | # Create png dataset 165 | png_path = os.path.join(TEMP_DIR, png_filename) 166 | png_driver = gdal.GetDriverByName('PNG') 167 | png_dataset = png_driver.CreateCopy(png_path, self._gdal_dataset, strict=0) 168 | assert(png_dataset) 169 | png_dataset = None 170 | 171 | # Read png file and convert to base64 data 172 | with open(png_path, 'rb') as fp: 173 | encoded_bytes = base64.b64encode(fp.read()) 174 | encoded_string = 'data:image/png;base64,' + encoded_bytes.decode('ascii') 175 | #print(encoded_string) 176 | feature_data['image'] = encoded_string 177 | 178 | options['data'] = [feature_data] 179 | display_model['options'] = options 180 | 181 | # Remove temp files (gdal creates auxilliary file in addition to .png file) 182 | pattern = '{path}/{prefix}.*'.format(path=TEMP_DIR, prefix=ts) 183 | for path in glob.iglob(pattern): 184 | os.remove(path) 185 | 186 | return display_model 187 | 188 | 189 | def _convert_to_lonlat(self, points, from_spatial_ref): 190 | '''Converts a list of [x,y] points to a list with [lon, lat] coords 191 | 192 | 193 | ''' 194 | # Set up transform 195 | lonlat_ref = osr.SpatialReference() 196 | lonlat_ref .ImportFromEPSG(4326) 197 | ref_transform = osr.CoordinateTransformation(from_spatial_ref, lonlat_ref) 198 | lonlat_points = list() 199 | for point in points: 200 | native_x = point[0] 201 | native_y = point[1] 202 | x,y,z = ref_transform.TransformPoint(native_x, native_y) 203 | lonlat_points.append([x, y]) 204 | return lonlat_points 205 | -------------------------------------------------------------------------------- /notebooks/geojson_data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Initialize the map and add OSM and feature layers.\n", 10 | "# In general, notebooks should not display the map in each cell,\n", 11 | "# because that will use up all available gl contexts quickly.\n", 12 | "# Best practice is to comment out the geomap for cells that\n", 13 | "# are known to be working.\n", 14 | "\n", 15 | "from jupyterlab_geojs import Scene\n", 16 | "kwcoords = {'y': 43.0, 'x': -76.5}\n", 17 | "scene = Scene(center=kwcoords, zoom=7)\n", 18 | "\n", 19 | "osm_layer = scene.create_layer('osm')\n", 20 | "feature_layer = scene.create_layer('feature')\n", 21 | "\n", 22 | "scene;" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 2, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "# Add GeoJSON features by creating json object\n", 32 | "ny_poly = { \"type\": \"Feature\",\n", 33 | " \"geometry\": {\n", 34 | " \"type\": \"Polygon\",\n", 35 | " \"coordinates\": [[\n", 36 | " [-78.878369, 42.886447],\n", 37 | " [-76.147424, 43.048122],\n", 38 | " [-75.910756, 43.974784],\n", 39 | " [-73.756232, 42.652579],\n", 40 | " [-75.917974, 42.098687],\n", 41 | " [-78.429927, 42.083639],\n", 42 | " [-78.878369, 42.886447]\n", 43 | " ]]\n", 44 | " },\n", 45 | " \"properties\": {\n", 46 | " \"author\": \"Kitware\",\n", 47 | " \"cities\": [\"Buffalo\", \"Syracuse\", \"Watertown\", \"Albany\", \"Binghamton\", \"Olean\"]\n", 48 | " }\n", 49 | "}\n", 50 | "feature_layer.create_feature('geojson', data=ny_poly)\n", 51 | "scene;" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 3, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "# 2. Add GeoJSON features by specifying local file\n", 61 | "feature_layer.create_feature('geojson', data=\"./ny-points.geojson\")\n", 62 | "scene;" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 4, 68 | "metadata": {}, 69 | "outputs": [ 70 | { 71 | "data": { 72 | "application/geojs+json": { 73 | "layers": [ 74 | { 75 | "layerType": "osm", 76 | "options": {} 77 | }, 78 | { 79 | "features": [ 80 | { 81 | "data": { 82 | "geometry": { 83 | "coordinates": [ 84 | [ 85 | [ 86 | -78.878369, 87 | 42.886447 88 | ], 89 | [ 90 | -76.147424, 91 | 43.048122 92 | ], 93 | [ 94 | -75.910756, 95 | 43.974784 96 | ], 97 | [ 98 | -73.756232, 99 | 42.652579 100 | ], 101 | [ 102 | -75.917974, 103 | 42.098687 104 | ], 105 | [ 106 | -78.429927, 107 | 42.083639 108 | ], 109 | [ 110 | -78.878369, 111 | 42.886447 112 | ] 113 | ] 114 | ], 115 | "type": "Polygon" 116 | }, 117 | "properties": { 118 | "author": "Kitware", 119 | "cities": [ 120 | "Buffalo", 121 | "Syracuse", 122 | "Watertown", 123 | "Albany", 124 | "Binghamton", 125 | "Olean" 126 | ] 127 | }, 128 | "type": "Feature" 129 | }, 130 | "featureType": "geojson", 131 | "options": {} 132 | }, 133 | { 134 | "data": { 135 | "features": [ 136 | { 137 | "geometry": { 138 | "coordinates": [ 139 | -73.756232, 140 | 42.652579 141 | ], 142 | "type": "Point" 143 | }, 144 | "id": null, 145 | "properties": { 146 | "name": "Albany", 147 | "zipcode": 12201 148 | }, 149 | "type": "Feature" 150 | }, 151 | { 152 | "geometry": { 153 | "coordinates": [ 154 | -75.917974, 155 | 42.098687 156 | ], 157 | "type": "Point" 158 | }, 159 | "id": null, 160 | "properties": { 161 | "name": "Binghamton", 162 | "zipcode": 13901 163 | }, 164 | "type": "Feature" 165 | }, 166 | { 167 | "geometry": { 168 | "coordinates": [ 169 | -78.878369, 170 | 42.886447 171 | ], 172 | "type": "Point" 173 | }, 174 | "id": null, 175 | "properties": { 176 | "name": "Buffalo", 177 | "zipcode": 14201 178 | }, 179 | "type": "Feature" 180 | }, 181 | { 182 | "geometry": { 183 | "coordinates": [ 184 | -74.005941, 185 | 40.712784 186 | ], 187 | "type": "Point" 188 | }, 189 | "id": null, 190 | "properties": { 191 | "name": "New York City", 192 | "zipcode": 10001 193 | }, 194 | "type": "Feature" 195 | }, 196 | { 197 | "geometry": { 198 | "coordinates": [ 199 | -78.429927, 200 | 42.083639 201 | ], 202 | "type": "Point" 203 | }, 204 | "id": null, 205 | "properties": { 206 | "name": "Olean", 207 | "zipcode": 14760 208 | }, 209 | "type": "Feature" 210 | }, 211 | { 212 | "geometry": { 213 | "coordinates": [ 214 | -75.063775, 215 | 42.452857 216 | ], 217 | "type": "Point" 218 | }, 219 | "id": null, 220 | "properties": { 221 | "name": "Oneonto", 222 | "zipcode": 13820 223 | }, 224 | "type": "Feature" 225 | }, 226 | { 227 | "geometry": { 228 | "coordinates": [ 229 | -76.147424, 230 | 43.048122 231 | ], 232 | "type": "Point" 233 | }, 234 | "id": null, 235 | "properties": { 236 | "name": "Syracuse", 237 | "zipcode": 13201 238 | }, 239 | "type": "Feature" 240 | } 241 | ], 242 | "type": "FeatureCollection" 243 | }, 244 | "featureType": "geojson", 245 | "options": {} 246 | }, 247 | { 248 | "featureType": "geojson", 249 | "options": {}, 250 | "url": "https://data.kitware.com/api/v1/file/5ad8e9678d777f0685794ac6/download/ny-multilinestring.geojson" 251 | } 252 | ], 253 | "layerType": "feature", 254 | "options": {} 255 | } 256 | ], 257 | "options": { 258 | "center": { 259 | "x": -76.5, 260 | "y": 43, 261 | "z": 0 262 | }, 263 | "node": { 264 | "jQuery331079451237692747411": { 265 | "events": { 266 | "contextmenu": [ 267 | { 268 | "guid": 171, 269 | "namespace": "geojs", 270 | "origType": "contextmenu", 271 | "type": "contextmenu" 272 | } 273 | ], 274 | "dragover": [ 275 | { 276 | "guid": 164, 277 | "namespace": "geo", 278 | "origType": "dragover", 279 | "type": "dragover" 280 | } 281 | ], 282 | "dragstart": [ 283 | { 284 | "guid": 170, 285 | "namespace": "", 286 | "origType": "dragstart", 287 | "type": "dragstart" 288 | } 289 | ], 290 | "drop": [ 291 | { 292 | "guid": 165, 293 | "namespace": "geo", 294 | "origType": "drop", 295 | "type": "drop" 296 | } 297 | ], 298 | "mousedown": [ 299 | { 300 | "guid": 168, 301 | "namespace": "geojs", 302 | "origType": "mousedown", 303 | "type": "mousedown" 304 | } 305 | ], 306 | "mousemove": [ 307 | { 308 | "guid": 167, 309 | "namespace": "geojs", 310 | "origType": "mousemove", 311 | "type": "mousemove" 312 | } 313 | ], 314 | "mouseup": [ 315 | { 316 | "guid": 169, 317 | "namespace": "geojs", 318 | "origType": "mouseup", 319 | "type": "mouseup" 320 | } 321 | ], 322 | "wheel": [ 323 | { 324 | "guid": 166, 325 | "namespace": "geojs", 326 | "origType": "wheel", 327 | "type": "wheel" 328 | } 329 | ] 330 | } 331 | }, 332 | "jQuery331079451237692747412": { 333 | "dataGeojsMap": {} 334 | } 335 | }, 336 | "zoom": 7 337 | }, 338 | "viewpoint": null 339 | }, 340 | "text/plain": [ 341 | "" 342 | ] 343 | }, 344 | "metadata": { 345 | "application/geojs+json": { 346 | "expanded": false 347 | } 348 | }, 349 | "output_type": "display_data" 350 | } 351 | ], 352 | "source": [ 353 | "# 3. Add GeoJSON features by downloading from a remote server\n", 354 | "url = 'https://data.kitware.com/api/v1/file/5ad8e9678d777f0685794ac6/download/ny-multilinestring.geojson'\n", 355 | "feature_layer.create_feature('geojson', data=url)\n", 356 | "scene" 357 | ] 358 | }, 359 | { 360 | "cell_type": "code", 361 | "execution_count": null, 362 | "metadata": {}, 363 | "outputs": [], 364 | "source": [] 365 | } 366 | ], 367 | "metadata": { 368 | "kernelspec": { 369 | "display_name": "Python 3", 370 | "language": "python", 371 | "name": "python3" 372 | }, 373 | "language_info": { 374 | "codemirror_mode": { 375 | "name": "ipython", 376 | "version": 3 377 | }, 378 | "file_extension": ".py", 379 | "mimetype": "text/x-python", 380 | "name": "python", 381 | "nbconvert_exporter": "python", 382 | "pygments_lexer": "ipython3", 383 | "version": "3.5.2" 384 | } 385 | }, 386 | "nbformat": 4, 387 | "nbformat_minor": 2 388 | } 389 | -------------------------------------------------------------------------------- /notebooks/basic_features.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from jupyterlab_geojs import Scene, LayerType, FeatureType\n", 10 | "scene = Scene()\n", 11 | "scene.center = {'x': -97.67, 'y': 31.80}\n", 12 | "scene.zoom = 4\n", 13 | "osm_layer = scene.create_layer(LayerType.OSM)\n", 14 | "feature_layer = scene.create_layer(LayerType.FEATURE)" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 3, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import math # for point size logic\n", 24 | "\n", 25 | "# Point data\n", 26 | "# Copied from http://opengeoscience.github.io/geojs/tutorials/simple_point/\n", 27 | "cities = [\n", 28 | " {'lon': -74.0059413, 'lat': 40.7127837, 'name': \"New York\", 'population': 8405837},\n", 29 | " {'lon': -118.2436849, 'lat': 34.0522342, 'name': \"Los Angeles\", 'population': 3884307},\n", 30 | " {'lon': -87.6297982, 'lat': 41.8781136, 'name': \"Chicago\", 'population': 2718782},\n", 31 | " {'lon': -95.3698028, 'lat': 29.7604267, 'name': \"Houston\", 'population': 2195914},\n", 32 | " {'lon': -75.1652215, 'lat': 39.9525839, 'name': \"Philadelphia\", 'population': 1553165},\n", 33 | " {'lon': -112.0740373, 'lat': 33.4483771, 'name': \"Phoenix\", 'population': 1513367}\n", 34 | "]\n", 35 | "feature_layer.clear()\n", 36 | "points = feature_layer.create_feature(FeatureType.POINT, data=cities)\n", 37 | "\n", 38 | "#points.position = [{'x':city['lon'], 'y':city['lat']} for city in cities]\n", 39 | "points.position = lambda city: {'x':city['lon'], 'y':city['lat']}\n", 40 | "\n", 41 | "style = {'fillColor': 'gray', 'strokeColor': 'black', 'strokeWidth': 2}\n", 42 | "\n", 43 | "# Scale point size (area) proportional to population\n", 44 | "populations = [city['population'] for city in cities]\n", 45 | "pmin = min(populations)\n", 46 | "rmin = 8 # minimum radius\n", 47 | "style['radius'] = lambda city: int(math.sqrt(rmin*rmin*city['population']/pmin))\n", 48 | "points.style = style\n", 49 | "\n", 50 | "points.enableTooltip = True" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 5, 56 | "metadata": {}, 57 | "outputs": [ 58 | { 59 | "name": "stdout", 60 | "output_type": "stream", 61 | "text": [ 62 | "matplotlib loaded\n" 63 | ] 64 | } 65 | ], 66 | "source": [ 67 | "# Apply color map to longitude\n", 68 | "try:\n", 69 | " # Use matplotlib colormap if available\n", 70 | " import matplotlib as mpl\n", 71 | " import matplotlib.cm\n", 72 | " MPL_LOADED = True\n", 73 | " print('matplotlib loaded')\n", 74 | " \n", 75 | " cmap = mpl.cm.get_cmap('Spectral')\n", 76 | " lons = [city['lon'] for city in cities]\n", 77 | " lon_norm = mpl.colors.Normalize(vmin=min(lons), vmax=max(lons))\n", 78 | " style['fillColor'] = lambda city: cmap(lon_norm(city['lon']))\n", 79 | "# style['fillColor'] = {'mplcolormap': 'Spectral', 'dataItem': 'lon'}\n", 80 | "# style['fillColor'] = Scene.mpl_colormap('Spectral', 'lon') # vrange=[min, max]\n", 81 | "except ImportError:\n", 82 | " MPL_LOADED = False\n", 83 | " print('matplotlib NOT loaded')\n", 84 | " # Use backup coloring (deprecate?)\n", 85 | " points.colormap = {\n", 86 | " 'colorseries': 'rainbow',\n", 87 | " 'field': 'lon',\n", 88 | " 'range': [-118, -74]\n", 89 | " }\n" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 6, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "# Quad data\n", 99 | "# Copied from http://opengeoscience.github.io/geojs/tutorials/video_on_map/\n", 100 | "data = [{\n", 101 | " 'ul': {'x': -129.0625, 'y': 42.13468456089552},\n", 102 | " 'lr': {'x': -100.9375, 'y': 29.348416310781797}\n", 103 | "}]\n", 104 | "quad = feature_layer.create_feature(FeatureType.QUAD, data)\n", 105 | "quad.style = {'color': 'magenta', 'opacity': 0.2}\n" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 7, 111 | "metadata": {}, 112 | "outputs": [ 113 | { 114 | "data": { 115 | "application/geojs+json": { 116 | "layers": [ 117 | { 118 | "layerType": "osm", 119 | "options": {} 120 | }, 121 | { 122 | "features": [ 123 | { 124 | "featureType": "point", 125 | "options": { 126 | "data": [ 127 | { 128 | "__i": 0, 129 | "lat": 40.7127837, 130 | "lon": -74.0059413, 131 | "name": "New York", 132 | "population": 8405837 133 | }, 134 | { 135 | "__i": 1, 136 | "lat": 34.0522342, 137 | "lon": -118.2436849, 138 | "name": "Los Angeles", 139 | "population": 3884307 140 | }, 141 | { 142 | "__i": 2, 143 | "lat": 41.8781136, 144 | "lon": -87.6297982, 145 | "name": "Chicago", 146 | "population": 2718782 147 | }, 148 | { 149 | "__i": 3, 150 | "lat": 29.7604267, 151 | "lon": -95.3698028, 152 | "name": "Houston", 153 | "population": 2195914 154 | }, 155 | { 156 | "__i": 4, 157 | "lat": 39.9525839, 158 | "lon": -75.1652215, 159 | "name": "Philadelphia", 160 | "population": 1553165 161 | }, 162 | { 163 | "__i": 5, 164 | "lat": 33.4483771, 165 | "lon": -112.0740373, 166 | "name": "Phoenix", 167 | "population": 1513367 168 | } 169 | ], 170 | "enableTooltip": true, 171 | "position": [ 172 | { 173 | "x": -74.0059413, 174 | "y": 40.7127837 175 | }, 176 | { 177 | "x": -118.2436849, 178 | "y": 34.0522342 179 | }, 180 | { 181 | "x": -87.6297982, 182 | "y": 41.8781136 183 | }, 184 | { 185 | "x": -95.3698028, 186 | "y": 29.7604267 187 | }, 188 | { 189 | "x": -75.1652215, 190 | "y": 39.9525839 191 | }, 192 | { 193 | "x": -112.0740373, 194 | "y": 33.4483771 195 | } 196 | ], 197 | "style": { 198 | "fillColor": [ 199 | "#5e4fa2", 200 | "#9e0142", 201 | "#aedea3", 202 | "#fafdb8", 203 | "#535ca8", 204 | "#e04f4a" 205 | ], 206 | "radius": [ 207 | 18, 208 | 12, 209 | 10, 210 | 9, 211 | 8, 212 | 8 213 | ], 214 | "strokeColor": "black", 215 | "strokeWidget": 2 216 | } 217 | } 218 | }, 219 | { 220 | "featureType": "quad", 221 | "options": { 222 | "data": [ 223 | { 224 | "lr": { 225 | "x": -100.9375, 226 | "y": 29.348416310781797 227 | }, 228 | "ul": { 229 | "x": -129.0625, 230 | "y": 42.13468456089552 231 | } 232 | } 233 | ], 234 | "style": { 235 | "color": "magenta", 236 | "opacity": 0.2 237 | } 238 | } 239 | } 240 | ], 241 | "layerType": "feature", 242 | "options": {} 243 | } 244 | ], 245 | "options": { 246 | "center": { 247 | "x": -97.67, 248 | "y": 31.8, 249 | "z": 0 250 | }, 251 | "node": { 252 | "jQuery331085922562241861661": { 253 | "events": { 254 | "contextmenu": [ 255 | { 256 | "guid": 73, 257 | "namespace": "geojs", 258 | "origType": "contextmenu", 259 | "type": "contextmenu" 260 | } 261 | ], 262 | "dragover": [ 263 | { 264 | "guid": 66, 265 | "namespace": "geo", 266 | "origType": "dragover", 267 | "type": "dragover" 268 | } 269 | ], 270 | "dragstart": [ 271 | { 272 | "guid": 72, 273 | "namespace": "", 274 | "origType": "dragstart", 275 | "type": "dragstart" 276 | } 277 | ], 278 | "drop": [ 279 | { 280 | "guid": 67, 281 | "namespace": "geo", 282 | "origType": "drop", 283 | "type": "drop" 284 | } 285 | ], 286 | "mousedown": [ 287 | { 288 | "guid": 70, 289 | "namespace": "geojs", 290 | "origType": "mousedown", 291 | "type": "mousedown" 292 | } 293 | ], 294 | "mousemove": [ 295 | { 296 | "guid": 69, 297 | "namespace": "geojs", 298 | "origType": "mousemove", 299 | "type": "mousemove" 300 | } 301 | ], 302 | "mouseup": [ 303 | { 304 | "guid": 71, 305 | "namespace": "geojs", 306 | "origType": "mouseup", 307 | "type": "mouseup" 308 | } 309 | ], 310 | "wheel": [ 311 | { 312 | "guid": 68, 313 | "namespace": "geojs", 314 | "origType": "wheel", 315 | "type": "wheel" 316 | } 317 | ] 318 | } 319 | }, 320 | "jQuery331085922562241861662": { 321 | "dataGeojsMap": {} 322 | } 323 | }, 324 | "zoom": 4 325 | }, 326 | "viewpoint": null 327 | }, 328 | "text/plain": [ 329 | "" 330 | ] 331 | }, 332 | "metadata": { 333 | "application/geojs+json": { 334 | "expanded": false 335 | } 336 | }, 337 | "output_type": "display_data" 338 | } 339 | ], 340 | "source": [ 341 | "scene" 342 | ] 343 | }, 344 | { 345 | "cell_type": "code", 346 | "execution_count": null, 347 | "metadata": {}, 348 | "outputs": [], 349 | "source": [] 350 | } 351 | ], 352 | "metadata": { 353 | "kernelspec": { 354 | "display_name": "Python 3", 355 | "language": "python", 356 | "name": "python3" 357 | }, 358 | "language_info": { 359 | "codemirror_mode": { 360 | "name": "ipython", 361 | "version": 3 362 | }, 363 | "file_extension": ".py", 364 | "mimetype": "text/x-python", 365 | "name": "python", 366 | "nbconvert_exporter": "python", 367 | "pygments_lexer": "ipython3", 368 | "version": "3.5.2" 369 | } 370 | }, 371 | "nbformat": 4, 372 | "nbformat_minor": 2 373 | } 374 | -------------------------------------------------------------------------------- /jupyterlab_geojs/lasutils.py: -------------------------------------------------------------------------------- 1 | '''A set of classes to support point cloud features derived from 2 | LAS/LAZ input data. 3 | ''' 4 | 5 | import os 6 | import pkg_resources 7 | import struct 8 | import sys 9 | import uuid 10 | 11 | class LASHeader: 12 | '''Data representing LAS public header block 13 | 14 | ''' 15 | def __init__(self): 16 | # In public header block order: 17 | self.file_signature = None 18 | self.file_source_id = None 19 | self.global_encoding = None 20 | self.project_guid = None 21 | self.version_major = None 22 | self.version_minor = None 23 | self.system_identifier = None 24 | self.generating_software = None 25 | self.file_creation_doy = None # (day of year) 26 | self.file_creation_year = None 27 | self.header_size = None 28 | self.offset_to_points = None 29 | self.number_of_vlr = None 30 | self.point_data_record_format = None 31 | self.point_data_record_length = None 32 | self.legacy_point_count = None 33 | self.legacy_number_of_points_by_return = None 34 | self.x_scale_factor = None 35 | self.y_scale_factor = None 36 | self.z_scale_factor = None 37 | self.x_offset = None 38 | self.y_offset = None 39 | self.z_offset = None 40 | self.max_x = None 41 | self.min_x = None 42 | self.max_y = None 43 | self.min_y = None 44 | self.max_z = None 45 | self.min_z = None 46 | 47 | # Version 1.3: 48 | self.start_of_waveform_packet_records = None 49 | 50 | # Version 1.4: 51 | self.evlr_offset = None 52 | self.evlr_length = None 53 | self.number_of_point_records = None 54 | self.number_of_points_by_return = None 55 | 56 | def __str__(self): 57 | '''Generates a string listing all of the contents 58 | 59 | Does not include values that aren't initialized 60 | ''' 61 | data_list = list() 62 | for key, value in sorted(self.__dict__.items()): 63 | if value is None: 64 | continue 65 | 66 | if key in ['global_encoding']: 67 | data_list.append(' {}: 0x{:x}'.format(key, value)) 68 | else: 69 | data_list.append(' {}: {}'.format(key, value)) 70 | data_string = '\n'.join(data_list) 71 | return '{{\n{}\n}}'.format(data_string) 72 | 73 | 74 | 75 | class LASMetadata: 76 | '''Store for metadata extracted from LAS/LAZ file 77 | 78 | ''' 79 | def __init__(self): 80 | self.header = None 81 | self.projection_wkt = None 82 | 83 | 84 | class LASParser: 85 | '''Parser for LAS/LAZ header and vlr blocks 86 | 87 | ''' 88 | def __init__(self): 89 | '''''' 90 | self._header = None # LASHeader 91 | self._projection_wkt = None 92 | 93 | def parse(self, f): 94 | '''Parses LAS/LAZ header and vlr blocks 95 | 96 | @param f: input stream object 97 | Returns new LASMetadata instance. 98 | ''' 99 | self._header = self._parse_header(f) 100 | 101 | # Update projection WKT by parsing VLR data 102 | self._projection_wkt = None 103 | self._parse_vlrs(f) 104 | 105 | # Parse instream here 106 | 107 | metadata = LASMetadata() 108 | metadata.header = self._header 109 | metadata.projection_wkt = self._projection_wkt 110 | return metadata 111 | 112 | def _parse_header(self, f): 113 | 114 | '''Reads public header block 115 | 116 | Uses brute force to unpack the contents. 117 | Sets input file pointer to the end of the header. 118 | ''' 119 | h = LASHeader() 120 | 121 | # Read up to header size field 122 | first_block_size = 96 123 | 124 | f.seek(0, 0) # make sure stream is reset 125 | blob = f.read(first_block_size) 126 | pos = 0 127 | 128 | h.file_signature = struct.unpack_from('4s', blob, pos)[0] 129 | pos += 4 130 | if h.file_signature != 'LASF'.encode(): 131 | raise Exception('Not a LAS/LAZ file. Invalid file signature.') 132 | 133 | h.file_source_id,h.global_encoding = struct.unpack_from('HH', blob, pos) 134 | pos += 4 135 | 136 | h.project_guid = uuid.UUID(bytes=blob[pos:pos+16]) 137 | pos += 16 138 | 139 | h.version_major,h.version_minor = struct.unpack_from('BB', blob, pos) 140 | pos += 2 141 | 142 | sys_id = struct.unpack_from('32s', blob, pos)[0] 143 | h.system_identifier = str(sys_id, encoding='ascii') 144 | pos += 32 145 | 146 | gen_sw = struct.unpack_from('32s', blob, pos)[0] 147 | h.generating_software = str(gen_sw, encoding='ascii') 148 | pos += 32 149 | 150 | h.file_creation_doy,h.file_creation_year = struct.unpack_from('HH', blob, pos) 151 | pos += 4 152 | 153 | h.header_size = struct.unpack_from('H', blob, pos)[0] 154 | pos += 2 155 | #print('header size {}, currently at pos {}'.format(h.header_size, pos)) 156 | 157 | 158 | # Read rest of header and reset pos 159 | next_block_size = h.header_size - first_block_size 160 | blob = f.read(next_block_size) 161 | pos = 0 162 | 163 | 164 | h.offset_to_points = struct.unpack_from('I', blob, pos)[0] 165 | pos += 4 166 | 167 | h.number_of_vlr = struct.unpack_from('I', blob, pos)[0] 168 | pos += 4 169 | 170 | h.point_data_record_format,h.point_data_record_length = struct.unpack_from('BH', blob, pos) 171 | pos += 3 172 | 173 | h.legacy_point_count = struct.unpack_from('I', blob, pos)[0] 174 | pos += 4 175 | 176 | h.legacy_number_of_points_by_return = struct.unpack_from('IIIII', blob, pos) 177 | pos += 20 178 | 179 | h.x_scale_factor,h.y_scale_factor,h.z_scale_factor = struct.unpack_from('ddd', blob, pos) 180 | pos += 24 181 | h.x_offset,h.y_offset,h.z_offset = struct.unpack_from('ddd', blob, pos) 182 | pos += 24 183 | 184 | h.max_x,h.min_x,h.max_y,h.min_y,h.max_z,h.min_z = struct.unpack_from('dddddd', blob, pos) 185 | pos += 48 186 | 187 | if h.version_major >= 1 and h.version_minor >= 3: 188 | h.start_of_waveform_packet_records = struct.unpack_from('Q', blob, pos)[0] 189 | pos += 8 190 | #print('Block1.3 pos', pos) 191 | 192 | if h.version_major >= 1 and h.version_minor >= 4: 193 | h.evlr_offset,h.evlr_length = struct.unpack_from('QI', blob, pos) 194 | pos += 12 195 | 196 | h.number_of_point_records = struct.unpack_from('Q', blob, pos)[0] 197 | pos += 8 198 | 199 | h.number_of_points_by_return = struct.unpack_from('QQQQQ', blob, pos) 200 | pos += 40 201 | 202 | #print('Final pos', pos) 203 | self._header = h 204 | return h 205 | 206 | def _parse_vlrs(self, f): 207 | '''Read variable length records looking for spatial coord system 208 | 209 | NOTE: The input file refernce (f) MUST be set to the start of the VLR records 210 | ''' 211 | # Initialize Struct to unpack VLR headers 212 | vlr_header_struct = struct.Struct('H16sHH32s') 213 | #print('vlr_header_struct.size: {}'.format(vlr_header_struct.size)) 214 | for i in range(self._header.number_of_vlr): 215 | block = f.read(vlr_header_struct.size) 216 | vlr_header_tuple = vlr_header_struct.unpack(block) 217 | #print('vlr_header_tuple {}'.format(i+1)) 218 | 219 | reserved, user_id_bytes, record_id, record_length, description_bytes = \ 220 | vlr_header_tuple 221 | # Convert text items (bytes to string) 222 | user_id = self._bytes_to_string(user_id_bytes) 223 | description = self._bytes_to_string(description_bytes) 224 | # print(' user_id: {}'.format(user_id)) 225 | # print(' record_id: {}'.format(record_id)) 226 | # print(' record_length: {}'.format(record_length)) 227 | # print(' descripton: {}'.format(description)) 228 | 229 | # Read record 2112 to get coord system wkt 230 | if user_id == 'LASF_Projection' and record_id == 2112: 231 | payload = f.read(record_length) 232 | self._projection_wkt = self._bytes_to_string(payload) 233 | # print('coord_sys wkt:') 234 | # print(self._projection_wkt) 235 | # print() 236 | 237 | # Otherwise skip over 238 | else: 239 | f.seek(record_length, 1) 240 | # payload = f.read(record_length) 241 | 242 | def _bytes_to_string(self, input_bytes, rstrip=True, encoding='ascii'): 243 | '''Convert input bytes to ascii string 244 | 245 | @param: rstrip (boolean) if true, remove terminating null bytes 246 | ''' 247 | if rstrip: 248 | # Remove trailing zeros (https://stackoverflow.com/a/5076070) 249 | input_bytes = input_bytes.split(b'\0',1)[0] 250 | output_string = str(input_bytes, encoding=encoding) 251 | return output_string 252 | 253 | 254 | # Transcribe point attribute names for point data record formats, 255 | # as of July 2018, LAS version 1.4, per https://www.asprs.org 256 | # Transcribed by hand, so user beware 257 | LASPointAttributes = dict() 258 | LASPointAttributes[0] = ( 259 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 260 | 'Number of Returns (given pulse)', 'Scan Direction Flag', 261 | 'Edge of Flight Line', 'Classification', 262 | 'Scan Angle Rank (-90 to +90) – Left side', 'User Data' 263 | ) 264 | LASPointAttributes[1] = ( 265 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 266 | 'Number of Returns (given pulse)', 'Scan Direction Flag', 267 | 'Edge of Flight Line', 'Classification', 268 | 'Scan Angle Rank (-90 to +90) – Left side', 'User Data', 269 | 'Point Source ID', 'GPS Time' 270 | ) 271 | 272 | LASPointAttributes[2] = ( 273 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 274 | 'Number of Returns (given pulse)', 'Scan Direction Flag', 275 | 'Edge of Flight Line', 'Classification', 276 | 'Scan Angle Rank (-90 to +90) – Left side', 'User Data', 277 | 'Point Source ID', 'Red', 'Green', 'Blue' 278 | ) 279 | LASPointAttributes[3] = ( 280 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 281 | 'Number of Returns (given pulse)', 'Scan Direction Flag', 282 | 'Edge of Flight Line', 'Classification', 283 | 'Scan Angle Rank (-90 to +90) – Left side', 'User Data', 284 | 'Point Source ID', 'GPS Time', 'Red', 'Green', 'Blue' 285 | ) 286 | LASPointAttributes[4] = ( 287 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 288 | 'Number of Returns (given pulse)', 'Scan Direction Flag', 289 | 'Edge of Flight Line', 'Classification', 290 | 'Scan Angle Rank (-90 to +90) – Left side', 'User Data', 291 | 'Point Source ID', 'GPS Time', 'Wave Packet Descriptor Index', 292 | 'Byte offset to waveform data', 'Return Point Waveform Location', 293 | 'X(t)', 'Y(t)', 'Z(t)' 294 | ) 295 | LASPointAttributes[5] = ( 296 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 297 | 'Number of Returns (given pulse)', 'Scan Direction Flag', 298 | 'Edge of Flight Line', 'Classification', 299 | 'Scan Angle Rank (-90 to +90) – Left side', 'User Data', 300 | 'Point Source ID', 'GPS Time', 'Red', 'Green', 'Blue', 301 | 'Wave Packet Descriptor Index', 'Byte offset to waveform data', 302 | 'Return Point Waveform Location', 'X(t)', 'Y(t)', 'Z(t)' 303 | ) 304 | LASPointAttributes[6] = ( 305 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 306 | 'Number of Returns (given pulse)', 'Classification Flags', 307 | 'Scanner Channel', 'Scan Direction Flag', 'Edge of Flight Line', 308 | 'Classification', 'User Data', 'Scan Angle', 'Point Source ID', 309 | 'GPS Time' 310 | ) 311 | LASPointAttributes[7] = ( 312 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 313 | 'Number of Returns (given pulse)', 'Classification Flags', 314 | 'Scanner Channel', 'Scan Direction Flag', 'Edge of Flight Line', 315 | 'Classification', 'User Data', 'Scan Angle', 'Point Source ID', 316 | 'GPS Time', 'Red', 'Green', 'Blue' 317 | ) 318 | LASPointAttributes[8] = ( 319 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 320 | 'Number of Returns (given pulse)', 'Classification Flags', 321 | 'Scanner Channel', 'Scan Direction Flag', 'Edge of Flight Line', 322 | 'Classification', 'User Data', 'Scan Angle', 'Point Source ID', 323 | 'GPS Time', 'Red', 'Green', 'Blue', 'NIR' 324 | ) 325 | LASPointAttributes[9] = ( 326 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 327 | 'Number of Returns (given pulse)', 'Classification Flags', 328 | 'Scanner Channel', 'Scan Direction Flag', 'Edge of Flight Line', 329 | 'Classification', 'User Data', 'Scan Angle', 'Point Source ID', 330 | 'GPS Time', 'Wave Packet Descriptor Index', 331 | 'Byte offset to waveform data', 'Return Point Waveform Location', 332 | 'X(t)', 'Y(t)', 'Z(t)' 333 | ) 334 | LASPointAttributes[10] = ( 335 | 'X', 'Y', 'Z', 'Intensity', 'Return Number', 336 | 'Number of Returns (given pulse)', 'Classification Flags', 337 | 'Scanner Channel', 'Scan Direction Flag', 'Edge of Flight Line', 338 | 'Classification', 'User Data', 'Scan Angle', 'Point Source ID', 339 | 'GPS Time', 'Red', 'Green', 'Blue', 'NIR', 340 | 'Wave Packet Descriptor Index', 'Byte offset to waveform data', 341 | 'Return Point Waveform Location', 'X(t)', 'Y(t)', 'Z(t)' 342 | ) 343 | -------------------------------------------------------------------------------- /src/geojsbuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Class for generating geomap based on geojs model received from Jupyter kernel. 3 | */ 4 | 5 | import { JSONObject } from '@phosphor/coreutils'; 6 | 7 | 8 | // Local interface definitions - do these need to be exported? 9 | export interface IFeatureModel { 10 | data?: any; 11 | featureType: string; 12 | options?: JSONObject; 13 | url?: string; 14 | } 15 | 16 | export interface ILayerModel { 17 | features?: IFeatureModel[]; 18 | layerType: string; 19 | options?: JSONObject; 20 | } 21 | 22 | export interface IMapModel { 23 | layers?: ILayerModel[]; 24 | options?: JSONObject; 25 | viewpoint?: JSONObject; 26 | } 27 | 28 | interface IStringMap { 29 | [key: string] : any; 30 | } 31 | 32 | 33 | import { ColorFormat, ColorMap } from 'colorkit'; 34 | import * as geo from 'geojs' 35 | console.debug(`Using geojs ${geo.version}`); 36 | 37 | // Static var used to disable OSM layer renderer; 38 | // For testing, so that we don't need to mock html canvas 39 | let _disableOSMRenderer: boolean = false; 40 | 41 | 42 | class GeoJSBuilder { 43 | // The GeoJS instance 44 | private _geoMap: any; 45 | 46 | // Hard code UI layer and tooltip logic 47 | private _tooltipLayer: any; 48 | private _tooltip: any; 49 | private _tooltipElem: HTMLElement; 50 | private _preElem: HTMLPreElement; 51 | 52 | private _colorMap: ColorMap; 53 | 54 | // Promise list used when building geojs map 55 | private _promiseList: Promise[]; 56 | 57 | constructor() { 58 | this._geoMap = null; 59 | this._promiseList = null; // for loading data 60 | 61 | this._tooltipLayer = null; 62 | this._tooltip = null; 63 | this._tooltipElem = null; 64 | this._preElem = null; 65 | 66 | this._colorMap = null; 67 | 68 | // let colormap = new ColorMap('rainbow'); 69 | // for (let i=0; i<=10; ++i) { 70 | // let x:number = 0.1 * i; 71 | // let hex: string = colormap.interpolateColor(x, ColorFormat.HEX); 72 | // console.log(`${i}. ${x.toFixed(1)} => ${hex}`); 73 | // } 74 | } 75 | 76 | // Sets static var 77 | static disableOSMRenderer(state: boolean): void { 78 | _disableOSMRenderer = state; 79 | } 80 | 81 | // Clears the current geo.map instance 82 | clear(): void { 83 | if (!!this._geoMap) { 84 | this._geoMap.exit(); 85 | this._geoMap = null; 86 | } 87 | } 88 | 89 | // Returns PROMISE that resolves to geo.map instance 90 | // Note that caller is responsible for disposing the geo.map 91 | generate(node: HTMLElement, model: IMapModel={}): Promise { 92 | console.log('GeoJSBuilder.generate() input model:') 93 | console.dir(model); 94 | if (!!this._geoMap) { 95 | console.warn('Deleting existing GeoJS instance'); 96 | this.clear() 97 | } 98 | 99 | let options: JSONObject = model.options || {}; 100 | // Add dom node to the map options 101 | const mapOptions = Object.assign(options, {node: node}); 102 | this._geoMap = geo.map(mapOptions); 103 | 104 | this.update(model); 105 | const viewpoint: JSONObject = model.viewpoint; 106 | if (viewpoint) { 107 | switch (viewpoint.mode) { 108 | case 'bounds': 109 | // console.log('Input viewpoint bounds:'); 110 | // console.dir(viewpoint.bounds); 111 | let spec = this._geoMap.zoomAndCenterFromBounds(viewpoint.bounds); 112 | // console.log('Computed viewpoint spec:') 113 | // console.dir(spec); 114 | this._geoMap.center(spec.center); 115 | this._geoMap.zoom(spec.zoom); 116 | break; 117 | 118 | default: 119 | console.warn(`Unrecognized viewpoint object ${model.viewpoint}`); 120 | console.dir(model.viewpoint); 121 | } 122 | } 123 | 124 | // Return promise that resolves to this._geoMap 125 | return new Promise((resolve, reject) => { 126 | Promise.all(this._promiseList) 127 | .then(() => resolve(this._geoMap), reject => console.error(reject)) 128 | .catch(error => reject(error)); 129 | }); 130 | } // generate() 131 | 132 | // Generates geomap layers 133 | // Note: Internal logic can push promise instances onto this._promiseList 134 | update(model: IMapModel={}): void { 135 | this._promiseList = []; 136 | let layerModels: ILayerModel[] = model.layers || []; 137 | for (let layerModel of layerModels) { 138 | let options: JSONObject = layerModel.options || {}; 139 | let layerType: string = layerModel.layerType; 140 | //console.log(`layerType: ${layerType}`); 141 | if (_disableOSMRenderer && layerType == 'osm') { 142 | options.renderer = null; 143 | } 144 | let layer: any = this._geoMap.createLayer(layerType, options); 145 | //console.log(`Renderer is ${layer.rendererName()}`) 146 | if (layerModel.features) { 147 | this._createFeatures(layer, layerModel.features) 148 | } 149 | } // for (layerModel) 150 | } // update() 151 | 152 | 153 | // Creates features 154 | _createFeatures(layer: any, featureModels: IFeatureModel[]): void { 155 | for (let featureModel of featureModels) { 156 | //console.dir(featureModel); 157 | switch(featureModel.featureType) { 158 | case 'geojson': 159 | this._createGeoJSONFeature(layer, featureModel); 160 | break; 161 | 162 | case 'point': 163 | case 'quad': 164 | let feature: any = layer.createFeature(featureModel.featureType); 165 | let options: JSONObject = featureModel.options || {}; 166 | if (options.data) { 167 | if (featureModel.featureType === 'quad') { 168 | // Copy the options.data array, because geojs might 169 | // attach a cached texture object, which won't serialze 170 | // when saving the associated notebook. 171 | let dataCopy: Array = 172 | (options.data as Array).map(obj => ({...obj})); 173 | //console.log(`Same? ${dataCopy[0] == (options.data as Array)[0]}`); 174 | feature.data(dataCopy); 175 | } 176 | else { 177 | feature.data(options.data); 178 | } 179 | } // if (options.data) 180 | 181 | // If position array included, set position method 182 | if (options.position) { 183 | feature.position((dataItem: any, dataIndex: number) => { 184 | //console.debug(`dataIndex ${dataIndex}`); 185 | 186 | // Workaround undiagnosed problem where dataIndex 187 | // is sometimes undefined. It appears to be realted 188 | // to mousemove events. 189 | if (dataIndex === undefined) { 190 | // Check for Kitware workaround 191 | if ('__i' in dataItem) { 192 | //console.debug('dataItem is undefined'); 193 | dataIndex = dataItem.__i; 194 | } 195 | else { 196 | throw Error('dataIndex is undefined ') 197 | } 198 | } // if 199 | let positions: any = options.position; 200 | let position: any = positions[dataIndex]; 201 | // console.debug(`Position ${position}`); 202 | return position; 203 | }); 204 | } 205 | 206 | // Other options that are simple properties 207 | const properties = ['bin', 'gcs', 'selectionAPI', 'visible'] 208 | for (let property of properties) { 209 | if (options[property]) { 210 | feature[property](options[property]); 211 | } 212 | } 213 | 214 | // Handle style separately, since its components can be constant or array 215 | const styleProperties = options.style as IStringMap || {}; 216 | let useStyle: IStringMap = {}; 217 | for (let key in styleProperties) { 218 | let val: any = styleProperties[key] 219 | if (Array.isArray(val)) { 220 | useStyle[key] = function(d: IStringMap): any { 221 | let index = d.__i as number; 222 | return val[index]; 223 | } 224 | } 225 | else { 226 | useStyle[key] = val; 227 | } 228 | } 229 | feature['style'](useStyle); 230 | 231 | // Events - note that we must explicitly bind to "this" 232 | if (options.enableTooltip) { 233 | // Add hover/tooltip - only click seems to work 234 | this._enableTooltipDisplay(); 235 | feature.selectionAPI(true); 236 | feature.geoOn(geo.event.feature.mouseon, function(evt: any) { 237 | // console.debug('feature.mouseon'); 238 | // console.dir(evt); 239 | this._tooltip.position(evt.mouse.geo); 240 | 241 | // Work from a copy of the event data 242 | let userData: any = Object.assign({}, evt.data); 243 | delete userData.__i; 244 | let jsData:string = JSON.stringify( 245 | userData, Object.keys(userData).sort(), 2); 246 | // Strip off first and last lines (curly braces) 247 | let lines: string[] = jsData.split('\n'); 248 | let innerLines: string[] = lines.slice(1, lines.length-1); 249 | this._preElem.innerHTML = innerLines.join('\n'); 250 | this._tooltipElem.classList.remove('hidden'); 251 | }.bind(this)); 252 | feature.geoOn(geo.event.feature.mouseoff, function(evt: any) { 253 | //console.debug('featuremouseoff'); 254 | this._preElem.textContent = ''; 255 | this._tooltipElem.classList.add('hidden'); 256 | }.bind(this)); 257 | 258 | // feature.geoOn(geo.event.mouseclick, function(evt: any) { 259 | // console.log('plain mouseclick, evt:'); 260 | // console.dir(evt); 261 | // // this._tooltip.position(evt.geo); 262 | // // this._tooltipElem.textContent = 'hello'; 263 | // // this._tooltipElem.classList.remove('hidden'); 264 | // }.bind(this)); 265 | 266 | //.geoOn(geo.event.zoom, resimplifyOnZoom); 267 | } // if (options.data) 268 | 269 | if (options.colormap) { 270 | console.log('Using colormap'); 271 | if (!this._colorMap) { 272 | this._colorMap = new ColorMap(); 273 | } 274 | let colorOptions = options.colormap as JSONObject; 275 | let field = colorOptions.field as string; 276 | if (!field) { 277 | throw Error('colormap specified without field item'); 278 | } 279 | if ('colorseries' in colorOptions) { 280 | this._colorMap.useColorSeries(colorOptions.colorseries as string); 281 | } 282 | if ('range' in colorOptions) { 283 | this._colorMap.setInputRange(colorOptions.range as number[]); 284 | } 285 | // Setup fillColor function 286 | feature.style({ 287 | fillColor: function(dataItem: any): string { 288 | // console.log(`fillColor with dataItem:`); 289 | // console.log(dataItem); 290 | // console.log(`field: ${field}`); 291 | //return '#993399'; 292 | let val = dataItem[field] as number; 293 | // console.log(`input value ${val}`) 294 | if (val) { 295 | let color: string= this._colorMap.interpolateColor(val, ColorFormat.HEX); 296 | // console.log(`color \"${color}\"`) 297 | return color; 298 | } 299 | // (else) 300 | return 'red'; 301 | }.bind(this) // fillColor 302 | }); // feature.style() 303 | 304 | } // if (options.color) 305 | break; 306 | 307 | default: 308 | throw `Unrecognized feature type ${featureModel.featureType}`; 309 | } // switch 310 | } 311 | } // _createFeatures() 312 | 313 | // Generates GeoJSON feature from feature model 314 | _createGeoJSONFeature(layer: any, featureModel: IFeatureModel): void { 315 | if (featureModel.data) { 316 | let p: Promise = this._loadGeoJSONObject(layer, featureModel.data); 317 | this._promiseList.push(p); 318 | } 319 | if (featureModel.url) { 320 | let p: Promise = this._downloadGeoJSONFile(layer, featureModel.url); 321 | this._promiseList.push(p); 322 | } 323 | } 324 | 325 | // Loads GeoJSON object 326 | _loadGeoJSONObject(layer:any, data: any): Promise { 327 | // console.dir(layer); 328 | // console.dir(data); 329 | 330 | let reader: any = geo.createFileReader('jsonReader', {layer: layer}); 331 | return new Promise((resolve, reject) => { 332 | try { 333 | reader.read(data, resolve); 334 | } 335 | catch (error) { 336 | console.error(error); 337 | reject(error); 338 | } 339 | }) // new Promise() 340 | } // loadGeoJSONData() 341 | 342 | _downloadGeoJSONFile(layer: any, url: string): Promise { 343 | //console.log(`_downloadGeoJSONFile: ${url}`); 344 | return new Promise(function(resolve, reject) { 345 | fetch(url) 346 | .then(response => response.text()) 347 | .then(text => { 348 | let reader = geo.createFileReader('jsonReader', {layer: layer}); 349 | reader.read(text, resolve); 350 | }) 351 | .catch(error => { 352 | console.error(error); 353 | reject(error) 354 | }) 355 | }) // new Promise() 356 | } // _downloadGeoJSONFile() 357 | 358 | // Initializes UI layer for tooltip display 359 | _enableTooltipDisplay(): void { 360 | if (this._tooltipLayer) { 361 | return; 362 | } 363 | 364 | // Initialize UI layer and tooltip 365 | console.log('Adding tooltip layer'); 366 | this._tooltipLayer = this._geoMap.createLayer('ui', {zIndex: 9999}); 367 | this._tooltip = this._tooltipLayer.createWidget('dom', {position: {x: 0, y:0}}); 368 | this._tooltipElem = this._tooltip.canvas(); 369 | //this._tooltipElem.id = 'tooltip'; 370 | this._tooltipElem.classList.add('jp-TooltipGeoJS', 'hidden'); 371 | this._preElem = document.createElement('pre'); 372 | this._tooltipElem.appendChild(this._preElem); 373 | } 374 | 375 | } // GeoJSBuilder 376 | 377 | export { GeoJSBuilder } 378 | -------------------------------------------------------------------------------- /src/pointcloud/laslaz.js: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2014 Uday Verma, uday.karan@gmail.com 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | /** 24 | * Point format spec - currently reading 0, 1, 2, 3 25 | */ 26 | var pointFormatReaders = { 27 | 0: function(dv) { 28 | return { 29 | "position": [ dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], 30 | "intensity": dv.getUint16(12, true), 31 | "classification": dv.getUint8(15, true) 32 | }; 33 | }, 34 | 1: function(dv) { 35 | return { 36 | "position": [ dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], 37 | "intensity": dv.getUint16(12, true), 38 | "classification": dv.getUint8(15, true) 39 | }; 40 | }, 41 | 2: function(dv) { 42 | return { 43 | "position": [ dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], 44 | "intensity": dv.getUint16(12, true), 45 | "classification": dv.getUint8(15, true), 46 | "color": [dv.getUint16(20, true), dv.getUint16(22, true), dv.getUint16(24, true)] 47 | }; 48 | }, 49 | 3: function(dv) { 50 | return { 51 | "position": [ dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], 52 | "intensity": dv.getUint16(12, true), 53 | "classification": dv.getUint8(15, true), 54 | "color": [dv.getUint16(28, true), dv.getUint16(30, true), dv.getUint16(32, true)] 55 | }; 56 | } 57 | }; 58 | 59 | /** 60 | * 61 | * @param {*} buf 62 | * @param {*} Type 63 | * @param {*} offset 64 | * @param {*} count 65 | */ 66 | function readAs(buf, Type, offset, count) { 67 | count = (count === undefined || count === 0 ? 1 : count); 68 | var sub = buf.slice(offset, offset + Type.BYTES_PER_ELEMENT * count); 69 | 70 | var r = new Type(sub); 71 | if (count === undefined || count === 1) 72 | return r[0]; 73 | 74 | var ret = []; 75 | for (var i = 0 ; i < count ; i ++) { 76 | ret.push(r[i]); 77 | } 78 | 79 | return ret; 80 | } 81 | 82 | /** 83 | * Parse LAS header 84 | * 85 | * @param {*} arraybuffer 86 | */ 87 | function parseLASHeader(arraybuffer) { 88 | var _this = {}; 89 | 90 | _this.pointsOffset = readAs(arraybuffer, Uint32Array, 32*3); 91 | _this.pointsFormatId = readAs(arraybuffer, Uint8Array, 32*3+8); 92 | _this.pointsStructSize = readAs(arraybuffer, Uint16Array, 32*3+8+1); 93 | _this.pointsCount = readAs(arraybuffer, Uint32Array, 32*3 + 11); 94 | 95 | var start = 32*3 + 35; 96 | _this.scale = readAs(arraybuffer, Float64Array, start, 3); start += 24; // 8*3 97 | _this.offset = readAs(arraybuffer, Float64Array, start, 3); start += 24; 98 | 99 | var bounds = readAs(arraybuffer, Float64Array, start, 6); start += 48; // 8*6; 100 | _this.maxs = [bounds[0], bounds[2], bounds[4]]; 101 | _this.mins = [bounds[1], bounds[3], bounds[5]]; 102 | 103 | return _this; 104 | } 105 | 106 | /** 107 | * LAS Reader 108 | * @param {*} arraybuffer 109 | */ 110 | var LASLoader = function(arraybuffer) { 111 | this.arraybuffer = arraybuffer; 112 | }; 113 | 114 | /** 115 | * Open the file 116 | */ 117 | LASLoader.prototype.open = function() { 118 | this.readOffset = 0; 119 | return new Promise(function(res, rej) { 120 | setTimeout(res, 0); 121 | }); 122 | }; 123 | 124 | /** 125 | * Get header information 126 | */ 127 | LASLoader.prototype.getHeader = function() { 128 | var _this = this; 129 | 130 | return new Promise(function(res, rej) { 131 | setTimeout(function() { 132 | _this.header = parseLASHeader(_this.arraybuffer); 133 | res(_this.header); 134 | }, 0); 135 | }); 136 | }; 137 | 138 | /** 139 | * Read point data in mini-batch mode 140 | */ 141 | LASLoader.prototype.readData = function(count, offset, skip) { 142 | var _this = this; 143 | 144 | return new Promise(function(res, rej) { 145 | setTimeout(function() { 146 | if (!_this.header) 147 | return rej(new Error("Cannot start reading data till a header request is issued")); 148 | 149 | var start; 150 | if (skip <= 1) { 151 | count = Math.min(count, _this.header.pointsCount - _this.readOffset); 152 | start = _this.header.pointsOffset + _this.readOffset * _this.header.pointsStructSize; 153 | var end = start + count * _this.header.pointsStructSize; 154 | res({ 155 | buffer: _this.arraybuffer.slice(start, end), 156 | count: count, 157 | hasMoreData: _this.readOffset + count < _this.header.pointsCount}); 158 | _this.readOffset += count; 159 | } 160 | else { 161 | var pointsToRead = Math.min(count * skip, _this.header.pointsCount - _this.readOffset); 162 | var bufferSize = Math.ceil(pointsToRead / skip); 163 | var pointsRead = 0; 164 | 165 | var buf = new Uint8Array(bufferSize * _this.header.pointsStructSize); 166 | for (var i = 0 ; i < pointsToRead ; i ++) { 167 | if (i % skip === 0) { 168 | start = _this.header.pointsOffset + _this.readOffset * _this.header.pointsStructSize; 169 | var src = new Uint8Array(_this.arraybuffer, start, _this.header.pointsStructSize); 170 | 171 | buf.set(src, pointsRead * _this.header.pointsStructSize); 172 | pointsRead ++; 173 | } 174 | 175 | _this.readOffset ++; 176 | } 177 | 178 | res({ 179 | buffer: buf.buffer, 180 | count: pointsRead, 181 | hasMoreData: _this.readOffset < _this.header.pointsCount 182 | }); 183 | } 184 | }, 0); 185 | }); 186 | }; 187 | 188 | /** 189 | * Close the reader 190 | */ 191 | LASLoader.prototype.close = function() { 192 | var _this = this; 193 | return new Promise(function(res, rej) { 194 | _this.arraybuffer = null; 195 | setTimeout(res, 0); 196 | }); 197 | }; 198 | 199 | // LAZ Loader 200 | // Uses NaCL module to load LAZ files 201 | // 202 | var LAZLoader = function(arraybuffer) { 203 | this.arraybuffer = arraybuffer; 204 | this.ww = new Worker("workers/laz-loader-worker.js"); 205 | 206 | this.nextCB = null; 207 | var _this = this; 208 | 209 | this.ww.onmessage = function(e) { 210 | if (_this.nextCB !== null) { 211 | console.log('dorr: >>', e.data); 212 | _this.nextCB(e.data); 213 | _this.nextCB = null; 214 | } 215 | }; 216 | 217 | this.dorr = function(req, cb) { 218 | console.log('dorr: <<', req); 219 | _this.nextCB = cb; 220 | _this.ww.postMessage(req); 221 | }; 222 | }; 223 | 224 | LAZLoader.prototype.open = function() { 225 | 226 | // nothing needs to be done to open this file 227 | // 228 | var _this = this; 229 | return new Promise(function(res, rej) { 230 | _this.dorr({type:"open", arraybuffer: _this.arraybuffer}, function(r) { 231 | if (r.status !== 1) 232 | return rej(new Error("Failed to open file")); 233 | 234 | res(true); 235 | }); 236 | }); 237 | }; 238 | 239 | /** 240 | * 241 | */ 242 | LAZLoader.prototype.getHeader = function() { 243 | var _this = this; 244 | 245 | return new Promise(function(res, rej) { 246 | _this.dorr({type:'header'}, function(r) { 247 | if (r.status !== 1) 248 | return rej(new Error("Failed to get header")); 249 | 250 | res(r.header); 251 | }); 252 | }); 253 | }; 254 | 255 | /** 256 | * 257 | */ 258 | LAZLoader.prototype.readData = function(count, offset, skip) { 259 | var _this = this; 260 | 261 | return new Promise(function(res, rej) { 262 | _this.dorr({type:'read', count: count, offset: offset, skip: skip}, function(r) { 263 | if (r.status !== 1) 264 | return rej(new Error("Failed to read data")); 265 | res({ 266 | buffer: r.buffer, 267 | count: r.count, 268 | hasMoreData: r.hasMoreData 269 | }); 270 | }); 271 | }); 272 | }; 273 | 274 | /** 275 | * 276 | */ 277 | LAZLoader.prototype.close = function() { 278 | var _this = this; 279 | 280 | return new Promise(function(res, rej) { 281 | _this.dorr({type:'close'}, function(r) { 282 | if (r.status !== 1) 283 | return rej(new Error("Failed to close file")); 284 | 285 | res(true); 286 | }); 287 | }); 288 | }; 289 | 290 | /** 291 | * A single consistent interface for loading LAS/LAZ files 292 | */ 293 | var LASFile = function(arraybuffer) { 294 | this.arraybuffer = arraybuffer; 295 | 296 | this.determineVersion(); 297 | if (this.version > 13) 298 | throw new Error("Only file versions <= 1.3 are supported at this time"); 299 | 300 | this.determineFormat(); 301 | if (pointFormatReaders[this.formatId] === undefined) 302 | throw new Error("The point format ID is not supported"); 303 | 304 | this.loader = this.isCompressed ? 305 | new LAZLoader(this.arraybuffer) : 306 | new LASLoader(this.arraybuffer); 307 | }; 308 | 309 | /** 310 | * 311 | */ 312 | LASFile.prototype.determineFormat = function() { 313 | var formatId = readAs(this.arraybuffer, Uint8Array, 32*3+8); 314 | var bit_7 = (formatId & 0x80) >> 7; 315 | var bit_6 = (formatId & 0x40) >> 6; 316 | 317 | if (bit_7 === 1 && bit_6 === 1) 318 | throw new Error("Old style compression not supported"); 319 | 320 | this.formatId = formatId & 0x3f; 321 | this.isCompressed = (bit_7 === 1 || bit_6 === 1); 322 | }; 323 | 324 | /** 325 | * 326 | */ 327 | LASFile.prototype.determineVersion = function() { 328 | var ver = new Int8Array(this.arraybuffer, 24, 2); 329 | this.version = ver[0] * 10 + ver[1]; 330 | this.versionAsString = ver[0] + "." + ver[1]; 331 | }; 332 | 333 | /** 334 | * 335 | */ 336 | LASFile.prototype.open = function() { 337 | return this.loader.open(); 338 | }; 339 | 340 | /** 341 | * 342 | */ 343 | LASFile.prototype.getHeader = function() { 344 | return this.loader.getHeader(); 345 | }; 346 | 347 | /** 348 | * 349 | */ 350 | LASFile.prototype.readData = function(count, start, skip) { 351 | return this.loader.readData(count, start, skip); 352 | }; 353 | 354 | LASFile.prototype.close = function() { 355 | return this.loader.close(); 356 | }; 357 | 358 | /** 359 | * Decodes LAS records into points 360 | * 361 | * @param {*} buffer 362 | * @param {*} len 363 | * @param {*} header 364 | */ 365 | var LASDecoder = function(buffer, len, header) { 366 | //console.log(header); 367 | //console.log("POINT FORMAT ID:", header.pointsFormatId); 368 | this.arrayb = buffer; 369 | this.decoder = pointFormatReaders[header.pointsFormatId]; 370 | this.pointsCount = len; 371 | this.pointSize = header.pointsStructSize; 372 | this.scale = header.scale; 373 | this.offset = header.offset; 374 | this.mins = header.mins; 375 | this.maxs = header.maxs; 376 | }; 377 | 378 | /** 379 | * 380 | */ 381 | LASDecoder.prototype.getPoint = function(index) { 382 | if (index < 0 || index >= this.pointsCount) 383 | throw new Error("Point index out of range"); 384 | 385 | var dv = new DataView(this.arrayb, index * this.pointSize, this.pointSize); 386 | return this.decoder(dv); 387 | }; 388 | 389 | // NACL Module support 390 | // Called by the common.js module. 391 | // 392 | // window.startNaCl = function(name, tc, config, width, height) { 393 | // // check browser support for nacl 394 | // // 395 | // if(!common.browserSupportsNaCl()) { 396 | // return $.event.trigger({ 397 | // type: "plasi_this.nacl.error", 398 | // message: "NaCl support is not available" 399 | // }); 400 | // } 401 | // console.log("Requesting persistent memory"); 402 | 403 | // navigator.webkitPersistentStorage.requestQuota(2048 * 2048, function(bytes) { 404 | // common.updateStatus( 405 | // 'Allocated ' + bytes + ' bytes of persistant storage.'); 406 | // common.attachDefaultListeners(); 407 | // common.createNaClModule(name, tc, config, width, height); 408 | // }, 409 | // function(e) { 410 | // console.log("Failed!"); 411 | // $.event.trigger({ 412 | // type: "plasi_this.nacl.error", 413 | // message: "Could not allocate persistant storage" 414 | // }); 415 | // }); 416 | 417 | // $(document).on("plasi_this.nacl.available", function() { 418 | // scope.LASModuleWasLoaded = true; 419 | // console.log("NACL Available"); 420 | // }); 421 | // }; 422 | 423 | /** 424 | * 425 | */ 426 | LASFile.prototype.getUnpacker = function() { 427 | return LASDecoder; 428 | }; 429 | 430 | // 431 | 432 | 433 | /** 434 | * 435 | * @param {*} url 436 | * @param {*} cb 437 | */ 438 | var getBinary = function(url, cb) { 439 | var oReq = new XMLHttpRequest(); 440 | return new Promise(function(resolve, reject) { 441 | oReq.open("GET", url, true); 442 | oReq.responseType = "arraybuffer"; 443 | 444 | oReq.onload = function(oEvent) { 445 | if (oReq.status == 200) { 446 | console.log(oReq.getAllResponseHeaders()); 447 | return resolve(new LASFile(oReq.response)); 448 | } 449 | reject(new Error("Could not get binary data")); 450 | }; 451 | 452 | oReq.onerror = function(err) { 453 | reject(err); 454 | }; 455 | 456 | oReq.send(); 457 | }); 458 | }; 459 | 460 | /** 461 | * 462 | * @param {*} file 463 | * @param {*} cb 464 | */ 465 | var getBinaryLocal = function(file, cb) { 466 | var fr = new FileReader(); 467 | var p = Promise.defer(); 468 | 469 | fr.onprogress = function(e) { 470 | cb(e.loaded / e.total, e.loaded); 471 | }; 472 | fr.onload = function(e) { 473 | p.resolve(new LASFile(e.target.result)); 474 | }; 475 | 476 | fr.readAsArrayBuffer(file); 477 | 478 | return p.promise.cancellable().catch(Promise.CancellationError, function(e) { 479 | fr.abort(); 480 | throw e; 481 | }); 482 | }; 483 | 484 | 485 | export { LASFile }; 486 | --------------------------------------------------------------------------------