├── .gitignore ├── .eslintignore ├── .babelrc ├── app ├── Keys.js ├── Redux │ ├── Store.js │ ├── Action.js │ └── Reducer.js ├── Components │ ├── TileExporter │ │ ├── TileConfiguration.js │ │ ├── PreviewMap.js │ │ ├── MapSpells.js │ │ ├── QueryChecker.js │ │ ├── DomHelper.js │ │ ├── BasicScene.js │ │ ├── PreviewUnit.js │ │ └── Exporter.js │ └── Search │ │ ├── ResultRow.jsx │ │ ├── ResultTable.jsx │ │ └── SearchBox.jsx ├── App.jsx ├── scss │ ├── preview.scss │ ├── searchbar.scss │ ├── main.scss │ └── resources │ │ └── icons.svg └── libs │ ├── OBJ-Exporter.js │ ├── Triangulation.js │ ├── OrbitControl.js │ └── D3-Three.js ├── .eslintrc.js ├── server.js ├── README.md ├── webpack.config.js ├── package.json ├── index.html └── src └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | bundle.js.map -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | app/libs/* 2 | app/scss/* 3 | app/Components/TileExporter/MapSpells.js -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins": ["transform-object-rest-spread"] 7 | } 8 | -------------------------------------------------------------------------------- /app/Keys.js: -------------------------------------------------------------------------------- 1 | const Keys = { 2 | search: 'ge-5c11caa6fac22390', 3 | vectorTile: 'Arui5lbFQL6Q7hG3rU9XQQ' 4 | }; 5 | 6 | module.exports = Keys; 7 | -------------------------------------------------------------------------------- /app/Redux/Store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import reducer from './Reducer'; 3 | 4 | const store = createStore(reducer); 5 | 6 | export default store; 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "plugins": ["react", "import"], 7 | "extends": ["airbnb", "plugin:import/errors", "plugin:import/warnings"], 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "comma-dangle": ["error", "never"], 16 | "no-underscore-dangle": 0 17 | } 18 | }; -------------------------------------------------------------------------------- /app/Components/TileExporter/TileConfiguration.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // These are all features from Mapzen Vector tile all layers 3 | water: { 4 | height: 5 5 | }, 6 | buildings: { 7 | height: 11 8 | }, 9 | places: { 10 | height: 0 11 | }, 12 | transit: { 13 | height: 0 14 | }, 15 | pois: { 16 | height: 0 17 | }, 18 | boundaries: { 19 | height: 7 20 | }, 21 | roads: { 22 | height: 8 23 | }, 24 | earth: { 25 | height: 6 26 | }, 27 | landuse: { 28 | height: 7 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /app/Components/Search/ResultRow.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | function ResultRow(props) { 4 | const { data, dataIndex, rowIndex, pointAction } = props; 5 | return ( 6 |
  • pointAction(data)} 10 | > 11 | {data.properties.label} 12 |
  • 13 | ); 14 | } 15 | 16 | ResultRow.propTypes = { 17 | data: PropTypes.object, // eslint-disable-line 18 | dataIndex: PropTypes.number, 19 | rowIndex: PropTypes.number, 20 | pointAction: PropTypes.func 21 | }; 22 | 23 | export default ResultRow; 24 | -------------------------------------------------------------------------------- /app/Components/Search/ResultTable.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import ResultRow from './ResultRow'; 3 | 4 | function ResultTable(props) { 5 | const listItem = props.searchData.map((searchResult, index) => 6 | 13 | ); 14 | 15 | return ( 16 | ); 19 | } 20 | 21 | ResultTable.propTypes = { 22 | searchData: PropTypes.array, // eslint-disable-line 23 | dataIndex: PropTypes.number, // eslint-disable-line 24 | pointAction: PropTypes.func 25 | }; 26 | 27 | export default ResultTable; 28 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import webpack from 'webpack'; 3 | import config from './webpack.config'; 4 | import path from 'path'; 5 | 6 | import webpackDevMiddleware from 'webpack-dev-middleware'; 7 | import webpackHotMiddleware from 'webpack-hot-middleware'; 8 | 9 | const app = express(); 10 | const compiler = webpack(config); 11 | 12 | app.use(webpackDevMiddleware(compiler, { 13 | noInfo: true, 14 | publicPath: config.output.publicPath 15 | })); 16 | 17 | app.use(webpackHotMiddleware(compiler)); 18 | 19 | app.get('*', (req, res) => { 20 | res.sendFile(path.join(__dirname, 'index.html')); 21 | }); 22 | 23 | app.listen(3000, 'localhost', error => { 24 | if (error) { 25 | console.log(error); 26 | return; 27 | } 28 | 29 | console.log('Listening at http://localhost:3000 !!! '); 30 | }); 31 | -------------------------------------------------------------------------------- /app/Redux/Action.js: -------------------------------------------------------------------------------- 1 | function updatePoint(latLon) { 2 | return { 3 | type: 'updateLatLon', 4 | lat: parseFloat(latLon.lat), 5 | lon: parseFloat(latLon.lon) 6 | }; 7 | } 8 | 9 | function updateZoom(zoomLevel) { 10 | return { 11 | type: 'updateZoom', 12 | zoom: parseInt(zoomLevel, 10) 13 | }; 14 | } 15 | 16 | function updateTileNum(tileLatLon) { 17 | return { 18 | type: 'updateZoom', 19 | tileLat: parseInt(tileLatLon.lat, 10), 20 | tileLon: parseInt(tileLatLon.lon, 10) 21 | }; 22 | } 23 | 24 | function updatePointZoom(latLonZoom) { 25 | return { 26 | type: 'updatePointZoom', 27 | lat: parseFloat(latLonZoom.lat), 28 | lon: parseFloat(latLonZoom.lon), 29 | zoom: parseInt(latLonZoom.zoom, 10) 30 | }; 31 | } 32 | 33 | module.exports = { updatePoint, updateTileNum, updateZoom, updatePointZoom }; 34 | -------------------------------------------------------------------------------- /app/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import Keys from './Keys'; 5 | import SearchBox from './Components/Search/SearchBox'; 6 | 7 | import TileExporter from './Components/TileExporter/Exporter'; 8 | 9 | require('./scss/main.scss'); 10 | 11 | // Tile Exporter is not written as React Component 12 | const exporter = new TileExporter(); 13 | exporter.attachEvents(); 14 | 15 | const searchConfig = { 16 | placeholder: 'Search address or or place', 17 | childClass: 'searchBox', 18 | key: Keys.search 19 | }; 20 | 21 | function SearchBoxWrapper() { 22 | return ( 23 |
    24 |
    25 | 26 |
    27 |
    28 | ); 29 | } 30 | 31 | render(, document.getElementById('search-bar')); 32 | -------------------------------------------------------------------------------- /app/Components/TileExporter/PreviewMap.js: -------------------------------------------------------------------------------- 1 | import PreviewUnit from './PreviewUnit'; 2 | 3 | class PreviewMap { 4 | constructor(exporter) { 5 | this.previewSvgs = [ 6 | new PreviewUnit('preview-north-east', exporter), 7 | new PreviewUnit('preview-north', exporter), 8 | new PreviewUnit('preview-north-west', exporter), 9 | new PreviewUnit('preview-center-east', exporter), 10 | new PreviewUnit('preview-center', exporter), 11 | new PreviewUnit('preview-center-west', exporter), 12 | new PreviewUnit('preview-south-east', exporter), 13 | new PreviewUnit('preview-south', exporter), 14 | new PreviewUnit('preview-south-west', exporter) 15 | ]; 16 | } 17 | 18 | drawData() { 19 | this.previewSvgs.map(svg => svg.drawData()); 20 | } 21 | 22 | destroy() { 23 | this.previewSvgs.map(svg => svg.destroy()); 24 | } 25 | } 26 | 27 | export default PreviewMap; 28 | -------------------------------------------------------------------------------- /app/Components/TileExporter/MapSpells.js: -------------------------------------------------------------------------------- 1 | // Convert lat/lon to mercator style number 2 | function lon2tile(lon, zoom) { 3 | return (Math.round((lon+180)/360*Math.pow(2,zoom))); 4 | } 5 | function lat2tile(lat ,zoom) { 6 | return (Math.round((1-Math.log(Math.tan(lat*Math.PI/180) + 1/Math.cos(lat*Math.PI/180))/Math.PI)/2 *Math.pow(2,zoom))); 7 | } 8 | // Reverse functions of the one above to navigate tiles 9 | // Functions done by Matt Blair https://github.com/blair1618 Thank you :) 10 | 11 | function tile2Lon(tileLon, zoom) { 12 | return (tileLon*360/Math.pow(2,zoom)-180).toFixed(7); 13 | } 14 | 15 | function tile2Lat(tileLat, zoom) { 16 | return ((360/Math.PI) * Math.atan(Math.pow( Math.E, (Math.PI - 2*Math.PI*tileLat/(Math.pow(2,zoom)))))-90).toFixed(7); 17 | } 18 | // This is the left over from an attemp to get right xyz ratio of the tile 19 | function getMeterValue(lat ,zoom) { 20 | return 40075016.686 * Math.abs(Math.cos(lat * 180/Math.PI)) / Math.pow(2, zoom+8).toFixed(4); 21 | } 22 | 23 | module.exports = { lon2tile, lat2tile, tile2Lon, tile2Lat, getMeterValue }; 24 | -------------------------------------------------------------------------------- /app/Redux/Reducer.js: -------------------------------------------------------------------------------- 1 | import { lon2tile, lat2tile } from '../Components/TileExporter/MapSpells'; 2 | 3 | const initialState = { 4 | lat: 40.71427, 5 | lon: -74.00597, 6 | tileLat: lat2tile(40.71427), 7 | tileLon: lon2tile(-74.00597), 8 | zoom: 16 9 | }; 10 | 11 | function tileInfo(state = initialState, action = {}) { 12 | switch (action.type) { 13 | case 'updateLatLon': 14 | return { 15 | ...state, 16 | lat: action.lat, 17 | lon: action.lon, 18 | tileLat: lat2tile(action.lat, state.zoom), 19 | tileLon: lon2tile(action.lon, state.zoom) 20 | }; 21 | case 'updateZoom': 22 | return { 23 | ...state, 24 | tileLat: lat2tile(parseFloat(state.lat), action.zoom), 25 | tileLon: lon2tile(parseFloat(state.lon), action.zoom), 26 | zoom: action.zoom 27 | }; 28 | case 'updatePointZoom': 29 | return { 30 | zoom: action.zoom, 31 | lat: action.lat, 32 | lon: action.lon, 33 | tileLat: lat2tile(action.lat, action.zoom), 34 | tileLon: lon2tile(action.lon, action.zoom) 35 | }; 36 | default: 37 | return state; 38 | } 39 | } 40 | 41 | export default tileInfo; 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Tile Exporter 2 | 3 | [Try Tile Exporter!](http://hanbyul-here.github.io/tile-exporter/) 4 | 5 | ![tile exporter screenshot](https://s3.amazonaws.com/assets-staging.mapzen.com/images/tile-exporter/tile-exporter-screenshot.png) 6 | 7 | The tile exporter grabs a [Mapzen vector tile](https://mapzen.com/projects/vector-tiles), offers you 3d preview in your browser, and then creates an .OBJ file of the scene that you can download. The tile exporter gets the `buildings`, `earth`, `water`, `landuse` layers of a tile. Learn more about layers in tiles at the [Mapzen Vector Tile documentation](https://mapzen.com/documentation/vector-tiles/layers/). 8 | 9 | ### How to run locally 10 | 11 | Search component of tile exporter uses [React](https://facebook.github.io/react/), uses [webpack](https://webpack.github.io/) to bundle everything together. 12 | 13 | ``` 14 | npm install 15 | npm run-script dev 16 | ``` 17 | Then go to `localhost:3000` on any browser. 18 | 19 | If you want to build on local, you can run 20 | 21 | ``` 22 | npm build 23 | ``` 24 | 25 | This command builds `index.html` and `bundle.js` file on the directory. 26 | 27 | There is also a [vanilla javascript version](https://github.com/hanbyul-here/vector-tile-obj-exporter) of this, if you prefer. 28 | 29 | - If you are interested in large scale, elevation data combined 3d print, check out [Vectiler](https://github.com/karimnaaji/vectiler). 30 | - If you are interested in SVG export, check [SVG Export tool](https://github.com/hanbyul-here/svg-exporter). -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | var path = require('path'); 4 | 5 | var HtmlWebpackPlugin = require('html-webpack-plugin') 6 | 7 | const config = { 8 | addVendor: function (name, path) { 9 | this.resolve.alias[name] = path; 10 | this.module.noParse.push(path); 11 | }, 12 | entry: [ 13 | 'webpack-hot-middleware/client', 14 | './app/App.jsx' 15 | ], 16 | output: { 17 | path: path.resolve(__dirname, './'), 18 | filename: 'bundle.js' 19 | }, 20 | plugins: [ 21 | new HtmlWebpackPlugin({ 22 | template: './src/index.html', 23 | inject: false 24 | }), 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoErrorsPlugin() 27 | ], 28 | devtool: 'cheap-module-source-map', 29 | module: { 30 | noParse: [], 31 | loaders: [{ 32 | test: /\.css$/, 33 | loader: 'style-loader!css-loader' 34 | }, { 35 | test: /\.scss$/, 36 | loader: 'style!css!sass?sourceMap' 37 | }, 38 | { 39 | test: /\.(svg)$/, 40 | loader: 'url-loader?limit=10000' 41 | }, 42 | { 43 | test: /\.jsx?$/, 44 | loader: 'babel', 45 | exclude: /node_modules/, 46 | query: { 47 | presets: ['react', 'es2015'] 48 | } 49 | }, 50 | { 51 | test: /.js?$/, 52 | loader: 'babel-loader', 53 | exclude: /node_modules/, 54 | include: [ 55 | path.resolve(__dirname) 56 | ] 57 | }] 58 | }, 59 | resolve: { 60 | extensions: ['', '.js', '.jsx'] 61 | } 62 | }; 63 | 64 | module.exports = config; 65 | -------------------------------------------------------------------------------- /app/Components/TileExporter/QueryChecker.js: -------------------------------------------------------------------------------- 1 | // import PreviewUnit from './PreviewUnit'; 2 | import store from '../../Redux/Store'; 3 | import { updatePointZoom } from '../../Redux/Action'; 4 | 5 | class QueryChecker { 6 | constructor(exporter) { 7 | QueryChecker.checkQueries(exporter); 8 | } 9 | 10 | static checkQueries(exporter) { 11 | const _lon = QueryChecker.getParameterByName('lon'); 12 | const _lat = QueryChecker.getParameterByName('lat'); 13 | let _zoom = QueryChecker.getParameterByName('zoom'); 14 | 15 | if (_lon !== null && _lat !== null && _zoom !== null) { 16 | _zoom = _zoom.replace(/[^0-9]+/g, ''); 17 | document.zoomRadio.zoomLevel.value = _zoom; 18 | store.dispatch(updatePointZoom({ 19 | lat: _lat, 20 | lon: _lon, 21 | zoom: _zoom 22 | })); 23 | exporter.fetchTheTile(exporter.buildQueryURL()); 24 | document.getElementById('exportBtn').disabled = false; 25 | } 26 | } 27 | 28 | static updateQueryString(paramObj) { 29 | const params = []; 30 | for (const key of Object.keys(paramObj)) { 31 | params.push(`${encodeURIComponent(key)}=${encodeURIComponent(paramObj[key])}`); 32 | } 33 | 34 | const newUrl = `${window.location.origin}${window.location.pathname}?${params.join('&')}`; 35 | window.history.replaceState({}, '', newUrl); 36 | } 37 | 38 | static getParameterByName(_name) { 39 | const url = window.location.href; 40 | const name = _name.replace(/[\[\]]/g, '\\$&'); // eslint-disable-line 41 | const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); 42 | const results = regex.exec(url); 43 | if (!results) return null; 44 | if (!results[2]) return ''; 45 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 46 | } 47 | 48 | } 49 | 50 | export default QueryChecker; 51 | -------------------------------------------------------------------------------- /app/scss/preview.scss: -------------------------------------------------------------------------------- 1 | .navigation { 2 | position: fixed; 3 | right: 20px; 4 | bottom: 30px; 5 | width: 300px; 6 | height: 300px; 7 | } 8 | @media only screen and (max-device-width: 480px) { 9 | .navigation { 10 | display: none; 11 | } 12 | } 13 | 14 | .direction-control { 15 | position: relative; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | 21 | 22 | div[id^="preview"] { 23 | content: ' '; 24 | position: absolute; 25 | background-color: rgba(255,255,255,0.7); 26 | width: 100px; 27 | height: 100px; 28 | fill: rgba(205,205,205,0.5); 29 | stroke: #666; 30 | &:hover:before { 31 | cursor: pointer; 32 | text-align: center; 33 | color: #fff; 34 | font-weight: 600; 35 | line-height: 100px; 36 | background-color: rgba(100,100,100,0.7); 37 | position: absolute; 38 | width: 100px; 39 | height: 100px; 40 | } 41 | } 42 | 43 | #preview-north-east { 44 | top: 0px; 45 | left: 200px; 46 | &:hover:before { 47 | content: 'NE'; 48 | } 49 | } 50 | #preview-north { 51 | top: 0px; 52 | left: 100px; 53 | &:hover:before { content: 'N';} 54 | } 55 | #preview-north-west { 56 | top: 0px; 57 | left: 0px; 58 | &:hover:before { content: 'NW';} 59 | } 60 | 61 | #preview-center-east { 62 | top:100px; 63 | left: 200px; 64 | &:hover:before { content: 'E';} 65 | } 66 | #preview-center {top:100px; left: 100px; 67 | fill: rgba(255,255,255,0.8); 68 | stroke: #000; 69 | z-index: 5; 70 | } 71 | #preview-center-west { 72 | top:100px; 73 | left: 0px; 74 | &:hover:before { content: 'W'} 75 | } 76 | 77 | #preview-south-east { 78 | top: 200px; 79 | left: 200px; 80 | &:hover:before { content: 'SE'} 81 | } 82 | #preview-south { 83 | top: 200px; 84 | left: 100px; 85 | &:hover:before {content: 'S'} 86 | } 87 | #preview-south-west { 88 | top: 200px; 89 | left: 0px; 90 | &:hover:before {content: 'SW'} 91 | } 92 | -------------------------------------------------------------------------------- /app/Components/TileExporter/DomHelper.js: -------------------------------------------------------------------------------- 1 | import store from '../../Redux/Store'; 2 | import { updateZoom } from '../../Redux/Action'; 3 | 4 | class DomHelper { 5 | constructor(exporter) { 6 | this.exporter = exporter; 7 | this.latElem = document.getElementById('lat'); 8 | this.lonElem = document.getElementById('lon'); 9 | this.loadingBar = document.getElementById('loading-bar'); 10 | } 11 | 12 | attachEvents() { 13 | this._attachExportBtnEvent(); 14 | DomHelper.attachZoomBtnEvent(); 15 | DomHelper.attachControlPanelEvent(); 16 | } 17 | 18 | _attachExportBtnEvent() { 19 | // Export button event 20 | const exportBtn = document.getElementById('exportBtn'); 21 | 22 | exportBtn.addEventListener('click', () => { 23 | this.exporter.fetchTheTile(this.exporter.buildQueryURL()); 24 | }); 25 | } 26 | 27 | static attachZoomBtnEvent() { 28 | // Zoom button event 29 | const zoomRad = document.zoomRadio.zoomLevel; 30 | for (const zoomBtn of zoomRad) { 31 | zoomBtn.addEventListener('click', () => { 32 | const zoomLevel = parseInt(zoomBtn.value, 10); 33 | store.dispatch(updateZoom(zoomLevel)); 34 | }); 35 | } 36 | } 37 | 38 | static attachControlPanelEvent() { 39 | // Mobile UI (show hide-control button) 40 | const mainControl = document.getElementById('main-control'); 41 | const toggleBtn = document.getElementById('hide-toggle'); 42 | 43 | toggleBtn.addEventListener('click', () => { 44 | if (mainControl.style.display !== 'none') { 45 | mainControl.style.display = 'none'; 46 | this.innerHTML = 'Show control'; 47 | } else { 48 | mainControl.style.display = 'block'; 49 | this.innerHTML = 'Hide control'; 50 | } 51 | }); 52 | } 53 | 54 | showLoadingBar() { 55 | this.loadingBar.style.display = 'block'; 56 | } 57 | 58 | hideLoadingBar() { 59 | this.loadingBar.style.display = 'none'; 60 | } 61 | 62 | displayCoord() { 63 | this.latElem.innerHTML = store.getState().lat; 64 | this.lonElem.innerHTML = store.getState().lon; 65 | } 66 | } 67 | 68 | export default DomHelper; 69 | -------------------------------------------------------------------------------- /app/scss/searchbar.scss: -------------------------------------------------------------------------------- 1 | #search-bar { 2 | 3 | width: 100%; 4 | height: 100%; 5 | margin-bottom: 20px; 6 | background-color: #fff; 7 | position: relative; 8 | z-index: 300; 9 | 10 | .search-icon { 11 | width: 30px; 12 | height: 34px; 13 | position: absolute; 14 | background-color: #fff; 15 | background-image: url("./resources/icons.svg"); 16 | background-repeat: no-repeat; 17 | background-size: 200px 25px; 18 | background-position: 3px 3px; 19 | } 20 | 21 | .search-bar { 22 | width: 100%; 23 | float: left; 24 | padding-left: 33px; 25 | border: 0; 26 | text-overflow: ellipsis; 27 | 28 | &::-webkit-search-decoration::after { 29 | content: ' '; 30 | width: 0px; 31 | height: 0px; 32 | display: inline-block; 33 | background-image: none; 34 | margin: 0px 0px; 35 | } 36 | 37 | &::-webkit-search-decoration { 38 | display:none; 39 | } 40 | } 41 | 42 | 43 | .table-view.search-table { 44 | padding-left: 0; 45 | max-height: 150px; 46 | overflow-y: auto; 47 | position: absolute; 48 | 49 | top: 41px; 50 | width: calc(100% - 30px); 51 | 52 | li { 53 | background-color: #fff; 54 | font-size: 14px; 55 | width: 100%; 56 | float: left; 57 | z-index: 50; 58 | cursor: pointer; 59 | border-top: 1px solid #ddd; 60 | border-bottom: 0; 61 | padding-left: 5px; 62 | overflow: hidden; 63 | text-overflow: ellipsis; 64 | 65 | &:hover { 66 | background-color: #ddd; 67 | } 68 | &.select { 69 | background-color: #ccc; 70 | } 71 | 72 | &::before { 73 | content: ' '; 74 | width: 10px; 75 | height: 10px; 76 | display: inline-block; 77 | background-image: url("./resources/icons.svg"); 78 | background-repeat: no-repeat; 79 | background-size: 80px 10px; 80 | } 81 | &.search-result::before { 82 | background-position: -60px 0px; 83 | } 84 | &.search-term-result::before { 85 | background-position: -70px 0px; 86 | } 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /app/Components/TileExporter/BasicScene.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | import OrbitControls from '../../libs/OrbitControl'; // eslint-disable-line 3 | import '../../libs/Triangulation'; 4 | // Changes the way Threejs does triangulation 5 | THREE.Triangulation.setLibrary('earcut'); 6 | 7 | class BasicScene { 8 | 9 | constructor() { 10 | const w = window.innerWidth; 11 | const h = window.innerHeight; 12 | 13 | // Global : renderer 14 | this.renderer = new THREE.WebGLRenderer({ antialias: true }); 15 | this.renderer.setSize(w, h); 16 | 17 | // Global : scene 18 | this.scene = new THREE.Scene(); 19 | 20 | // Global : camera 21 | this.camera = new THREE.PerspectiveCamera(20, w / h, 1, 1000000); 22 | this.camera.position.set(0, 0, 500); 23 | this.camera.lookAt(new THREE.Vector3(0, 0, 0)); 24 | 25 | // orbit control 26 | this.controls = new OrbitControls(this.camera, this.renderer.domElement); 27 | this.controls.enableDamping = true; 28 | this.controls.dampingFactor = 0.25; 29 | this.controls.enableZoom = false; 30 | 31 | // direct light 32 | const light = new THREE.DirectionalLight(0xffffff); 33 | light.position.set(1, 1, 1); 34 | light.rotation.set(2, 1, 1); 35 | this.scene.add(light); 36 | 37 | // ambient light 38 | const ambientLight = new THREE.AmbientLight(0xffffff); 39 | this.scene.add(ambientLight); 40 | 41 | // attach renderer to DOM 42 | document.body.appendChild(this.renderer.domElement); 43 | // initiating animate of rendere at the same time 44 | this.animate(); 45 | } 46 | 47 | get getScene() { 48 | return this.scene; 49 | } 50 | 51 | animate() { 52 | requestAnimationFrame(this.animate.bind(this)); 53 | this.controls.update(); 54 | this.renderer.render(this.scene, this.camera); 55 | } 56 | 57 | onWindowResize() { 58 | this.camera.aspect = window.innerWidth / window.innerHeight; 59 | this.camera.updateProjectionMatrix(); 60 | this.renderer.setSize(window.innerWidth, window.innerHeight); 61 | } 62 | 63 | addObject(obj) { 64 | this.scene.add(obj); 65 | } 66 | 67 | removeObject(objName) { 68 | const selectedObj = this.scene.getObjectByName(objName); 69 | if (selectedObj) this.scene.remove(selectedObj); 70 | } 71 | } 72 | 73 | export default BasicScene; 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vector-tile-obj-exporter", 3 | "version": "1.0.0", 4 | "description": "get you favorite tile in obj form", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "./node_modules/.bin/eslint --ext app/Components/Search/*.jsx --ext app/Components/TileExporter/*.js;", 8 | "dev": "node -r babel-core/register server.js", 9 | "build": "webpack -p" 10 | }, 11 | "keywords": [ 12 | "vector", 13 | "tiles", 14 | "obj" 15 | ], 16 | "dependencies": { 17 | "body-parser": "^1.18.3", 18 | "d3": "^3.5.16", 19 | "earcut": "^2.1.1", 20 | "file-loader": "0.8.4", 21 | "http-proxy": "1.11.2", 22 | "less": "2.5.3", 23 | "less-loader": "^2.2.1", 24 | "lodash": "^4.17.0", 25 | "node-sass": "^4.13.1", 26 | "piping": "0.2.0", 27 | "pretty-error": "1.2.0", 28 | "react": "^0.14.0", 29 | "react-dom": "0.14.0", 30 | "react-inline-css": "2.0.0", 31 | "redux": "3.0.1", 32 | "serve-favicon": "^2.5.0", 33 | "serve-static": "^1.13.2", 34 | "three": "^0.74.0", 35 | "url-loader": "0.5.6", 36 | "webpack-isomorphic-tools": "^2.5.7" 37 | }, 38 | "devDependencies": { 39 | "autoprefixer-loader": "^3.1.0", 40 | "babel": "^6.5.2", 41 | "babel-core": "^6.13.2", 42 | "babel-eslint": "^6.1.2", 43 | "babel-loader": "^6.2.4", 44 | "babel-plugin-react-transform": "^2.0.2", 45 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 46 | "babel-plugin-typecheck": "^3.9.0", 47 | "babel-preset-es2015": "^6.13.2", 48 | "babel-preset-react": "^6.11.1", 49 | "babel-runtime": "^6.11.6", 50 | "better-npm-run": "^0.0.2", 51 | "clean-webpack-plugin": "^0.1.3", 52 | "concurrently": "^0.1.1", 53 | "css-loader": "^0.19.0", 54 | "eslint": "^3.9.0", 55 | "eslint-config-airbnb": "^12.0.0", 56 | "eslint-import-resolver-webpack": "^0.7.0", 57 | "eslint-plugin-import": "^2.1.0", 58 | "eslint-plugin-jsx-a11y": "^2.2.2", 59 | "eslint-plugin-react": "^6.5.0", 60 | "express": "^4.14.0", 61 | "extract-text-webpack-plugin": "^1.0.1", 62 | "html-webpack-plugin": "^2.22.0", 63 | "install": "^0.8.2", 64 | "json-loader": "^0.5.3", 65 | "node-sass": "^3.3.3", 66 | "react-a11y": "^0.2.6", 67 | "react-addons-test-utils": "^0.14.0", 68 | "react-hot-loader": "^1.3.0", 69 | "react-transform-catch-errors": "^1.0.0", 70 | "react-transform-hmr": "^1.0.1", 71 | "redbox-react": "^1.1.1", 72 | "sass-loader": "^3.0.0", 73 | "strip-loader": "^0.1.0", 74 | "style-loader": "^0.12.4", 75 | "webpack": "^1.12.2", 76 | "webpack-dev-middleware": "^1.2.0", 77 | "webpack-hot-middleware": "^2.4.1" 78 | }, 79 | "author": "Hanbyul Jo", 80 | "license": "MIT" 81 | } 82 | -------------------------------------------------------------------------------- /app/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 'searchbar'; 2 | @import 'preview'; 3 | @import 'styleguide.min.css'; 4 | 5 | html,body{ 6 | margin: 0; 7 | padding: 0; 8 | width: 100%; 9 | height: 100%; 10 | overflow-y: hidden; 11 | } 12 | 13 | .control { 14 | width: 300px; 15 | position: fixed; 16 | left: 20px; 17 | padding: 20px; 18 | background: rgba(255,255,255,0.7); 19 | font-size: 14px; 20 | } 21 | 22 | @media only screen and (max-device-width: 480px) { 23 | .control { 24 | width: calc(100% - 40px); 25 | } 26 | } 27 | 28 | .control-toggle { 29 | display: none; 30 | } 31 | 32 | @media only screen and (max-device-width: 480px) { 33 | .control-toggle { 34 | display: block; 35 | position: fixed; 36 | background-color: #fff; 37 | top: 0; 38 | right: 0; 39 | } 40 | } 41 | 42 | .form-group { 43 | margin-bottom: 5px; 44 | .label { 45 | font-style: italic; 46 | display: inline; 47 | } 48 | .geocode { 49 | font-weight: 400; 50 | display: inline; 51 | border-bottom: 1px solid #ccc; 52 | } 53 | } 54 | 55 | #exportA { 56 | font-size: 18px; 57 | } 58 | 59 | .loading-bar { 60 | position: fixed; 61 | top: 0px; 62 | left: 0px; 63 | width: 100%; 64 | height: 100%; 65 | background-color: rgba(255,255,255,0.3); 66 | z-index: 30; 67 | display: none; 68 | } 69 | 70 | .loading-bar::before { 71 | content: ' '; 72 | position: fixed; 73 | top: calc(50% - 50px); 74 | left: calc(50% - 50px) ; 75 | width: 100px; 76 | height: 100px; 77 | z-index: 20030; 78 | border-radius: 50%; 79 | border: 15px solid #fff; 80 | border-top-color: transparent; 81 | color: #fff; 82 | animation: spinning 1s infinite linear; 83 | -webkit-animation: spinning 1s infinite linear; 84 | -moz-animation: spinning 1s infinite linear; 85 | } 86 | 87 | .loading-bar::after { 88 | content: 'Loading'; 89 | position: fixed; 90 | color: #fff; 91 | font-size: 12px; 92 | top: calc(50% - 7px); 93 | left: calc(50% - 18px); 94 | } 95 | 96 | .marginTopDown { 97 | margin: 10px 0; 98 | } 99 | 100 | a.disabled { 101 | color: #eee; 102 | pointer-event: none; 103 | } 104 | 105 | 106 | .cc { 107 | position: fixed; 108 | bottom: 0; 109 | right: 0; 110 | padding: 5px; 111 | } 112 | @media only screen and (max-device-width: 480px) { 113 | .cc { 114 | font-size: 11px; 115 | } 116 | } 117 | 118 | //loading spinner animation 119 | @keyframes spinning { 120 | 0% { transform: rotate(0deg); } 121 | 100% { transform: rotate(360deg); } 122 | } 123 | 124 | @-webkit-keyframes spinning { 125 | 0% { -webkit-transform: rotate(0deg); } 126 | 100% { -webkit-transform: rotate(360deg); } 127 | } 128 | @-moz-keyframes spinning { 129 | 0% { -moz-transform: rotate(0deg); } 130 | 100% { -moz-transform: rotate(360deg); } 131 | } 132 | -------------------------------------------------------------------------------- /app/Components/TileExporter/PreviewUnit.js: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | import store from '../../Redux/Store'; 3 | import * as Key from '../../Keys'; 4 | import { tile2Lon, tile2Lat } from './MapSpells'; 5 | 6 | class PreviewUnit { 7 | 8 | constructor(domID, exporter) { 9 | const width = 100; 10 | const height = 100; 11 | 12 | this.svg = d3.select(`#${domID}`) 13 | .append('svg') 14 | .attr('width', width) 15 | .attr('height', height); 16 | this.tilePos = PreviewUnit.getTilePos(domID); 17 | 18 | const btn = document.getElementById(domID); 19 | btn.addEventListener('click', () => { 20 | exporter.navigateTile(this.tilePos); 21 | }); 22 | } 23 | 24 | static getTilePos(domID) { 25 | // Figure out tile number based on Preview element's ID 26 | const tilePosObj = { 27 | ns: 0, 28 | ew: 0 29 | }; 30 | 31 | const tilepos = domID.split('-'); 32 | 33 | if (tilepos[1] === 'south') tilePosObj.ns = 1; 34 | if (tilepos[1] === 'north') tilePosObj.ns = -1; 35 | 36 | if (tilepos.length > 2) { 37 | if (tilepos[2] === 'east') tilePosObj.ew = 1; 38 | if (tilepos[2] === 'west') tilePosObj.ew = -1; 39 | } 40 | 41 | return tilePosObj; 42 | } 43 | 44 | drawTheTile(url) { 45 | const zoom = store.getState().zoom; 46 | const previewProjection = d3.geo.mercator() 47 | .center([url.centerLatLon.lon, url.centerLatLon.lat]) 48 | // This scale is carved based on zoom 16, fit into 100px * 100px rect 49 | .scale(600000 * (100 / 57) * Math.pow(2, (zoom - 16))) 50 | .precision(0) 51 | .translate([0, 0]); 52 | 53 | const svg = this.svg; 54 | 55 | d3.json(url.callURL, (err, json) => { 56 | for (let obj in json) { // eslint-disable-line 57 | if (err) console.log(`Error : +${err}`); 58 | else { 59 | for (let geoFeature of json[obj].features) { // eslint-disable-line 60 | const previewPath = d3.geo.path().projection(previewProjection); 61 | const previewFeature = previewPath(geoFeature); 62 | if (previewFeature.indexOf('a') > 0) ; 63 | else { 64 | svg.append('path') 65 | .attr('d', previewFeature); 66 | } 67 | } 68 | } 69 | } 70 | }); 71 | } 72 | 73 | buildQueryURL() { 74 | const zoom = store.getState().zoom; 75 | 76 | const tLon = store.getState().tileLon + this.tilePos.ew; 77 | const tLat = store.getState().tileLat + this.tilePos.ns; 78 | 79 | const config = { 80 | baseURL: 'https://tile.nextzen.org/tilezen/vector/v1', 81 | dataKind: 'all', 82 | fileFormat: 'json' 83 | }; 84 | 85 | const _callURL = `${config.baseURL}/${config.dataKind}/${zoom}/${tLon}/${tLat}.${config.fileFormat}?api_key=${Key.vectorTile}`; 86 | 87 | const _centerLatLon = { 88 | lat: tile2Lat(tLat, zoom), 89 | lon: tile2Lon(tLon, zoom) 90 | }; 91 | 92 | return { 93 | callURL: _callURL, 94 | centerLatLon: _centerLatLon 95 | }; 96 | } 97 | 98 | drawData() { 99 | this.drawTheTile(this.buildQueryURL()); 100 | } 101 | 102 | destroy() { 103 | this.svg.selectAll('*').remove(); 104 | } 105 | } 106 | 107 | export default PreviewUnit; 108 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 |
    16 |
    17 |
    18 | 3D print your favorite map tile. Enter your coordinates to generate an OBJ file that you can send to a 3D printer! 19 |
    20 |
    21 |
    22 | 23 | 24 | 25 |
    26 |
    Longitude
    27 |
    Please select a place.
    28 |
    29 |
    30 |
    Latitude
    31 |
    32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 | 39 |
    40 |
    41 | 42 |
    43 |
    44 | 45 |
    46 |
    47 |
    48 |
    49 |
    50 |
    51 |
    52 | 53 |
    54 |
    55 |
    56 |
    57 |
    58 |
    59 | Download 60 |
    61 | 62 |
    63 |
    64 |
    65 |
    66 | Hide control 67 |
    68 | 69 | 82 | 83 |
    84 | © OSM contributors | Mapzen | Github Repo 85 |
    86 | 87 |
    88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 |
    16 |
    17 |
    18 | 3D print your favorite map tile. Enter your coordinates to generate an OBJ file that you can send to a 3D printer! 19 |
    20 |
    21 |
    22 | 23 | 24 | 25 |
    26 |
    Longitude
    27 |
    Please select a place.
    28 |
    29 |
    30 |
    Latitude
    31 |
    32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 | 39 |
    40 |
    41 | 42 |
    43 |
    44 | 45 |
    46 |
    47 |
    48 |
    49 |
    50 |
    51 |
    52 | 53 |
    54 |
    55 |
    56 |
    57 |
    58 |
    59 | Download 60 |
    61 | 62 |
    63 |
    64 |
    65 |
    66 | Hide control 67 |
    68 | 69 | 82 | 83 |
    84 | © OSM contributors | Mapzen | Github Repo 85 |
    86 | 87 |
    88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/libs/OBJ-Exporter.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three' 2 | 3 | /** 4 | * @author mrdoob / http://mrdoob.com/ 5 | */ 6 | 7 | THREE.OBJExporter = function () {}; 8 | 9 | THREE.OBJExporter.prototype = { 10 | 11 | constructor: THREE.OBJExporter, 12 | 13 | parse: function ( object ) { 14 | 15 | var output = ''; 16 | 17 | var indexVertex = 0; 18 | var indexVertexUvs = 0; 19 | var indexNormals = 0; 20 | 21 | var parseMesh = function ( mesh ) { 22 | 23 | var nbVertex = 0; 24 | var nbVertexUvs = 0; 25 | var nbNormals = 0; 26 | 27 | var geometry = mesh.geometry; 28 | 29 | if ( geometry instanceof THREE.Geometry ) { 30 | 31 | output += 'o ' + mesh.name + '\n'; 32 | 33 | var vertices = geometry.vertices; 34 | 35 | for ( var i = 0, l = vertices.length; i < l; i ++ ) { 36 | 37 | var vertex = vertices[ i ].clone(); 38 | vertex.applyMatrix4( mesh.matrixWorld ); 39 | 40 | output += 'v ' + vertex.x + ' ' + vertex.y + ' ' + vertex.z + '\n'; 41 | 42 | nbVertex ++; 43 | 44 | } 45 | 46 | // uvs 47 | 48 | var faces = geometry.faces; 49 | var faceVertexUvs = geometry.faceVertexUvs[ 0 ]; 50 | var hasVertexUvs = faces.length === faceVertexUvs.length; 51 | 52 | if ( hasVertexUvs ) { 53 | 54 | for ( var i = 0, l = faceVertexUvs.length; i < l; i ++ ) { 55 | 56 | var vertexUvs = faceVertexUvs[ i ]; 57 | 58 | for ( var j = 0, jl = vertexUvs.length; j < jl; j ++ ) { 59 | 60 | var uv = vertexUvs[ j ]; 61 | 62 | output += 'vt ' + uv.x + ' ' + uv.y + '\n'; 63 | 64 | nbVertexUvs ++; 65 | 66 | } 67 | 68 | } 69 | 70 | } 71 | 72 | // normals 73 | 74 | var normalMatrixWorld = new THREE.Matrix3(); 75 | normalMatrixWorld.getNormalMatrix( mesh.matrixWorld ); 76 | 77 | for ( var i = 0, l = faces.length; i < l; i ++ ) { 78 | 79 | var face = faces[ i ]; 80 | var vertexNormals = face.vertexNormals; 81 | 82 | if ( vertexNormals.length === 3 ) { 83 | 84 | for ( var j = 0, jl = vertexNormals.length; j < jl; j ++ ) { 85 | 86 | var normal = vertexNormals[ j ].clone(); 87 | normal.applyMatrix3( normalMatrixWorld ); 88 | 89 | output += 'vn ' + normal.x + ' ' + normal.y + ' ' + normal.z + '\n'; 90 | 91 | nbNormals ++; 92 | 93 | } 94 | 95 | } else { 96 | 97 | var normal = face.normal.clone(); 98 | normal.applyMatrix3( normalMatrixWorld ); 99 | 100 | for ( var j = 0; j < 3; j ++ ) { 101 | 102 | output += 'vn ' + normal.x + ' ' + normal.y + ' ' + normal.z + '\n'; 103 | 104 | nbNormals ++; 105 | 106 | } 107 | 108 | } 109 | 110 | } 111 | 112 | // faces 113 | 114 | 115 | for ( var i = 0, j = 1, l = faces.length; i < l; i ++, j += 3 ) { 116 | 117 | var face = faces[ i ]; 118 | 119 | output += 'f '; 120 | output += ( indexVertex + face.a + 1 ) + '/' + ( hasVertexUvs ? ( indexVertexUvs + j ) : '' ) + '/' + ( indexNormals + j ) + ' '; 121 | output += ( indexVertex + face.b + 1 ) + '/' + ( hasVertexUvs ? ( indexVertexUvs + j + 1 ) : '' ) + '/' + ( indexNormals + j + 1 ) + ' '; 122 | output += ( indexVertex + face.c + 1 ) + '/' + ( hasVertexUvs ? ( indexVertexUvs + j + 2 ) : '' ) + '/' + ( indexNormals + j + 2 ) + '\n'; 123 | 124 | } 125 | 126 | } else { 127 | 128 | console.warn( 'THREE.OBJExporter.parseMesh(): geometry type unsupported', mesh ); 129 | // TODO: Support only BufferGeometry and use use setFromObject() 130 | 131 | } 132 | 133 | // update index 134 | indexVertex += nbVertex; 135 | indexVertexUvs += nbVertexUvs; 136 | indexNormals += nbNormals; 137 | 138 | }; 139 | 140 | object.traverse( function ( child ) { 141 | 142 | if ( child instanceof THREE.Mesh ) parseMesh( child ); 143 | 144 | } ); 145 | 146 | return output; 147 | 148 | } 149 | 150 | }; 151 | 152 | module.exports = THREE.OBJExporter; -------------------------------------------------------------------------------- /app/scss/resources/icons.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/Components/Search/SearchBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { debounce } from 'lodash'; 3 | 4 | import ResultTable from './ResultTable'; 5 | 6 | import store from '../../Redux/Store'; 7 | import { updatePoint } from '../../Redux/Action'; 8 | 9 | 10 | class SearchBox extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.baseurl ='https://api.geocode.earth/v1'; 14 | this.state = { 15 | searchResult: [], 16 | dataIndex: -1, 17 | filterText: this.props.label || '' 18 | }; 19 | } 20 | 21 | componentWillMount() { 22 | // Make a debounced autocomplete call function so it can be used with keyboard event 23 | this.makeAutoCompleteCall = debounce(function () { 24 | this.makeNotDebouncedAutoCompleteCall.apply(this, [this.state.filterText]); 25 | }, 250); 26 | this.handleChange = this.handleChange.bind(this); 27 | this.handleKeyDown = this.handleKeyDown.bind(this); 28 | this.pointAction = this.pointAction.bind(this); 29 | } 30 | 31 | componentDidMount() { 32 | this.searchInput.focus(); 33 | } 34 | 35 | setInputValue(val) { 36 | this.setState({ 37 | filterText: val 38 | }, () => this.deactivateSearching()); 39 | } 40 | 41 | makeNotDebouncedAutoCompleteCall() { 42 | const callurl = `${this.baseurl}/autocomplete?text=${this.state.filterText}&api_key=${this.props.config.key}`; 43 | this.makePeliasCall(callurl); 44 | } 45 | 46 | makeSearchCall() { 47 | const callurl = `${this.baseurl}/search?text=${this.state.filterText}&api_key=${this.props.config.key}`; 48 | this.makePeliasCall(callurl); 49 | } 50 | 51 | makePeliasCall(callurl) { 52 | const request = new XMLHttpRequest(); 53 | request.open('GET', callurl, true); 54 | request.onload = () => { 55 | if (request.status >= 200 && request.status < 400) { 56 | // Success! 57 | const resp = JSON.parse(request.responseText); 58 | this.setState({ searchResult: resp.features }); 59 | } else { 60 | // when there is no search result? 61 | } 62 | }; 63 | 64 | request.onerror = function () { 65 | // when there is no search result / error? 66 | }; 67 | request.send(); 68 | } 69 | 70 | handleKeyDown(event) { 71 | const key = event.which || event.keyCode; 72 | // var self = this; 73 | 74 | let currentDataIndex = this.state.dataIndex; 75 | 76 | switch (key) { 77 | case 13: 78 | if (currentDataIndex !== -1) { 79 | this.pointAction(this.state.searchResult[currentDataIndex]); 80 | } else { 81 | this.makeSearchCall(); 82 | } 83 | break; 84 | case 38: 85 | currentDataIndex -= 1; 86 | currentDataIndex += this.state.searchResult.length; 87 | currentDataIndex %= this.state.searchResult.length; 88 | break; 89 | case 40: 90 | currentDataIndex += 1; 91 | currentDataIndex %= this.state.searchResult.length; 92 | break; 93 | default: 94 | break; 95 | } 96 | 97 | this.setState({ 98 | dataIndex: currentDataIndex 99 | }); 100 | 101 | event.stopPropagation(); 102 | } 103 | 104 | 105 | handleChange(event) { 106 | const currentType = event.target.value; 107 | if (currentType.length > 0) { 108 | this.setState({ 109 | filterText: currentType 110 | }, this.makeAutoCompleteCall()); 111 | } else { 112 | this.setState({ 113 | searchResult: [], 114 | filterText: '', 115 | dataIndex: -1 116 | }); 117 | } 118 | } 119 | 120 | pointAction(data) { 121 | const selectedPoint = { 122 | name: data.properties.label, 123 | gid: data.properties.gid, 124 | lat: data.geometry.coordinates[1], 125 | lon: data.geometry.coordinates[0] 126 | }; 127 | 128 | store.dispatch(updatePoint(selectedPoint)); 129 | 130 | this.setInputValue(selectedPoint.name); 131 | this.setState({ 132 | dataIndex: -1 133 | }); 134 | 135 | document.getElementById('exportBtn').disabled = false; 136 | document.getElementById('lon').innerHTML = selectedPoint.lon; 137 | document.getElementById('lat').innerHTML = selectedPoint.lat; 138 | } 139 | 140 | deactivateSearching() { 141 | this.setState({ 142 | searchTerm: [], 143 | searchResult: [] 144 | }); 145 | } 146 | 147 | render() { 148 | const { config } = this.props; 149 | const { searchResult, dataIndex } = this.state; 150 | return ( 151 |
    152 |
    153 | this.searchInput = input} // eslint-disable-line 157 | type="search" 158 | value={this.state.filterText} 159 | onChange={this.handleChange} 160 | onKeyDown={this.handleKeyDown} 161 | /> 162 | 167 |
    168 | ); 169 | } 170 | } 171 | 172 | SearchBox.propTypes = { 173 | config: PropTypes.object, // eslint-disable-line 174 | label: PropTypes.string 175 | }; 176 | 177 | export default SearchBox; 178 | -------------------------------------------------------------------------------- /app/Components/TileExporter/Exporter.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | import THREE from 'three'; 3 | import D3d from '../../libs/D3-Three'; 4 | import '../../libs/OBJ-Exporter'; 5 | 6 | import PreviewMap from './PreviewMap'; 7 | import QueryChecker from './QueryChecker'; 8 | import { tile2Lon, tile2Lat } from './MapSpells'; 9 | import DomHelper from './DomHelper'; 10 | import TileConfig from './TileConfiguration'; 11 | 12 | import Key from '../../Keys'; 13 | 14 | import store from '../../Redux/Store'; 15 | import { updatePoint } from '../../Redux/Action'; 16 | import BasicScene from './BasicScene'; 17 | 18 | import '../../libs/Triangulation'; 19 | // Changes the way Threejs does triangulation 20 | THREE.Triangulation.setLibrary('earcut'); 21 | 22 | class TileExporter { 23 | constructor() { 24 | this.basicScene = new BasicScene(); 25 | this.previewMap = new PreviewMap(this); 26 | this.domHelper = new DomHelper(this); 27 | this.queryChecker = new QueryChecker(this); 28 | 29 | this.dthreed = new D3d(); 30 | this.objExporter = new THREE.OBJExporter(); 31 | } 32 | 33 | attachEvents() { 34 | this.domHelper.attachEvents(); 35 | window.addEventListener('resize', () => { this.basicScene.onWindowResize(); }); 36 | } 37 | 38 | navigateTile(tilePos) { 39 | // Update store's coordinates to the new tile. 40 | const tLon = store.getState().tileLon + tilePos.ew; 41 | const tLat = store.getState().tileLat + tilePos.ns; 42 | 43 | const _zoom = store.getState().zoom; 44 | const newLatLonZoom = { 45 | lon: tile2Lon(tLon, _zoom), 46 | lat: tile2Lat(tLat, _zoom), 47 | zoom: _zoom 48 | }; 49 | 50 | store.dispatch(updatePoint(newLatLonZoom)); 51 | this.fetchTheTile(this.buildQueryURL()); 52 | } 53 | 54 | buildQueryURL() { // eslint-disable-line 55 | const tLon = store.getState().tileLon; 56 | const tLat = store.getState().tileLat; 57 | const zoom = store.getState().zoom; 58 | 59 | const config = { 60 | baseURL: 'https://tile.nextzen.org/tilezen/vector/v1', 61 | dataKind: 'all', 62 | fileFormat: 'json' 63 | }; 64 | 65 | const callURL = `${config.baseURL}/${config.dataKind}/${zoom}/${tLon}/${tLat}.${config.fileFormat}?api_key=${Key.vectorTile}`; 66 | console.log(callURL); 67 | return callURL; 68 | } 69 | 70 | fetchTheTile(callURL) { 71 | this.domHelper.showLoadingBar(); 72 | 73 | // get rid of current Tile from scene if there is any 74 | this.basicScene.removeObject('geoObjectsGroup'); 75 | 76 | // get rid of current preview 77 | this.previewMap.destroy(); 78 | 79 | // draw previewmap 80 | this.previewMap.drawData(); 81 | 82 | d3.json(callURL, (err, json) => { 83 | if (err) { 84 | console.log(`Error during fetching json from the tile server ${err}`); 85 | } else { 86 | this.basicScene.addObject(this.bakeTile(json)); 87 | } 88 | // Update hash value 89 | QueryChecker.updateQueryString({ 90 | lon: store.getState().lon, 91 | lat: store.getState().lat, 92 | zoom: store.getState().zoom 93 | }); 94 | 95 | this.domHelper.displayCoord(); 96 | this.domHelper.hideLoadingBar(); 97 | this.enableDownloadLink(); 98 | }); 99 | } 100 | 101 | bakeTile(json) { 102 | // var tileX, tileY, tileW, tileH; 103 | const projection = d3.geo.mercator() 104 | .center([store.getState().lon, store.getState().lat]) 105 | .scale(1000000) 106 | .precision(0.0) 107 | .translate([0, 0]); 108 | 109 | // Will flip the Y coordinates that result from the geo projection 110 | const flipY = d3.geo.transform({ 111 | point(x, y) { 112 | this.stream.point(x, -y); 113 | } 114 | }); 115 | 116 | // Mercator Geo Projection then flipped in Y 117 | // Solution taken from http://stackoverflow.com/a/31647135/3049530 118 | const projectionThenFlipY = { 119 | stream(s) { 120 | return projection.stream(flipY.stream(s)); 121 | } 122 | }; 123 | const path = d3.geo.path().projection(projectionThenFlipY); 124 | 125 | const geoObj = TileConfig; 126 | for (var obj in json) { // eslint-disable-line 127 | const pathWithHeights = []; 128 | for (const geoFeature of json[obj].features) { 129 | const feature = path(geoFeature); 130 | // 'a' command is not implemented in d3-three, skipping for now. 131 | // 'a' is SVG path command for Ellpitic Arc Curve. https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands 132 | if (feature.indexOf('a') < 0) { 133 | const mesh = this.dthreed.exportSVG(feature); 134 | // This is very arbitrary way to scale real 'meter' to pixel level 135 | // Better way can come up with calculating pixel per meter value 136 | // getting right xyz ratio (See getMeterValue from MapSpells) 137 | const h = geoFeature.properties['height']/5 + geoObj['earth']['height'] || geoObj[obj]['height']; // eslint-disable-line 138 | 139 | pathWithHeights.push({ 140 | threeMesh: mesh, 141 | height: h 142 | }); 143 | } 144 | } 145 | geoObj[obj].paths = pathWithHeights; 146 | } 147 | 148 | const geoGroup = this.getThreeGroup(geoObj); 149 | geoGroup.translateX(-100 / ((-15 + store.getState().zoom) * 2)); //* 2); 150 | geoGroup.translateY(100 / ((-15 + store.getState().zoom) * 2)); //* 2); 151 | return geoGroup; 152 | } 153 | 154 | getThreeGroup(geoGroup) { // eslint-disable-line 155 | const geoObjectsGroup = new THREE.Group(); 156 | geoObjectsGroup.name = 'geoObjectsGroup'; 157 | 158 | for (const feature of Object.keys(geoGroup)) { 159 | const color = geoGroup[feature]['color'] || new THREE.Color('#5c5c5c'); // eslint-disable-line 160 | const material = new THREE.MeshLambertMaterial({ color }); 161 | for (const meshPath of geoGroup[feature].paths) { 162 | for (const eachMesh of meshPath.threeMesh) { 163 | const shape3d = eachMesh.extrude({ 164 | amount: meshPath.height, 165 | bevelEnabled: false 166 | }); 167 | geoObjectsGroup.add(new THREE.Mesh(shape3d, material)); 168 | } 169 | } 170 | } 171 | return geoObjectsGroup; 172 | } 173 | 174 | 175 | enableDownloadLink() { 176 | const buildingObj = this.exportToObj(); 177 | const exportA = document.getElementById('exportA'); 178 | exportA.className = ''; 179 | exportA.download = `tile-${store.getState().tileLon}-${store.getState().tileLat}-${store.getState().zoom}.obj`; 180 | 181 | const blob = new Blob([buildingObj], { type: 'text' }); 182 | const url = URL.createObjectURL(blob); 183 | exportA.href = url; 184 | } 185 | 186 | exportToObj() { 187 | const result = this.objExporter.parse(this.basicScene.getScene); 188 | return result; 189 | } 190 | 191 | } 192 | 193 | export default TileExporter; 194 | -------------------------------------------------------------------------------- /app/libs/Triangulation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Found on https://github.com/Wilt/three.js_triangulation 4 | * Solves the problem of triangulation found in ThreeJS 5 | * Possible that one of these solution will be integrated 6 | * in the future with ThreeJS. 7 | * See this issue: https://github.com/mrdoob/three.js/issues/5959 8 | */ 9 | 10 | // if ( ! window.THREE ) throw new Error( 'ERROR: three.js not loaded' ); 11 | import THREE from 'three' 12 | import earcut from 'earcut' 13 | 14 | THREE.Triangulation = ( function() { 15 | 16 | var timer = { 17 | 18 | enabled: false, 19 | start: null, 20 | end: null 21 | 22 | }; 23 | 24 | var library = 'original'; 25 | 26 | /** 27 | * Names of the supported libraries 28 | * 29 | * @type {{original: string, earcut: string, poly2tri: string, libtess: string}} 30 | */ 31 | var libraries = { 32 | original: 'original', 33 | earcut: 'earcut', 34 | poly2tri: 'poly2tri', 35 | libtess: 'libtess' 36 | }; 37 | 38 | /** 39 | * Container object for holding the different library adapters 40 | * 41 | * @type {{}} 42 | */ 43 | var adapters = {}; 44 | 45 | adapters[ libraries.original ] = { 46 | 47 | triangulate: THREE.ShapeUtils.triangulate, 48 | 49 | triangulateShape: THREE.ShapeUtils.triangulateShape 50 | 51 | }; 52 | 53 | adapters[ libraries.earcut ] = { 54 | 55 | triangulateShape: function( contour, holes ) { 56 | 57 | var i, il, dim = 2, array; 58 | var holeIndices = []; 59 | var points = []; 60 | 61 | addPoints( contour ); 62 | 63 | for ( i = 0, il = holes.length; i < il; i ++ ) { 64 | 65 | holeIndices.push( points.length / dim ); 66 | 67 | addPoints( holes[ i ] ); 68 | 69 | } 70 | 71 | array = earcut( points, holeIndices, dim ); 72 | 73 | var result = []; 74 | 75 | for ( i = 0, il = array.length; i < il; i += 3 ) { 76 | 77 | result.push( array.slice( i, i + 3 ) ); 78 | 79 | } 80 | 81 | return result; 82 | 83 | function addPoints( a ) { 84 | 85 | var i, il = a.length; 86 | 87 | for ( i = 0; i < il; i ++ ) { 88 | 89 | points.push( a[ i ].x, a[ i ].y ); 90 | 91 | } 92 | 93 | } 94 | 95 | } 96 | 97 | }; 98 | 99 | adapters[ libraries.poly2tri ] = { 100 | 101 | triangulateShape: function( contour, holes ) { 102 | 103 | var i, il, object, sweepContext, triangles; 104 | var pointMap = {}, count = 0; 105 | 106 | points = makePoints( contour ); 107 | 108 | sweepContext = new poly2tri.SweepContext( points ); 109 | 110 | for ( i = 0, il = holes.length; i < il; i ++ ) { 111 | 112 | points = makePoints( holes[ i ] ); 113 | 114 | sweepContext.addHole( points ); 115 | 116 | points = points.concat( points ); 117 | 118 | } 119 | 120 | object = sweepContext.triangulate(); 121 | 122 | triangles = object.triangles_; 123 | 124 | var a, b, c, points, result = []; 125 | 126 | for ( i = 0, il = triangles.length; i < il; i ++ ) { 127 | 128 | points = triangles[ i ].points_; 129 | 130 | a = pointMap[ points[ 0 ].x + ',' + points[ 0 ].y ]; 131 | b = pointMap[ points[ 1 ].x + ',' + points[ 1 ].y ]; 132 | c = pointMap[ points[ 2 ].x + ',' + points[ 2 ].y ]; 133 | 134 | result.push( [ a, b, c ] ); 135 | 136 | } 137 | 138 | return result; 139 | 140 | function makePoints( a ) { 141 | 142 | var i, il = a.length, 143 | points = []; 144 | 145 | for ( i = 0; i < il; i ++ ) { 146 | 147 | points.push( new poly2tri.Point( a[ i ].x, a[ i ].y ) ); 148 | pointMap[ a[ i ].x + ',' + a[ i ].y ] = count; 149 | count ++; 150 | 151 | } 152 | 153 | return points; 154 | 155 | } 156 | 157 | } 158 | 159 | }; 160 | 161 | adapters[ libraries.libtess ] = { 162 | 163 | triangulateShape: function( contour, holes ) { 164 | 165 | var i, il, triangles = []; 166 | var pointMap = {}, count = 0; 167 | 168 | // libtess will take 3d verts and flatten to a plane for tesselation 169 | // since only doing 2d tesselation here, provide z=1 normal to skip 170 | // iterating over verts only to get the same answer. 171 | // comment out to test normal-generation code 172 | tessy.gluTessNormal( 0, 0, 1 ); 173 | 174 | tessy.gluTessBeginPolygon( triangles ); 175 | 176 | points = makePoints( contour ); 177 | 178 | for ( i = 0, il = holes.length; i < il; i ++ ) { 179 | 180 | points = makePoints( holes[ i ] ); 181 | 182 | } 183 | 184 | tessy.gluTessEndPolygon(); 185 | 186 | var a, b, c, points, result = []; 187 | 188 | for ( i = 0, il = triangles.length; i < il; i += 6 ) { 189 | 190 | a = pointMap[ triangles[ i ] + ',' + triangles[ i + 1 ]]; 191 | b = pointMap[ triangles[ i + 2 ] + ',' + triangles[ i + 3 ]]; 192 | c = pointMap[ triangles[ i + 4 ] + ',' + triangles[ i + 5 ]]; 193 | 194 | result.push( [ a, b, c ] ); 195 | 196 | } 197 | 198 | return result; 199 | 200 | function makePoints( a ) { 201 | 202 | var i, il = a.length, 203 | coordinates; 204 | 205 | tessy.gluTessBeginContour(); 206 | 207 | for ( i = 0; i < il; i ++ ) { 208 | 209 | coordinates = [ a[ i ].x, a[ i ].y, 0 ]; 210 | tessy.gluTessVertex( coordinates, coordinates ); 211 | pointMap[ a[ i ].x + ',' + a[ i ].y ] = count; 212 | count ++; 213 | 214 | } 215 | 216 | tessy.gluTessEndContour(); 217 | 218 | return points; 219 | 220 | } 221 | 222 | } 223 | 224 | }; 225 | 226 | /** 227 | * Initialize the library by attaching the triangulation methods to the three.js API 228 | */ 229 | function init() { 230 | 231 | checkDependencies( library ); 232 | 233 | if ( timer.enabled ) { 234 | 235 | THREE.ShapeUtils.triangulate = function() { 236 | 237 | return adapters[ library ].triangulate.apply( this, arguments ); 238 | 239 | }; 240 | 241 | THREE.ShapeUtils.triangulateShape = function() { 242 | 243 | timer.start = Date.now(); 244 | 245 | var result = adapters[ library ].triangulateShape.apply( this, arguments ); 246 | 247 | timer.end = Date.now(); 248 | 249 | console.log( "%c " + library + ": " + ( timer.end - timer.start ) + "ms", 'color: #0000ff'); 250 | 251 | return result; 252 | 253 | }; 254 | 255 | } else { 256 | 257 | THREE.ShapeUtils.triangulate = adapters[ library ].triangulate; 258 | 259 | THREE.ShapeUtils.triangulateShape = adapters[ library ].triangulateShape; 260 | 261 | } 262 | 263 | } 264 | 265 | /** 266 | * Checks dependencies needed for the current library 267 | * 268 | * @param library 269 | */ 270 | function checkDependencies( library ) { 271 | 272 | switch ( library ) { 273 | 274 | case libraries.earcut: 275 | 276 | // if ( ! window.earcut ) throw new Error( 'ERROR: earcut not loaded' ); 277 | 278 | break; 279 | 280 | case libraries.poly2tri: 281 | 282 | if ( ! window.poly2tri ) throw new Error( 'ERROR: poly2tri not loaded' ); 283 | 284 | break; 285 | 286 | case libraries.libtess: 287 | 288 | if ( ! window.tessy ) throw new Error( 'ERROR: libtess not loaded' ); 289 | 290 | break; 291 | 292 | } 293 | 294 | } 295 | 296 | /** 297 | * Set the current triangulation library 298 | * 299 | * @param name 300 | */ 301 | function setLibrary( name ) { 302 | 303 | if ( ! libraries.hasOwnProperty( name ) ) throw new Error( 'ERROR: unknown library ' + name ); 304 | 305 | library = name; 306 | 307 | init(); 308 | 309 | } 310 | 311 | /** 312 | * Set timer for triangulation on/off 313 | * 314 | * @param boolean 315 | */ 316 | function setTimer( boolean ) { 317 | 318 | timer.enabled = boolean; 319 | 320 | init(); 321 | 322 | } 323 | 324 | /** 325 | * Get total time in seconds 326 | * 327 | * @return number 328 | */ 329 | function getTime() { 330 | 331 | if( timer.enabled ){ 332 | 333 | return ( timer.end - timer.start ); 334 | 335 | } 336 | 337 | return false; 338 | 339 | } 340 | 341 | init(); 342 | 343 | return { 344 | 345 | libraries: libraries, 346 | 347 | setTimer: setTimer, 348 | 349 | getTime: getTime, 350 | 351 | setLibrary: setLibrary 352 | 353 | }; 354 | 355 | } )(); 356 | -------------------------------------------------------------------------------- /app/libs/OrbitControl.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three' 2 | /** 3 | * @author qiao / https://github.com/qiao 4 | * @author mrdoob / http://mrdoob.com 5 | * @author alteredq / http://alteredqualia.com/ 6 | * @author WestLangley / http://github.com/WestLangley 7 | * @author erich666 / http://erichaines.com 8 | */ 9 | /*global THREE, console */ 10 | 11 | // This set of controls performs orbiting, dollying (zooming), and panning. It maintains 12 | // the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is 13 | // supported. 14 | // 15 | // Orbit - left mouse / touch: one finger move 16 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 17 | // Pan - right mouse, or arrow keys / touch: three finter swipe 18 | // 19 | // This is a drop-in replacement for (most) TrackballControls used in examples. 20 | // That is, include this js file and wherever you see: 21 | // controls = new THREE.TrackballControls( camera ); 22 | // controls.target.z = 150; 23 | // Simple substitute "OrbitControls" and the control should work as-is. 24 | 25 | THREE.OrbitControls = function ( object, domElement ) { 26 | 27 | this.object = object; 28 | this.domElement = ( domElement !== undefined ) ? domElement : document; 29 | 30 | // API 31 | 32 | // Set to false to disable this control 33 | this.enabled = true; 34 | 35 | // "target" sets the location of focus, where the control orbits around 36 | // and where it pans with respect to. 37 | this.target = new THREE.Vector3(); 38 | 39 | // center is old, deprecated; use "target" instead 40 | this.center = this.target; 41 | 42 | // This option actually enables dollying in and out; left as "zoom" for 43 | // backwards compatibility 44 | this.noZoom = false; 45 | this.zoomSpeed = 1.0; 46 | 47 | // Limits to how far you can dolly in and out 48 | this.minDistance = 0; 49 | this.maxDistance = Infinity; 50 | 51 | // Set to true to disable this control 52 | this.noRotate = false; 53 | this.rotateSpeed = 1.0; 54 | 55 | // Set to true to disable this control 56 | this.noPan = false; 57 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 58 | 59 | // Set to true to automatically rotate around the target 60 | this.autoRotate = false; 61 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 62 | 63 | // How far you can orbit vertically, upper and lower limits. 64 | // Range is 0 to Math.PI radians. 65 | this.minPolarAngle = 0; // radians 66 | this.maxPolarAngle = Math.PI; // radians 67 | 68 | // Set to true to disable use of the keys 69 | this.noKeys = false; 70 | 71 | // The four arrow keys 72 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 73 | 74 | //////////// 75 | // internals 76 | 77 | var scope = this; 78 | 79 | var EPS = 0.000001; 80 | 81 | var rotateStart = new THREE.Vector2(); 82 | var rotateEnd = new THREE.Vector2(); 83 | var rotateDelta = new THREE.Vector2(); 84 | 85 | var panStart = new THREE.Vector2(); 86 | var panEnd = new THREE.Vector2(); 87 | var panDelta = new THREE.Vector2(); 88 | var panOffset = new THREE.Vector3(); 89 | 90 | var offset = new THREE.Vector3(); 91 | 92 | var dollyStart = new THREE.Vector2(); 93 | var dollyEnd = new THREE.Vector2(); 94 | var dollyDelta = new THREE.Vector2(); 95 | 96 | var phiDelta = 0; 97 | var thetaDelta = 0; 98 | var scale = 1; 99 | var pan = new THREE.Vector3(); 100 | 101 | var lastPosition = new THREE.Vector3(); 102 | 103 | var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 104 | 105 | var state = STATE.NONE; 106 | 107 | // for reset 108 | 109 | this.target0 = this.target.clone(); 110 | this.position0 = this.object.position.clone(); 111 | 112 | // so camera.up is the orbit axis 113 | 114 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 115 | var quatInverse = quat.clone().inverse(); 116 | 117 | // events 118 | 119 | var changeEvent = { type: 'change' }; 120 | var startEvent = { type: 'start'}; 121 | var endEvent = { type: 'end'}; 122 | 123 | this.rotateLeft = function ( angle ) { 124 | 125 | if ( angle === undefined ) { 126 | 127 | angle = getAutoRotationAngle(); 128 | 129 | } 130 | 131 | thetaDelta -= angle; 132 | 133 | }; 134 | 135 | this.rotateUp = function ( angle ) { 136 | 137 | if ( angle === undefined ) { 138 | 139 | angle = getAutoRotationAngle(); 140 | 141 | } 142 | 143 | phiDelta -= angle; 144 | 145 | }; 146 | 147 | // pass in distance in world space to move left 148 | this.panLeft = function ( distance ) { 149 | 150 | var te = this.object.matrix.elements; 151 | 152 | // get X column of matrix 153 | panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); 154 | panOffset.multiplyScalar( - distance ); 155 | 156 | pan.add( panOffset ); 157 | 158 | }; 159 | 160 | // pass in distance in world space to move up 161 | this.panUp = function ( distance ) { 162 | 163 | var te = this.object.matrix.elements; 164 | 165 | // get Y column of matrix 166 | panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); 167 | panOffset.multiplyScalar( distance ); 168 | 169 | pan.add( panOffset ); 170 | 171 | }; 172 | 173 | // pass in x,y of change desired in pixel space, 174 | // right and down are positive 175 | this.pan = function ( deltaX, deltaY ) { 176 | 177 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 178 | 179 | if ( scope.object.fov !== undefined ) { 180 | 181 | // perspective 182 | var position = scope.object.position; 183 | var offset = position.clone().sub( scope.target ); 184 | var targetDistance = offset.length(); 185 | 186 | // half of the fov is center to top of screen 187 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 188 | 189 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 190 | scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); 191 | scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); 192 | 193 | } else if ( scope.object.top !== undefined ) { 194 | 195 | // orthographic 196 | scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); 197 | scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); 198 | 199 | } else { 200 | 201 | // camera neither orthographic or perspective 202 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 203 | 204 | } 205 | 206 | }; 207 | 208 | this.dollyIn = function ( dollyScale ) { 209 | 210 | if ( dollyScale === undefined ) { 211 | 212 | dollyScale = getZoomScale(); 213 | 214 | } 215 | 216 | scale /= dollyScale; 217 | 218 | }; 219 | 220 | this.dollyOut = function ( dollyScale ) { 221 | 222 | if ( dollyScale === undefined ) { 223 | 224 | dollyScale = getZoomScale(); 225 | 226 | } 227 | 228 | scale *= dollyScale; 229 | 230 | }; 231 | 232 | this.update = function () { 233 | 234 | var position = this.object.position; 235 | 236 | offset.copy( position ).sub( this.target ); 237 | 238 | // rotate offset to "y-axis-is-up" space 239 | offset.applyQuaternion( quat ); 240 | 241 | // angle from z-axis around y-axis 242 | 243 | var theta = Math.atan2( offset.x, offset.z ); 244 | 245 | // angle from y-axis 246 | 247 | var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); 248 | 249 | if ( this.autoRotate ) { 250 | 251 | this.rotateLeft( getAutoRotationAngle() ); 252 | 253 | } 254 | 255 | theta += thetaDelta; 256 | phi += phiDelta; 257 | 258 | // restrict phi to be between desired limits 259 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); 260 | 261 | // restrict phi to be betwee EPS and PI-EPS 262 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); 263 | 264 | var radius = offset.length() * scale; 265 | 266 | // restrict radius to be between desired limits 267 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); 268 | 269 | // move target to panned location 270 | this.target.add( pan ); 271 | 272 | offset.x = radius * Math.sin( phi ) * Math.sin( theta ); 273 | offset.y = radius * Math.cos( phi ); 274 | offset.z = radius * Math.sin( phi ) * Math.cos( theta ); 275 | 276 | // rotate offset back to "camera-up-vector-is-up" space 277 | offset.applyQuaternion( quatInverse ); 278 | 279 | position.copy( this.target ).add( offset ); 280 | 281 | this.object.lookAt( this.target ); 282 | 283 | thetaDelta = 0; 284 | phiDelta = 0; 285 | scale = 1; 286 | pan.set( 0, 0, 0 ); 287 | 288 | if ( lastPosition.distanceToSquared( this.object.position ) > EPS ) { 289 | 290 | this.dispatchEvent( changeEvent ); 291 | 292 | lastPosition.copy( this.object.position ); 293 | 294 | } 295 | 296 | }; 297 | 298 | 299 | this.reset = function () { 300 | 301 | state = STATE.NONE; 302 | 303 | this.target.copy( this.target0 ); 304 | this.object.position.copy( this.position0 ); 305 | 306 | this.update(); 307 | 308 | }; 309 | 310 | function getAutoRotationAngle() { 311 | 312 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 313 | 314 | } 315 | 316 | function getZoomScale() { 317 | 318 | return Math.pow( 0.95, scope.zoomSpeed ); 319 | 320 | } 321 | 322 | function onMouseDown( event ) { 323 | 324 | if ( scope.enabled === false ) return; 325 | event.preventDefault(); 326 | 327 | if ( event.button === 0 ) { 328 | if ( scope.noRotate === true ) return; 329 | 330 | state = STATE.ROTATE; 331 | 332 | rotateStart.set( event.clientX, event.clientY ); 333 | 334 | } else if ( event.button === 1 ) { 335 | if ( scope.noZoom === true ) return; 336 | 337 | state = STATE.DOLLY; 338 | 339 | dollyStart.set( event.clientX, event.clientY ); 340 | 341 | } else if ( event.button === 2 ) { 342 | if ( scope.noPan === true ) return; 343 | 344 | state = STATE.PAN; 345 | 346 | panStart.set( event.clientX, event.clientY ); 347 | 348 | } 349 | 350 | scope.domElement.addEventListener( 'mousemove', onMouseMove, false ); 351 | scope.domElement.addEventListener( 'mouseup', onMouseUp, false ); 352 | scope.dispatchEvent( startEvent ); 353 | 354 | } 355 | 356 | function onMouseMove( event ) { 357 | 358 | if ( scope.enabled === false ) return; 359 | 360 | event.preventDefault(); 361 | 362 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 363 | 364 | if ( state === STATE.ROTATE ) { 365 | 366 | if ( scope.noRotate === true ) return; 367 | 368 | rotateEnd.set( event.clientX, event.clientY ); 369 | rotateDelta.subVectors( rotateEnd, rotateStart ); 370 | 371 | // rotating across whole screen goes 360 degrees around 372 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 373 | 374 | // rotating up and down along whole screen attempts to go 360, but limited to 180 375 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 376 | 377 | rotateStart.copy( rotateEnd ); 378 | 379 | } else if ( state === STATE.DOLLY ) { 380 | 381 | if ( scope.noZoom === true ) return; 382 | 383 | dollyEnd.set( event.clientX, event.clientY ); 384 | dollyDelta.subVectors( dollyEnd, dollyStart ); 385 | 386 | if ( dollyDelta.y > 0 ) { 387 | 388 | scope.dollyIn(); 389 | 390 | } else { 391 | 392 | scope.dollyOut(); 393 | 394 | } 395 | 396 | dollyStart.copy( dollyEnd ); 397 | 398 | } else if ( state === STATE.PAN ) { 399 | 400 | if ( scope.noPan === true ) return; 401 | 402 | panEnd.set( event.clientX, event.clientY ); 403 | panDelta.subVectors( panEnd, panStart ); 404 | 405 | scope.pan( panDelta.x, panDelta.y ); 406 | 407 | panStart.copy( panEnd ); 408 | 409 | } 410 | 411 | scope.update(); 412 | 413 | } 414 | 415 | function onMouseUp( /* event */ ) { 416 | 417 | if ( scope.enabled === false ) return; 418 | 419 | scope.domElement.removeEventListener( 'mousemove', onMouseMove, false ); 420 | scope.domElement.removeEventListener( 'mouseup', onMouseUp, false ); 421 | scope.dispatchEvent( endEvent ); 422 | state = STATE.NONE; 423 | 424 | } 425 | 426 | function onMouseWheel( event ) { 427 | 428 | if ( scope.enabled === false || scope.noZoom === true ) return; 429 | 430 | event.preventDefault(); 431 | event.stopPropagation(); 432 | 433 | var delta = 0; 434 | 435 | if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 436 | 437 | delta = event.wheelDelta; 438 | 439 | } else if ( event.detail !== undefined ) { // Firefox 440 | 441 | delta = - event.detail; 442 | 443 | } 444 | 445 | if ( delta > 0 ) { 446 | 447 | scope.dollyOut(); 448 | 449 | } else { 450 | 451 | scope.dollyIn(); 452 | 453 | } 454 | 455 | scope.update(); 456 | scope.dispatchEvent( startEvent ); 457 | scope.dispatchEvent( endEvent ); 458 | 459 | } 460 | 461 | function onKeyDown( event ) { 462 | 463 | if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; 464 | 465 | switch ( event.keyCode ) { 466 | 467 | case scope.keys.UP: 468 | scope.pan( 0, scope.keyPanSpeed ); 469 | scope.update(); 470 | break; 471 | 472 | case scope.keys.BOTTOM: 473 | scope.pan( 0, - scope.keyPanSpeed ); 474 | scope.update(); 475 | break; 476 | 477 | case scope.keys.LEFT: 478 | scope.pan( scope.keyPanSpeed, 0 ); 479 | scope.update(); 480 | break; 481 | 482 | case scope.keys.RIGHT: 483 | scope.pan( - scope.keyPanSpeed, 0 ); 484 | scope.update(); 485 | break; 486 | 487 | } 488 | 489 | } 490 | 491 | function touchstart( event ) { 492 | 493 | if ( scope.enabled === false ) return; 494 | 495 | switch ( event.touches.length ) { 496 | 497 | case 1: // one-fingered touch: rotate 498 | 499 | if ( scope.noRotate === true ) return; 500 | 501 | state = STATE.TOUCH_ROTATE; 502 | 503 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 504 | break; 505 | 506 | case 2: // two-fingered touch: dolly 507 | 508 | if ( scope.noZoom === true ) return; 509 | 510 | state = STATE.TOUCH_DOLLY; 511 | 512 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 513 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 514 | var distance = Math.sqrt( dx * dx + dy * dy ); 515 | dollyStart.set( 0, distance ); 516 | break; 517 | 518 | case 3: // three-fingered touch: pan 519 | 520 | if ( scope.noPan === true ) return; 521 | 522 | state = STATE.TOUCH_PAN; 523 | 524 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 525 | break; 526 | 527 | default: 528 | 529 | state = STATE.NONE; 530 | 531 | } 532 | 533 | scope.dispatchEvent( startEvent ); 534 | 535 | } 536 | 537 | function touchmove( event ) { 538 | 539 | if ( scope.enabled === false ) return; 540 | 541 | event.preventDefault(); 542 | event.stopPropagation(); 543 | 544 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 545 | 546 | switch ( event.touches.length ) { 547 | 548 | case 1: // one-fingered touch: rotate 549 | 550 | if ( scope.noRotate === true ) return; 551 | if ( state !== STATE.TOUCH_ROTATE ) return; 552 | 553 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 554 | rotateDelta.subVectors( rotateEnd, rotateStart ); 555 | 556 | // rotating across whole screen goes 360 degrees around 557 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 558 | // rotating up and down along whole screen attempts to go 360, but limited to 180 559 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 560 | 561 | rotateStart.copy( rotateEnd ); 562 | 563 | scope.update(); 564 | break; 565 | 566 | case 2: // two-fingered touch: dolly 567 | 568 | if ( scope.noZoom === true ) return; 569 | if ( state !== STATE.TOUCH_DOLLY ) return; 570 | 571 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 572 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 573 | var distance = Math.sqrt( dx * dx + dy * dy ); 574 | 575 | dollyEnd.set( 0, distance ); 576 | dollyDelta.subVectors( dollyEnd, dollyStart ); 577 | 578 | if ( dollyDelta.y > 0 ) { 579 | 580 | scope.dollyOut(); 581 | 582 | } else { 583 | 584 | scope.dollyIn(); 585 | 586 | } 587 | 588 | dollyStart.copy( dollyEnd ); 589 | 590 | scope.update(); 591 | break; 592 | 593 | case 3: // three-fingered touch: pan 594 | 595 | if ( scope.noPan === true ) return; 596 | if ( state !== STATE.TOUCH_PAN ) return; 597 | 598 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 599 | panDelta.subVectors( panEnd, panStart ); 600 | 601 | scope.pan( panDelta.x, panDelta.y ); 602 | 603 | panStart.copy( panEnd ); 604 | 605 | scope.update(); 606 | break; 607 | 608 | default: 609 | 610 | state = STATE.NONE; 611 | 612 | } 613 | 614 | } 615 | 616 | function touchend( /* event */ ) { 617 | 618 | if ( scope.enabled === false ) return; 619 | 620 | scope.dispatchEvent( endEvent ); 621 | state = STATE.NONE; 622 | 623 | } 624 | 625 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 626 | this.domElement.addEventListener( 'mousedown', onMouseDown, false ); 627 | this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); 628 | this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox 629 | 630 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 631 | this.domElement.addEventListener( 'touchend', touchend, false ); 632 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 633 | 634 | window.addEventListener( 'keydown', onKeyDown, false ); 635 | 636 | // force an update at start 637 | this.update(); 638 | 639 | }; 640 | 641 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 642 | 643 | module.exports = THREE.OrbitControls; 644 | -------------------------------------------------------------------------------- /app/libs/D3-Three.js: -------------------------------------------------------------------------------- 1 | import D3 from 'd3' 2 | import THREE from 'three' 3 | 4 | /* This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 6 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 7 | 8 | function d3threeD(exports) { 9 | 10 | const DEGS_TO_RADS = Math.PI / 180, 11 | UNIT_SIZE = 1; 12 | 13 | const DIGIT_0 = 48, DIGIT_9 = 57, COMMA = 44, SPACE = 32, PERIOD = 46, 14 | MINUS = 45; 15 | 16 | function transformSVGPath(pathStr) { 17 | 18 | var paths = []; 19 | var path = new THREE.Shape(); 20 | 21 | var idx = 1, len = pathStr.length, activeCmd, 22 | x = 0, y = 0, nx = 0, ny = 0, firstX = null, firstY = null, 23 | x1 = 0, x2 = 0, y1 = 0, y2 = 0, 24 | rx = 0, ry = 0, xar = 0, laf = 0, sf = 0, cx, cy; 25 | 26 | function eatNum() { 27 | var sidx, c, isFloat = false, s; 28 | // eat delims 29 | while (idx < len) { 30 | c = pathStr.charCodeAt(idx); 31 | if (c !== COMMA && c !== SPACE) 32 | break; 33 | idx++; 34 | } 35 | if (c === MINUS) 36 | sidx = idx++; 37 | else 38 | sidx = idx; 39 | // eat number 40 | while (idx < len) { 41 | c = pathStr.charCodeAt(idx); 42 | if (DIGIT_0 <= c && c <= DIGIT_9) { 43 | idx++; 44 | continue; 45 | } 46 | else if (c === PERIOD) { 47 | idx++; 48 | isFloat = true; 49 | continue; 50 | } 51 | 52 | s = pathStr.substring(sidx, idx); 53 | return isFloat ? parseFloat(s) : parseInt(s); 54 | } 55 | 56 | s = pathStr.substring(sidx); 57 | return isFloat ? parseFloat(s) : parseInt(s); 58 | } 59 | 60 | function nextIsNum() { 61 | var c; 62 | // do permanently eat any delims... 63 | while (idx < len) { 64 | c = pathStr.charCodeAt(idx); 65 | if (c !== COMMA && c !== SPACE) 66 | break; 67 | idx++; 68 | } 69 | c = pathStr.charCodeAt(idx); 70 | return (c === MINUS || (DIGIT_0 <= c && c <= DIGIT_9)); 71 | } 72 | 73 | var canRepeat; 74 | var enteredSub = false; 75 | var zSeen = false; 76 | activeCmd = pathStr[0]; 77 | 78 | while (idx <= len) { 79 | canRepeat = true; 80 | switch (activeCmd) { 81 | // moveto commands, become lineto's if repeated 82 | case 'M': 83 | enteredSub = false; 84 | x = eatNum(); 85 | y = eatNum(); 86 | path.moveTo(x, y); 87 | activeCmd = 'L'; 88 | break; 89 | case 'm': 90 | x += eatNum(); 91 | y += eatNum(); 92 | path.moveTo(x, y); 93 | activeCmd = 'l'; 94 | break; 95 | case 'Z': 96 | case 'z': 97 | // z is a special case. This ends a segment and starts 98 | // a new path. Since the three.js path is continuous 99 | // we should start a new path here. This also draws a 100 | // line from the current location to the start location. 101 | canRepeat = false; 102 | if (x !== firstX || y !== firstY) 103 | path.lineTo(firstX, firstY); 104 | 105 | paths.push(path); 106 | 107 | // reset the elements 108 | firstX = null; 109 | firstY = null; 110 | 111 | // avoid x,y being set incorrectly 112 | enteredSub = true; 113 | 114 | path = new THREE.Shape(); 115 | 116 | zSeen = true; 117 | 118 | break; 119 | // - lines! 120 | case 'L': 121 | case 'H': 122 | case 'V': 123 | nx = (activeCmd === 'V') ? x : eatNum(); 124 | ny = (activeCmd === 'H') ? y : eatNum(); 125 | path.lineTo(nx, ny); 126 | x = nx; 127 | y = ny; 128 | break; 129 | case 'l': 130 | case 'h': 131 | case 'v': 132 | nx = (activeCmd === 'v') ? x : (x + eatNum()); 133 | ny = (activeCmd === 'h') ? y : (y + eatNum()); 134 | path.lineTo(nx, ny); 135 | x = nx; 136 | y = ny; 137 | break; 138 | // - cubic bezier 139 | case 'C': 140 | x1 = eatNum(); y1 = eatNum(); 141 | case 'S': 142 | if (activeCmd === 'S') { 143 | x1 = 2 * x - x2; y1 = 2 * y - y2; 144 | } 145 | x2 = eatNum(); 146 | y2 = eatNum(); 147 | nx = eatNum(); 148 | ny = eatNum(); 149 | path.bezierCurveTo(x1, y1, x2, y2, nx, ny); 150 | x = nx; y = ny; 151 | break; 152 | case 'c': 153 | x1 = x + eatNum(); 154 | y1 = y + eatNum(); 155 | case 's': 156 | if (activeCmd === 's') { 157 | x1 = 2 * x - x2; 158 | y1 = 2 * y - y2; 159 | } 160 | x2 = x + eatNum(); 161 | y2 = y + eatNum(); 162 | nx = x + eatNum(); 163 | ny = y + eatNum(); 164 | path.bezierCurveTo(x1, y1, x2, y2, nx, ny); 165 | x = nx; y = ny; 166 | break; 167 | // - quadratic bezier 168 | case 'Q': 169 | x1 = eatNum(); y1 = eatNum(); 170 | case 'T': 171 | if (activeCmd === 'T') { 172 | x1 = 2 * x - x1; 173 | y1 = 2 * y - y1; 174 | } 175 | nx = eatNum(); 176 | ny = eatNum(); 177 | path.quadraticCurveTo(x1, y1, nx, ny); 178 | x = nx; 179 | y = ny; 180 | break; 181 | case 'q': 182 | x1 = x + eatNum(); 183 | y1 = y + eatNum(); 184 | case 't': 185 | if (activeCmd === 't') { 186 | x1 = 2 * x - x1; 187 | y1 = 2 * y - y1; 188 | } 189 | nx = x + eatNum(); 190 | ny = y + eatNum(); 191 | path.quadraticCurveTo(x1, y1, nx, ny); 192 | x = nx; y = ny; 193 | break; 194 | // - elliptical arc 195 | case 'A': 196 | rx = eatNum(); 197 | ry = eatNum(); 198 | xar = eatNum() * DEGS_TO_RADS; 199 | laf = eatNum(); 200 | sf = eatNum(); 201 | nx = eatNum(); 202 | ny = eatNum(); 203 | if (rx !== ry) { 204 | console.warn("Forcing elliptical arc to be a circular one :(", 205 | rx, ry); 206 | } 207 | // SVG implementation notes does all the math for us! woo! 208 | // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes 209 | // step1, using x1 as x1' 210 | x1 = Math.cos(xar) * (x - nx) / 2 + Math.sin(xar) * (y - ny) / 2; 211 | y1 = -Math.sin(xar) * (x - nx) / 2 + Math.cos(xar) * (y - ny) / 2; 212 | // step 2, using x2 as cx' 213 | var norm = Math.sqrt( 214 | (rx*rx * ry*ry - rx*rx * y1*y1 - ry*ry * x1*x1) / 215 | (rx*rx * y1*y1 + ry*ry * x1*x1)); 216 | if (laf === sf) 217 | norm = -norm; 218 | x2 = norm * rx * y1 / ry; 219 | y2 = norm * -ry * x1 / rx; 220 | // step 3 221 | cx = Math.cos(xar) * x2 - Math.sin(xar) * y2 + (x + nx) / 2; 222 | cy = Math.sin(xar) * x2 + Math.cos(xar) * y2 + (y + ny) / 2; 223 | 224 | var u = new THREE.Vector2(1, 0), 225 | v = new THREE.Vector2((x1 - x2) / rx, 226 | (y1 - y2) / ry); 227 | var startAng = Math.acos(u.dot(v) / u.length() / v.length()); 228 | if (u.x * v.y - u.y * v.x < 0) 229 | startAng = -startAng; 230 | 231 | // we can reuse 'v' from start angle as our 'u' for delta angle 232 | u.x = (-x1 - x2) / rx; 233 | u.y = (-y1 - y2) / ry; 234 | 235 | var deltaAng = Math.acos(v.dot(u) / v.length() / u.length()); 236 | // This normalization ends up making our curves fail to triangulate... 237 | if (v.x * u.y - v.y * u.x < 0) 238 | deltaAng = -deltaAng; 239 | if (!sf && deltaAng > 0) 240 | deltaAng -= Math.PI * 2; 241 | if (sf && deltaAng < 0) 242 | deltaAng += Math.PI * 2; 243 | 244 | path.absarc(cx, cy, rx, startAng, startAng + deltaAng, sf); 245 | x = nx; 246 | y = ny; 247 | break; 248 | 249 | case ' ': 250 | // if it's an empty space, just skip it, and see if we can find a real command 251 | break; 252 | 253 | default: 254 | throw new Error("weird path command: " + activeCmd); 255 | } 256 | if (firstX === null && !enteredSub) { 257 | firstX = x; 258 | firstY = y; 259 | } 260 | 261 | // just reissue the command 262 | if (canRepeat && nextIsNum()) 263 | continue; 264 | activeCmd = pathStr[idx++]; 265 | } 266 | 267 | if (zSeen) { 268 | return paths; 269 | } else { 270 | paths.push(path); 271 | return paths; 272 | } 273 | } 274 | 275 | function applySVGTransform(obj, tstr) { 276 | 277 | 278 | var idx = tstr.indexOf('('), len = tstr.length, 279 | cmd = tstr.substring(0, idx++); 280 | function eatNum() { 281 | var sidx, c, isFloat = false, s; 282 | // eat delims 283 | while (idx < len) { 284 | c = tstr.charCodeAt(idx); 285 | if (c !== COMMA && c !== SPACE) 286 | break; 287 | idx++; 288 | } 289 | if (c === MINUS) 290 | sidx = idx++; 291 | else 292 | sidx = idx; 293 | // eat number 294 | while (idx < len) { 295 | c = tstr.charCodeAt(idx); 296 | if (DIGIT_0 <= c && c <= DIGIT_9) { 297 | idx++; 298 | continue; 299 | } 300 | else if (c === PERIOD) { 301 | idx++; 302 | isFloat = true; 303 | continue; 304 | } 305 | 306 | s = tstr.substring(sidx, idx); 307 | return isFloat ? parseFloat(s) : parseInt(s); 308 | } 309 | 310 | s = tstr.substring(sidx); 311 | return isFloat ? parseFloat(s) : parseInt(s); 312 | } 313 | switch (cmd) { 314 | case 'translate': 315 | obj.position.x = Math.floor(eatNum() * UNIT_SIZE); 316 | obj.position.y = Math.floor(eatNum() * UNIT_SIZE); 317 | break; 318 | case 'scale': 319 | obj.scale.x = Math.floor(eatNum() * UNIT_SIZE); 320 | obj.scale.y = Math.floor(eatNum() * UNIT_SIZE); 321 | break; 322 | default: 323 | console.warn("don't understand transform", tstr); 324 | break; 325 | } 326 | } 327 | 328 | 329 | var extrudeDefaults = { 330 | amount: 20, 331 | bevelEnabled: true, 332 | material: 0, 333 | extrudeMaterial: 0, 334 | }; 335 | 336 | 337 | 338 | 339 | 340 | function commonSetAttribute(name, value) { 341 | switch (name) { 342 | case 'x': 343 | this.position.x = Math.floor(value * UNIT_SIZE); 344 | break; 345 | 346 | case 'y': 347 | this.position.y = Math.floor(value * UNIT_SIZE); 348 | break; 349 | 350 | case 'class': 351 | this.clazz = value; 352 | break; 353 | 354 | case 'stroke': 355 | case 'fill': 356 | if (typeof(value) !== 'string') 357 | value = value.toString(); 358 | this.material.color.setHex(parseInt(value.substring(1), 16)); 359 | break; 360 | 361 | case 'transform': 362 | applySVGTransform(this, value); 363 | break; 364 | 365 | case 'd': 366 | var shape = transformSVGPath(value), 367 | geom = shape.extrude(extrudeDefaults); 368 | this.geometry = geom; 369 | this.geometry.boundingSphere = {radius: 3 * UNIT_SIZE}; 370 | this.scale.set(UNIT_SIZE, UNIT_SIZE, UNIT_SIZE); 371 | 372 | break; 373 | 374 | default: 375 | throw new Error("no setter for: " + name); 376 | } 377 | } 378 | function commonSetAttributeNS(namespace, name, value) { 379 | this.setAttribute(name, value); 380 | } 381 | 382 | function Group(parentThing) { 383 | THREE.Object3D.call(this); 384 | 385 | this.d3class = ''; 386 | 387 | parentThing.add(this); 388 | }; 389 | Group.prototype = new THREE.Object3D(); 390 | Group.prototype.constructor = Group; 391 | Group.prototype.d3tag = 'g'; 392 | Group.prototype.setAttribute = commonSetAttribute; 393 | Group.prototype.setAttributeNS = commonSetAttributeNS; 394 | 395 | function fabGroup() { 396 | return new Group(this); 397 | } 398 | 399 | function Mesh(parentThing, tag, geometry, material) { 400 | THREE.Mesh.call(this, geometry, material); 401 | 402 | this.d3tag = tag; 403 | this.d3class = ''; 404 | 405 | parentThing.add(this); 406 | } 407 | Mesh.prototype = new THREE.Mesh(); 408 | Mesh.prototype.constructor = Mesh; 409 | Mesh.prototype.setAttribute = commonSetAttribute; 410 | Mesh.prototype.setAttributeNS = commonSetAttributeNS; 411 | 412 | 413 | const SPHERE_SEGS = 16, SPHERE_RINGS = 16, 414 | DEFAULT_COLOR = 0xcc0000; 415 | 416 | var sharedSphereGeom = null, 417 | sharedCubeGeom = null; 418 | 419 | function fabSphere() { 420 | if (!sharedSphereGeom) 421 | sharedSphereGeom = new THREE.SphereGeometry( 422 | UNIT_SIZE / 2, SPHERE_SEGS, SPHERE_RINGS); 423 | var material = new THREE.MeshLambertMaterial({ 424 | color: DEFAULT_COLOR, 425 | }); 426 | return new Mesh(this, 'sphere', sharedSphereGeom, material); 427 | } 428 | 429 | function fabCube() { 430 | if (!sharedCubeGeom) 431 | sharedCubeGeom = new THREE.CubeGeometry(UNIT_SIZE, UNIT_SIZE, UNIT_SIZE); 432 | var material = new THREE.MeshLambertMaterial({ 433 | color: DEFAULT_COLOR, 434 | }); 435 | return new Mesh(this, 'cube', sharedCubeGeom, material); 436 | } 437 | 438 | function fabPath() { 439 | // start with a cube that we will replace with the path once it gets created 440 | if (!sharedCubeGeom) 441 | sharedCubeGeom = new THREE.CubeGeometry(UNIT_SIZE, UNIT_SIZE, UNIT_SIZE); 442 | var material = new THREE.MeshLambertMaterial({ 443 | color: DEFAULT_COLOR, 444 | }); 445 | return new Mesh(this, 'path', sharedCubeGeom, material); 446 | } 447 | 448 | function Scene() { 449 | THREE.Scene.call(this); 450 | this.renderer = null; 451 | this.camera = null; 452 | this.controls = null; 453 | this._d3_width = null; 454 | this._d3_height = null; 455 | } 456 | Scene.prototype = new THREE.Scene(); 457 | Scene.prototype.constructor = Scene; 458 | Scene.prototype._setBounds = function() { 459 | this.renderer.setSize(this._d3_width, this._d3_height); 460 | var aspect = this.camera.aspect; 461 | this.camera.position.set( 462 | this._d3_width * UNIT_SIZE / 2, 463 | this._d3_height * UNIT_SIZE / 2, 464 | Math.max(this._d3_width * UNIT_SIZE / Math.sqrt(2), 465 | this._d3_height * UNIT_SIZE / Math.sqrt(2))); 466 | this.controls.target.set(this.camera.position.x, this.camera.position.y, 0); 467 | console.log("camera:", this.camera.position.x, this.camera.position.y, 468 | this.camera.position.z); 469 | 470 | 471 | 472 | //this.camera.position.z = 1000; 473 | }; 474 | Scene.prototype.setAttribute = function(name, value) { 475 | switch (name) { 476 | case 'width': 477 | this._d3_width = value; 478 | if (this._d3_height) 479 | this._setBounds(); 480 | break; 481 | case 'height': 482 | this._d3_height = value; 483 | if (this._d3_width) 484 | this._setBounds(); 485 | break; 486 | } 487 | }; 488 | 489 | 490 | 491 | function fabVis() { 492 | var camera, scene, controls, renderer; 493 | 494 | // - scene 495 | scene = new Scene(); 496 | threeJsScene = scene; 497 | 498 | // - camera 499 | camera = scene.camera = new THREE.PerspectiveCamera( 500 | 75, 501 | window.innerWidth / window.innerHeight, 502 | 1, 100000); 503 | /* 504 | camera = scene.camera = new THREE.OrthographicCamera( 505 | window.innerWidth / -2, window.innerWidth / 2, 506 | window.innerHeight / 2, window.innerHeight / -2, 507 | 1, 50000); 508 | */ 509 | scene.add(camera); 510 | 511 | // - controls 512 | // from misc_camera_trackball.html example 513 | controls = scene.controls = new THREE.TrackballControls(camera); 514 | controls.rotateSpeed = 1.0; 515 | controls.zoomSpeed = 1.2; 516 | controls.panSpeed = 0.8; 517 | 518 | controls.noZoom = false; 519 | controls.noPan = false; 520 | 521 | controls.staticMoving = true; 522 | controls.dynamicDampingFactor = 0.3; 523 | 524 | controls.keys = [65, 83, 68]; 525 | 526 | controls.addEventListener('change', render); 527 | 528 | // - light 529 | /* 530 | var pointLight = new THREE.PointLight(0xFFFFFF); 531 | pointLight.position.set(10, 50, 130); 532 | scene.add(pointLight); 533 | */ 534 | 535 | var spotlight = new THREE.SpotLight(0xffffff); 536 | spotlight.position.set(-50000, 50000, 100000); 537 | scene.add(spotlight); 538 | 539 | var backlight = new THREE.SpotLight(0x888888); 540 | backlight.position.set(50000, -50000, -100000); 541 | scene.add(backlight); 542 | 543 | /* 544 | var ambientLight = new THREE.AmbientLight(0x888888); 545 | scene.add(ambientLight); 546 | */ 547 | 548 | function helperPlanes(maxBound) { 549 | var geom = new THREE.PlaneGeometry(maxBound, maxBound, 4, 4); 550 | for (var i = 0; i < 4; i++) { 551 | var color, cx, cy; 552 | switch (i) { 553 | case 0: 554 | color = 0xff0000; 555 | cx = maxBound / 2; 556 | cy = maxBound / 2; 557 | break; 558 | case 1: 559 | color = 0x00ff00; 560 | cx = maxBound / 2; 561 | cy = -maxBound / 2; 562 | break; 563 | case 2: 564 | color = 0x0000ff; 565 | cx = -maxBound / 2; 566 | cy = -maxBound / 2; 567 | break; 568 | case 3: 569 | color = 0xffff00; 570 | cx = -maxBound / 2; 571 | cy = maxBound / 2; 572 | break; 573 | } 574 | var material = new THREE.MeshLambertMaterial({ color: color }); 575 | var mesh = new THREE.Mesh(geom, material); 576 | mesh.position.set(cx, cy, -1); 577 | 578 | scene.add(mesh); 579 | } 580 | } 581 | //helperPlanes(UNIT_SIZE * 225); 582 | 583 | // - renderer 584 | renderer = scene.renderer = new THREE.WebGLRenderer({ 585 | // too slow... 586 | //antialias: true, 587 | }); 588 | this.appendChild( renderer.domElement ); 589 | 590 | // - stats 591 | var stats = new Stats(); 592 | stats.domElement.style.position = 'absolute'; 593 | stats.domElement.style.top = '0px'; 594 | stats.domElement.style.zIndex = 100; 595 | this.appendChild( stats.domElement ); 596 | 597 | function animate() { 598 | requestAnimationFrame(animate, renderer.domElement); 599 | controls.update(); 600 | } 601 | 602 | function render() { 603 | renderer.render(scene, camera); 604 | stats.update(); 605 | } 606 | 607 | animate(); 608 | 609 | return scene; 610 | }; 611 | 612 | 613 | d3.selection.prototype.append3d = function(name) { 614 | var append; 615 | switch (name) { 616 | case 'svg': 617 | append = fabVis; 618 | break; 619 | 620 | case 'g': 621 | append = fabGroup; 622 | break; 623 | 624 | case 'path': 625 | append = fabPath; 626 | break; 627 | 628 | case 'text': 629 | case 'line': 630 | case 'rect': 631 | throw new Error("Did not implement: " + name); 632 | break; 633 | 634 | case 'sphere': 635 | append = fabSphere; 636 | break; 637 | } 638 | 639 | return this.select(append); 640 | }; 641 | d3.selection.enter.prototype.append3d = d3.selection.prototype.append3d; 642 | 643 | function select3d_selector(constraint) { 644 | var tagCheck = null, classCheck = null, 645 | idxPeriod = constraint.indexOf('.'); 646 | if (idxPeriod === -1) { 647 | tagCheck = constraint; 648 | } 649 | else if (idxPeriod === 0) { 650 | classCheck = constraint.substring(1); 651 | } 652 | else { 653 | tagCheck = constraint.substring(0, idxPeriod); 654 | classCheck = constraint.substring(idxPeriod + 1); 655 | } 656 | return function() { 657 | var results = []; 658 | for (var i = 0; i < this.children.length; i++) { 659 | var kid = this.children[i]; 660 | if ((!tagCheck || kid.d3tag === tagCheck) && 661 | (!classCheck || kid.d3class === classCheck)) 662 | results.push(kid); 663 | } 664 | return results; 665 | }; 666 | } 667 | 668 | d3.selection.prototype.select3d = function(constraint) { 669 | return this.select(select3d_selector(constraint)); 670 | }; 671 | d3.selection.prototype.selectAll3d = function(constraint) { 672 | return this.selectAll(select3d_selector(constraint)); 673 | }; 674 | 675 | return { 676 | exportSVG: transformSVGPath 677 | } 678 | 679 | } 680 | 681 | module.exports = d3threeD; --------------------------------------------------------------------------------