├── .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 |
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 | 
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 |
29 |
33 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
64 |
65 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
86 |
87 |
88 |
89 |
90 |
91 |