├── .gitignore ├── .gitmodules ├── Cesium.externs.js ├── LICENSE ├── Leaflet.externs.js ├── Makefile ├── README.md ├── api-debug.json ├── api.json ├── app-debug.json ├── app.json ├── deploy ├── api.html ├── header.js ├── main.css ├── polygon.html ├── stars.jpg └── world512.jpg ├── src-app ├── main.html └── main.js └── src ├── api-l.html ├── api-l.js ├── api.html ├── api.js ├── app.js ├── camera.js ├── cameraanimator.js ├── canvas2image.js ├── custommap.js ├── editablepolygon.js ├── map.js ├── maps.js ├── markers ├── abstractmarker.js ├── basicmarker.js ├── markermanager.js ├── polydragger.js ├── popup.js └── prettymarker.js ├── miniglobe.js ├── polygon.html ├── polygon.js ├── polyicon.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | /deploy/api.js 2 | /deploy/index.js 3 | /deploy/api_nocesium.js 4 | /deploy/index_nocesium.js 5 | /plovr-*.* 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "cesium"] 2 | path = cesium 3 | url = https://github.com/klokantech/cesium.git 4 | [submodule "src-app/library"] 5 | path = src-app/library 6 | url = https://github.com/klokantech/javascript.git 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Leaflet.externs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @externs 3 | * Very simple externs to prevent overwriting of L namespace 4 | */ 5 | var L = {}; 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PLOVR_VERSION=2.0.0 2 | PLOVR=plovr-$(PLOVR_VERSION).jar 3 | 4 | WEBGLEARTH_VERSION = $(firstword $(subst -, ,$(subst v,,$(shell git describe --tags)))) 5 | 6 | .PHONY: plovr cesium lint webserver 7 | 8 | all: build 9 | plovr: $(PLOVR) 10 | $(PLOVR): 11 | wget -q --no-check-certificate https://registry.npmjs.org/plovr/-/plovr-$(PLOVR_VERSION).tgz 12 | tar -xOzf plovr-$(PLOVR_VERSION).tgz package/bin/plovr.jar > $(PLOVR) 13 | rm plovr-$(PLOVR_VERSION).tgz 14 | serve: 15 | java -jar $(PLOVR) serve -p 9810 *.json 16 | build: 17 | java -jar $(PLOVR) build api.json > deploy/api_nocesium.js 18 | sed -e 's#{WEBGLEARTH_VERSION}#$(WEBGLEARTH_VERSION)#' deploy/header.js > deploy/api.js 19 | cat cesium/Build/Cesium/Cesium.js >> deploy/api.js 20 | cat deploy/api_nocesium.js >> deploy/api.js 21 | java -jar $(PLOVR) build app.json > deploy/index_nocesium.js 22 | sed -e 's#{WEBGLEARTH_VERSION}#$(WEBGLEARTH_VERSION)#' deploy/header.js > deploy/index.js 23 | cat cesium/Build/Cesium/Cesium.js >> deploy/index.js 24 | cat deploy/index_nocesium.js >> deploy/index.js 25 | lint: 26 | fixjsstyle --strict -r ./src 27 | fixjsstyle --strict -r ./src-app 28 | gjslint --strict -r ./src 29 | gjslint --strict -r ./src-app 30 | webserver: 31 | java -jar $(PLOVR) soyweb -p 9820 --dir . 32 | cesium: 33 | cd cesium && "./Tools/apache-ant-1.8.2/bin/ant" minify 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### GRANDFATHERED 2 | 3 | This project is no longer maintained. Contact us if you want to become a maintainer of this project. 4 | 5 | We recommend using [Cesium](https://github.com/AnalyticalGraphicsInc/cesium) instead ([code sample](https://cloud.maptiler.com/maps/hybrid/cesium)). 6 | 7 | --- 8 | 9 | # WebGL Earth 2.0 10 | 11 | Open-source virtual planet web application running in any web browser with support for WebGL HTML5 standard. Mobile devices such as iPhone, iPad or Android based mobile phones are going to be supported too. It is a free software and a community driven project. 12 | 13 | There is an extremely easy to use JavaScript API - fully mimicking LeafletJS. 14 | 15 | See: http://examples.webglearth.org/ for demos. 16 | 17 | ## Usage 18 | 19 | ### JavaScript API 20 | 21 | WebGL Earth in version 2.0 is adapting the popular LeafletJS API. So if you are familiar with Leaflet you can easily start to use WebGL Earth, things like markers, popups, centering and flying to a place on given latitude and longitude are possible. The code from Leaflet can be also mixed with WE - you can pass L.LatLng and L.LatLngBounds, etc. 22 | 23 | A simple Hello World: 24 | 25 | ```html 26 | 27 | 28 | 29 | 30 | 36 | 37 | 38 |
39 | 40 | 41 | ``` 42 | More examples are at: http://examples.webglearth.org/. 43 | 44 | Original version of WebGL Earth had also it's own easy API which is preserved for back compatibility. 45 | It is documented at http://www.webglearth.org/api. 46 | 47 | The supported Leaflet API methods are not yet documented -- the best source of the information are currently the API examples (`src/api-l.html`, `src/api.html`, `src/polygon.html`) and the API symbol exports itselves (`src/api.js` and `src/api-l.js`). 48 | 49 | ### Online hosted API 50 | 51 | The project API is available from Google CDN and can be linked and called directly from your web application as `http://www.webglearth.com/v2/api.js`. Examples mentioned above are demonstrating this form of use. 52 | 53 | Embedding of a globe in your own web is then extremely easy. 54 | 55 | ### From custom server or even offline 56 | 57 | Because the project is 100% open-source, the complete code can be hosted also on your own website or distributed with applications. Ready to use API releases are at: https://github.com/webglearth/webglearth2/releases 58 | 59 | With custom rendered map tiles (made with http://www.maptiler.com/) the project can be used on intranets, in restricted environments or even offline. The API should be accessed via HTTP protocol (possibly via localhost). 60 | 61 | 62 | ## Development and building 63 | 64 | * Clone the code from GitHub, including the submodules (cesium). 65 | * Use `make cesium` to build the required component. 66 | 67 | ### Running the app 68 | * Run `make serve` to start the plovr server (port 9810) 69 | * Open any of `src/*.html` 70 | 71 | ### Build the app 72 | * Run `make build` to produce `deploy/*_nocesium.js` 73 | * Combine the sources together to produce a single JavaScript file: 74 | 75 | ``` 76 | /** 77 | * WebGL Earth 2.0 78 | * =============== 79 | * Copyright (C) 2014 - Klokan Technologies GmbH 80 | * http://www.webglearth.org/ 81 | * Powered by Cesium (http://www.webglearth.org/cesium). Apache 2.0 license. 82 | */ 83 | 84 | CESIUM_BASE_URL = '.'; 85 | {content of cesium/Build/Cesium/Cesium.js} 86 | {content of deploy/api_nocesium.js} 87 | ``` 88 | 89 | ## Supported by 90 | 91 | - Klokan Technologies GmbH 92 | - Analytical Graphics, Inc. 93 | - Tween Globe Ltd 94 | - Miizee.com 95 | -------------------------------------------------------------------------------- /api-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "api-debug", 3 | "inherits": "api.json", 4 | 5 | "define": { 6 | "goog.DEBUG": true 7 | }, 8 | 9 | "mode": "ADVANCED", 10 | 11 | "pretty-print": true, 12 | "debug": true 13 | } 14 | -------------------------------------------------------------------------------- /api.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "api", 3 | 4 | "inputs": ["src/api.js", "src/api-l.js"], 5 | "paths": ['src'], 6 | 7 | "externs": [ 8 | "Cesium.externs.js", 9 | "Leaflet.externs.js" 10 | ], 11 | 12 | "define": { 13 | // "goog.dom.ASSUME_STANDARDS_MODE": true, 14 | "goog.DEBUG": false 15 | }, 16 | 17 | "mode": "ADVANCED", 18 | "level": "VERBOSE", 19 | 20 | "output-wrapper": "(function() {%output%})();", 21 | 22 | // "pretty-print": true, 23 | // "debug": true, 24 | 25 | "checks": { 26 | // Unfortunately, the Closure Library violates these in many places. 27 | // "accessControls": "ERROR", 28 | // "visibility": "ERROR" 29 | 30 | "checkTypes": "ERROR", 31 | "checkRegExp": "ERROR", 32 | "checkVars": "ERROR", 33 | "deprecated": "ERROR", 34 | "fileoverviewTags": "ERROR", 35 | "invalidCasts": "ERROR", 36 | "missingProperties": "ERROR", 37 | "nonStandardJsDocs": "OFF", 38 | "undefinedVars": "ERROR" 39 | }, 40 | 41 | "jsdoc-html-output-path": "." 42 | } 43 | -------------------------------------------------------------------------------- /app-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "app-debug", 3 | "inherits": "app.json", 4 | 5 | "define": { 6 | "goog.DEBUG": true, 7 | "weapi.GENERATE_EXPORTS": false 8 | }, 9 | 10 | "mode": "SIMPLE", 11 | 12 | "pretty-print": true, 13 | "debug": true 14 | } 15 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "app", 3 | 4 | "inputs": ["src-app/main.js"], 5 | "paths": ["src", "src-app", "src-app/library/src"], 6 | 7 | "externs": [ 8 | "Cesium.externs.js" 9 | ], 10 | 11 | "define": { 12 | // "goog.dom.ASSUME_STANDARDS_MODE": true, 13 | "goog.DEBUG": false, 14 | "weapi.GENERATE_EXPORTS": false 15 | }, 16 | 17 | "mode": "ADVANCED", 18 | "level": "VERBOSE", 19 | 20 | // "pretty-print": true, 21 | // "debug": true, 22 | 23 | "checks": { 24 | // Unfortunately, the Closure Library violates these in many places. 25 | // "accessControls": "ERROR", 26 | // "visibility": "ERROR" 27 | 28 | "checkTypes": "ERROR", 29 | "checkRegExp": "ERROR", 30 | "checkVars": "ERROR", 31 | "deprecated": "ERROR", 32 | "fileoverviewTags": "ERROR", 33 | "invalidCasts": "ERROR", 34 | "missingProperties": "ERROR", 35 | "nonStandardJsDocs": "OFF", 36 | "undefinedVars": "ERROR" 37 | }, 38 | 39 | "jsdoc-html-output-path": "." 40 | } 41 | -------------------------------------------------------------------------------- /deploy/api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebGL Earth API - Demo 8 | 9 | 10 | 11 | 85 | 86 | 87 | 88 |
89 |
90 |
91 | 92 | 93 | 94 | 95 |
96 | 97 | 98 | 99 |
100 | 101 | 102 |
103 | 104 | 105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 | 115 | 116 | 117 | 118 | 119 |
120 | 121 |      122 | Pick color for position ,
123 |
124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /deploy/header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WebGL Earth {WEBGLEARTH_VERSION} 3 | * =============== 4 | * Copyright (C) 2014 - Klokan Technologies GmbH 5 | * http://www.webglearth.org/ 6 | * Powered by Cesium (http://www.webglearth.org/cesium). Apache 2.0 license. 7 | */ 8 | 9 | //------------------------------------------------------------------------------ 10 | CESIUM_BASE_URL = ('https:' == document.location.protocol ? 'https:' : 'http:') + '//www.webglearth.com/v2/'; 11 | -------------------------------------------------------------------------------- /deploy/main.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,600,700&subset=latin,latin-ext); 2 | 3 | @font-face { 4 | font-family: 'webglearth'; 5 | src:url('fonts/webglearth.eot?8bl3d6'); 6 | src:url('fonts/webglearth.eot?#iefix8bl3d6') format('embedded-opentype'), 7 | url('fonts/webglearth.svg?8bl3d6#webglearth') format('svg'), 8 | url('fonts/webglearth.woff?8bl3d6') format('woff'), 9 | url('fonts/webglearth.ttf?8bl3d6') format('truetype'); 10 | font-weight: normal; 11 | font-style: normal; 12 | } 13 | 14 | html, body {width:100%;height:100%;} 15 | html, body, div {padding:0; margin:0;} 16 | body, input, select {font-family:'Open Sans',Arial,sans-serif;} 17 | img {border:none;} 18 | 19 | #logo, #klokan, #share, #sidelinks li a:before, #sidelinks li a:after {font-family:'webglearth';text-decoration:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overflow:hidden;text-shadow:#333 0 0 5px;} 20 | 21 | #logo {position:absolute;top:40px;left:0;width:200px;height:55px;line-height:55px;z-index:1;background:rgba(255,255,255,0.15);text-align:center;} 22 | #logo:before {content:"\e607";font-size:175px;color:#fff;} 23 | #geocoder {position:absolute;top:40px;height:24px;right:0;width:204px;z-index:1;padding:2px 4px;border:none;} 24 | .ac-renderer {position:absolute;right:0;width:204px;z-index:1;font-size:12px;background:#fff;border:0 solid #000;border-width:1px 0;overflow:hidden;white-space:nowrap;} 25 | .ac-row {border-bottom:1px solid #666;width:100%;overflow:hidden;text-overflow:ellipsis;padding:1px 0 1px 2px;cursor:pointer;} 26 | .ac-row .suffix { 27 | font-size:.9em; 28 | color:#aaa; 29 | padding:.1em 0 .1em 5px; 30 | } 31 | .ac-row .type { 32 | font-size:.8em; 33 | color:#888; 34 | display:block; 35 | width:100%; 36 | } 37 | .ac-row:hover {background:#ccc;} 38 | #maptype {position:absolute;top:67px;height:24px;right:0;width:204px;z-index:1;border:none;} 39 | 40 | .link {color:#eee;background:rgba(255,255,255,0.15);} 41 | .link:hover {color:#fff;background:rgba(255,255,255,0.175);} 42 | 43 | #klokan {position:absolute;bottom:62px;right:0;width:231px;z-index:1;height:58px;} 44 | #klokan:before {content:"\e600";font-size:225px;line-height:58px;} 45 | #webglearthdiv {position:absolute !important;top:0;right:0;bottom:0;left:0;background:#000 url(stars.jpg);background-size:cover;touch-action:none;} 46 | 47 | #share {position:absolute;bottom:75px;left:0;padding:0;list-style:none;margin:0;} 48 | #share li {float:left;line-height:32px;margin-right:2px;} 49 | #share li a {font-size:32px;overflow:hidden;display:block;width:33px;height:32px;text-decoration:none;} 50 | #share .gplus:before {content:"\e606";} 51 | #share .facebook:before {content:"\e605";} 52 | #share .twitter:before {content:"\e604";} 53 | 54 | #sidelinks {position:absolute;top:125px;right:0;padding:0;list-style:none;margin:0;} 55 | #sidelinks li {font-size:16px;line-height:18px;margin-bottom:2px;} 56 | #sidelinks li a {overflow:hidden;display:block;width:120px;text-decoration:none;text-align:center;padding:10px 0;} 57 | #sidelinks .maptiler:after {display:block;content:"\e601";font-size:90px;line-height:90px;} 58 | #sidelinks .jsapi:before {display:block;content:"\e602";font-size:45px;line-height:55px;} 59 | #sidelinks .github:before {display:block;content:"\e603";font-size:45px;line-height:55px;} 60 | .jsapi, .github {background:none;} 61 | 62 | .plovr-error-report {position:absolute;z-index:10;} 63 | 64 | .cesium-credit-textContainer a {color:#fff !important;} 65 | -------------------------------------------------------------------------------- /deploy/polygon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebGL Earth API - Polygon Example 8 | 9 | 10 | 11 | 140 | 141 | 142 | 143 |
144 |
145 |
146 |
147 |
148 | 149 | 150 | 151 |
152 | 153 | 154 | 155 | 156 |
157 | Change fill color: 158 | 166 | Change border color: 167 | 175 |
176 | 177 | 178 |
179 | 180 | 181 |
182 | 183 | 184 | 185 |
186 | 187 | 188 | -------------------------------------------------------------------------------- /deploy/stars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webglearth/webglearth2/ec69cf65d9fb32ca698ed01bd017347a8c39bff0/deploy/stars.jpg -------------------------------------------------------------------------------- /deploy/world512.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webglearth/webglearth2/ec69cf65d9fb32ca698ed01bd017347a8c39bff0/deploy/world512.jpg -------------------------------------------------------------------------------- /src-app/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebGL Earth 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 |
23 | 28 |
29 | 34 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src-app/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapp.App'); 9 | 10 | goog.require('goog.dom'); 11 | goog.require('goog.dom.ViewportSizeMonitor'); 12 | goog.require('goog.events'); 13 | goog.require('goog.events.EventType'); 14 | goog.require('goog.net.Jsonp'); 15 | goog.require('goog.userAgent'); 16 | 17 | goog.require('kt.OsmNamesAutocomplete'); 18 | goog.require('weapi.exports.App'); 19 | 20 | 21 | 22 | /** 23 | * @constructor 24 | */ 25 | weapp.App = function() { 26 | if (!weapi.App.detectWebGLSupport()) { 27 | window.location = '//www.webglearth.com/webgl-error.html'; 28 | } 29 | 30 | var thkey = 'kSwrAdFeuIo6rD2Bm9dc'; 31 | 32 | /** 33 | * @type {!weapi.exports.App} 34 | * @private 35 | */ 36 | this.app_ = new weapi.exports.App('webglearthdiv', { 37 | 'atmosphere': true, 38 | 'sky': false, 39 | 'terrain': 'https://api.maptiler.com/tiles/terrain-quantized-mesh/layer.json?key=' + thkey + '&', 40 | 'terrainCredit': '', // suppress the html credit 41 | 'position': [0, 0], 42 | 'altitude': weapp.App.DEFAULT_ALT, 43 | 'panning': true, 44 | 'tilting': true, 45 | 'zooming': true, 46 | 'proxyHost': '//srtm.webglearth.com/cgi-bin/corsproxy.fcgi?url=' 47 | }); 48 | 49 | if (window.location.hash.length < 4) { 50 | new goog.net.Jsonp('https://freegeoip.klokantech.com/json/').send( 51 | undefined, goog.bind(function(data) { 52 | if (data) { 53 | var lat = data['latitude'], lng = data['longitude']; 54 | if (!isNaN(lat) && !isNaN(lng)) { 55 | this.app_.setPosition(lat, lng); 56 | } 57 | } 58 | }, this)); 59 | } 60 | 61 | /** 62 | * @type {!goog.dom.ViewportSizeMonitor} 63 | * @private 64 | */ 65 | this.vsm_ = new goog.dom.ViewportSizeMonitor(); 66 | goog.events.listen(this.vsm_, goog.events.EventType.RESIZE, function(e) { 67 | this.resize_(this.vsm_.getSize()); 68 | }, false, this); 69 | this.resize_(this.vsm_.getSize()); 70 | 71 | var geocoderElement = /** @type {!Element} */ 72 | (goog.dom.getElement('geocoder')); 73 | 74 | var autocomplete = new kt.OsmNamesAutocomplete(geocoderElement, 75 | 'https://geocoder.tilehosting.com/', 'oeGRMGLUngqm8fMN45zj'); 76 | 77 | autocomplete.registerCallback(function(item) { 78 | console.log(item['boundingbox']); 79 | this.app_.flyToFitBounds( 80 | item['boundingbox'][1], 81 | item['boundingbox'][3], 82 | item['boundingbox'][0], 83 | item['boundingbox'][2] 84 | ); 85 | }.bind(this)); 86 | 87 | var initedMaps = {}; //cache 88 | var maptypeElement = /** @type {!HTMLSelectElement} */ 89 | (goog.dom.getElement('maptype')); 90 | var updateLayer = goog.bind(function() { 91 | var key = maptypeElement.options[maptypeElement.selectedIndex].value; 92 | switch (key) { 93 | default: 94 | if (!goog.isDefAndNotNull(initedMaps[key])) { 95 | initedMaps[key] = this.app_.initMap(weapi.maps.MapType.CUSTOM, { 96 | 'url': 'https://api.maptiler.com/maps/' + key + 97 | '/{z}/{x}/{y}.png?key=' + thkey, 98 | 'tileSize': 512, 99 | 'maximumLevel': 18, 100 | 'copyright': '© MapTiler © OpenStreetMap contributors', 101 | 'copyrightLink': 'https://www.maptiler.com/copyright/' 102 | }); 103 | } 104 | this.app_.setBaseMap(initedMaps[key]); 105 | break; 106 | case 'hybrid': 107 | if (!goog.isDefAndNotNull(initedMaps[key])) { 108 | initedMaps[key] = this.app_.initMap(weapi.maps.MapType.CUSTOM, { 109 | 'url': 'https://api.maptiler.com/maps/hybrid' + 110 | '/{z}/{x}/{y}.jpg?key=' + thkey, 111 | 'tileSize': 512, 112 | 'maximumLevel': 16, 113 | 'copyright': '© MapTiler © OpenStreetMap contributors', 114 | 'copyrightLink': 'https://www.maptiler.com/copyright/' 115 | }); 116 | } 117 | this.app_.setBaseMap(initedMaps[key]); 118 | break; 119 | case 'osm': 120 | if (!goog.isDefAndNotNull(initedMaps[key])) { 121 | initedMaps[key] = this.app_.initMap(weapi.maps.MapType.OSM); 122 | } 123 | this.app_.setBaseMap(initedMaps[key]); 124 | break; 125 | } 126 | }, this); 127 | goog.events.listen(maptypeElement, goog.events.EventType.CHANGE, updateLayer); 128 | updateLayer(); 129 | 130 | /* HASH UPDATING & PARSING */ 131 | 132 | /** 133 | * @type {string} 134 | * @private 135 | */ 136 | this.lastCreatedHash_ = ''; 137 | 138 | var updateHash = goog.bind(function() { 139 | var pos = this.app_.getPosition(); 140 | var newhash = '#ll=' + pos[0].toFixed(5) + ',' + pos[1].toFixed(5) + 141 | ';alt=' + this.app_.getAltitude().toFixed(0); 142 | var head = this.app_.getHeading(), tilt = this.app_.getTilt(); 143 | if (Math.abs(head) > 0.001) newhash += ';h=' + head.toFixed(3); 144 | if (Math.abs(tilt) > 0.001) newhash += ';t=' + tilt.toFixed(3); 145 | if (window.location.hash.toString() != newhash) { 146 | this.lastCreatedHash_ = newhash; 147 | window.location.hash = newhash; 148 | } 149 | }, this); 150 | 151 | var parseHash = goog.bind(function() { 152 | if (window.location.hash == this.lastCreatedHash_) return; 153 | var params = window.location.hash.substr(1).split(';'); 154 | var getValue = function(name) { 155 | name += '='; 156 | var pair = goog.array.find(params, function(el, i, a) { 157 | return el.indexOf(name) === 0;}); 158 | 159 | if (goog.isDefAndNotNull(pair)) { 160 | var value = pair.substr(name.length); 161 | if (value.length > 0) 162 | return value; 163 | } 164 | return undefined; 165 | }; 166 | 167 | var ll = getValue('ll'), altitude = getValue('alt'); 168 | var heading = getValue('h'), tilt = getValue('t'); 169 | if (goog.isDefAndNotNull(ll)) { 170 | var llsplit = ll.split(','); 171 | if (llsplit.length > 1 && !isNaN(llsplit[0]) && !isNaN(llsplit[1])) { 172 | if (!altitude || isNaN(altitude)) altitude = weapp.App.DEFAULT_ALT; 173 | if (!tilt || isNaN(tilt)) tilt = 0; 174 | if (!heading || isNaN(heading)) heading = 0; 175 | this.app_.setPosition(parseFloat(llsplit[0]), parseFloat(llsplit[1]), 176 | undefined, parseFloat(altitude), 177 | parseFloat(heading), parseFloat(tilt)); 178 | } 179 | } 180 | }, this); 181 | 182 | /** 183 | * @type {!goog.Timer} 184 | */ 185 | this.hashUpdateTimer = new goog.Timer(2000); 186 | goog.events.listen(this.hashUpdateTimer, goog.Timer.TICK, updateHash); 187 | this.hashUpdateTimer.start(); 188 | 189 | goog.events.listen(window, goog.events.EventType.HASHCHANGE, parseHash); 190 | 191 | parseHash(); 192 | 193 | if (goog.userAgent.MOBILE || goog.userAgent.ANDROID || 194 | goog.userAgent.IPHONE || goog.userAgent.IPAD) { 195 | var hideAddressBar = function() { 196 | if (document.documentElement.scrollHeight < 197 | window.outerHeight / window.devicePixelRatio) 198 | document.documentElement.style.height = 199 | (window.outerHeight / window.devicePixelRatio) + 'px'; 200 | setTimeout(window.scrollTo(1, 1), 0); 201 | }; 202 | goog.events.listen(window, 'orientationchange', hideAddressBar); 203 | hideAddressBar(); 204 | } else { 205 | geocoderElement.focus(); 206 | } 207 | }; 208 | 209 | 210 | /** 211 | * @define {number} default altitude in meters. 212 | */ 213 | weapp.App.DEFAULT_ALT = 17000000; 214 | 215 | 216 | /** 217 | * @param {?goog.math.Size} size 218 | * @private 219 | */ 220 | weapp.App.prototype.resize_ = function(size) { 221 | if (!size) return; 222 | this.app_.handleResize(); 223 | }; 224 | 225 | goog.exportSymbol('WebGLEarth', weapp.App); 226 | -------------------------------------------------------------------------------- /src/api-l.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebGL Earth API - Basic Leaflet compatibility 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 132 | 141 | 142 | 143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | 151 | 152 |     153 | 154 | 155 |
156 | Second layer opacity: 01 157 |     158 | Grand Canyon overlay opacity: 01 159 |
160 | 161 | 162 |
163 | 164 |
165 | 166 | 167 | -------------------------------------------------------------------------------- /src/api-l.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Additional Leaflet-style API 3 | * 4 | * @author petr.sloup@klokantech.com (Petr Sloup) 5 | * 6 | * Copyright 2014 Klokan Technologies Gmbh (www.klokantech.com) 7 | */ 8 | 9 | goog.provide('weapi.exportsL'); 10 | 11 | goog.require('goog.net.Jsonp'); 12 | 13 | goog.require('weapi'); 14 | goog.require('weapi.exports.App'); 15 | goog.require('weapi.maps'); 16 | 17 | 18 | /** 19 | * @define {boolean} Generate these exports? 20 | */ 21 | weapi.GENERATE_EXPORTS_L = true; 22 | 23 | var exportSymbolL = function(symbol, obj) { 24 | if (weapi.GENERATE_EXPORTS_L) goog.exportSymbol(symbol, obj); 25 | }; 26 | 27 | 28 | exportSymbolL('WE.map', function(id, opt_opts) { 29 | var opts = opt_opts || {}; 30 | var z = opts['zoom']; 31 | opts['zoom'] = undefined; 32 | opts['empty'] = true; 33 | var app = new weapi.exports.App(id, opts); 34 | if (z) app.setZoom(z); 35 | return app; 36 | }); 37 | 38 | 39 | exportSymbolL('WebGLEarth.prototype.setView', function(center, opt_zoom) { 40 | if (!goog.isArray(center)) center = [center['lat'], center['lng']]; 41 | this.setPosition(center[0], center[1], opt_zoom, undefined, 0, 0); 42 | }); 43 | 44 | 45 | exportSymbolL('WebGLEarth.prototype.zoomIn', function(opt_delta) { 46 | this.setZoom(this.getZoom() + (goog.isNumber(opt_delta) ? opt_delta : 1)); 47 | }); 48 | 49 | 50 | exportSymbolL('WebGLEarth.prototype.zoomOut', function(opt_delta) { 51 | this.setZoom(this.getZoom() - (goog.isNumber(opt_delta) ? opt_delta : 1)); 52 | }); 53 | 54 | 55 | exportSymbolL('WebGLEarth.prototype.panInsideBounds', function(bnds, opt_opts) { 56 | if (!goog.isArray(bnds)) 57 | bnds = [bnds.getSouth(), bnds.getNorth(), bnds.getWest(), bnds.getEast()]; 58 | if (goog.isArray(bnds[0])) 59 | bnds = [bnds[0][0], bnds[1][0], bnds[0][1], bnds[1][1]]; 60 | opt_opts = opt_opts || {}; 61 | this.flyToFitBounds(bnds[0], bnds[1], bnds[2], bnds[3], 62 | opt_opts['heading'], opt_opts['tilt'], 63 | opt_opts['duration']); 64 | }); 65 | 66 | 67 | exportSymbolL('WebGLEarth.prototype.fitBounds', function(bnds, opt_opts) { 68 | if (!goog.isArray(bnds)) 69 | bnds = [bnds.getSouth(), bnds.getNorth(), bnds.getWest(), bnds.getEast()]; 70 | if (goog.isArray(bnds[0])) 71 | bnds = [bnds[0][0], bnds[1][0], bnds[0][1], bnds[1][1]]; 72 | opt_opts = opt_opts || {}; 73 | 74 | var minlat = goog.math.toRadians(bnds[0]); 75 | var maxlat = goog.math.toRadians(bnds[1]); 76 | var minlon = goog.math.toRadians(bnds[2]); 77 | var maxlon = goog.math.toRadians(bnds[3]); 78 | var opt_heading = opt_opts['heading'], opt_tilt = opt_opts['tilt']; 79 | 80 | var altitude = this.camera.calcDistanceToViewBounds(minlat, maxlat, 81 | minlon, maxlon); 82 | 83 | minlon = goog.math.modulo(minlon, 2 * Math.PI); 84 | maxlon = goog.math.modulo(maxlon, 2 * Math.PI); 85 | 86 | var lonDiff = minlon - maxlon; 87 | if (lonDiff < -Math.PI) { 88 | minlon += 2 * Math.PI; 89 | } else if (lonDiff > Math.PI) { 90 | maxlon += 2 * Math.PI; 91 | } 92 | 93 | var center = [(minlat + maxlat) / 2, (minlon + maxlon) / 2]; 94 | 95 | this.setPosition( 96 | goog.math.toDegrees(center[0]), goog.math.toDegrees(center[1]), 97 | undefined, altitude, 98 | opt_heading, opt_tilt, goog.isDef(opt_heading) || goog.isDef(opt_tilt)); 99 | }); 100 | 101 | 102 | exportSymbolL('WebGLEarth.prototype.panTo', function(center, opt_opts) { 103 | if (!goog.isArray(center)) center = [center['lat'], center['lng']]; 104 | opt_opts = opt_opts || {}; 105 | this.flyTo(center[0], center[1], undefined, undefined, 0, 0, 106 | undefined, opt_opts['duration']); 107 | }); 108 | 109 | 110 | exportSymbolL('WE.tileLayer', function(url, opt_opts) { 111 | var opts = opt_opts || {}; 112 | goog.object.forEach(opts, function(val, k, arr) { 113 | url = url.replace('{' + k + '}', val); 114 | }); 115 | url = url.replace('{s}', '{sub}'); 116 | var subdoms = opts['subdomains'] || 'abc'; 117 | if (goog.isString(subdoms)) subdoms = subdoms.split(''); 118 | var bnds = opts['bounds']; 119 | if (bnds && goog.isArray(bnds[0])) 120 | bnds = [bnds[0][0], bnds[1][0], bnds[0][1], bnds[1][1]]; 121 | return weapi.maps.initMap(null, weapi.maps.MapType.CUSTOM, { 122 | 'url': url, 123 | 'minimumLevel': opts['minZoom'] || 0, 124 | 'maximumLevel': opts['maxZoom'] || 18, 125 | 'tileSize': opts['tileSize'] || 256, 126 | 'flipY': opts['tms'] || false, 127 | 'subdomains': subdoms, 128 | 'copyright': (opts['attribution'] || '').replace(/<(?:.|\n)*?>/gm, ''), 129 | 'opacity': opts['opacity'], 130 | 'bounds': bnds 131 | }); 132 | }); 133 | 134 | 135 | exportSymbolL('WE.tileLayerJSON', function(data, opt_app) { 136 | var load = function(data, opt_app, opt_map) { 137 | var url = data['tiles'][0]; 138 | var attribution = data['attribution']; 139 | var minzoom = data['minzoom']; 140 | var maxzoom = data['maxzoom']; 141 | var bnds = data['bounds']; 142 | var center = data['center']; 143 | 144 | var opts = { 145 | 'url': url, 146 | 'minimumLevel': minzoom || 0, 147 | 'maximumLevel': maxzoom || 18, 148 | 'copyright': (attribution || '').replace(/<(?:.|\n)*?>/gm, ''), 149 | 'bounds': bnds ? [bnds[1], bnds[3], bnds[0], bnds[2]] : undefined 150 | }; 151 | var map; 152 | 153 | if (opt_map) { 154 | var prov = /** @type {!weapi.CustomMap} */(opt_map.layer.imageryProvider); 155 | prov.setOptions(opts); 156 | map = opt_map; 157 | if (data['opacity'] && map.getOpacity() == 1) 158 | map.setOpacity(parseFloat(data['opacity'])); 159 | if (bnds) map.setBoundingBox(bnds[1], bnds[3], bnds[0], bnds[2]); 160 | } else { 161 | map = weapi.maps.initMap(null, weapi.maps.MapType.CUSTOM, opts); 162 | } 163 | 164 | if (opt_app) { 165 | map['addTo'](opt_app); 166 | if (center && center.length && center.length > 1) { 167 | opt_app.setPosition(center[1], center[0]); 168 | if (center.length > 2) { 169 | opt_app.setZoom(center[2]); 170 | } 171 | } 172 | } 173 | return map; 174 | }; 175 | var map; 176 | if (goog.isString(data)) { 177 | map = weapi.maps.initMap(null, weapi.maps.MapType.CUSTOM, {}); 178 | var jsonp = new goog.net.Jsonp(data); 179 | jsonp.send(undefined, function(data) { 180 | load(data, opt_app, map); 181 | }); 182 | } else { 183 | map = load(data, opt_app, undefined); 184 | } 185 | 186 | return map; 187 | }); 188 | 189 | 190 | exportSymbolL('WebGLEarth.Map.prototype.addTo', function(app) { 191 | this.layer.imageryProvider.proxy = app.mapProxyObject; 192 | var layers = app.scene.imageryLayers; 193 | layers.add(this.layer); 194 | app.listenCORSErrors(this.layer.imageryProvider['errorEvent']); 195 | this.app = app; 196 | app.sceneChanged = true; 197 | 198 | return this; 199 | }); 200 | 201 | 202 | exportSymbolL('WebGLEarth.Map.prototype.removeFrom', function(app) { 203 | app = this.app; 204 | var layers = app.scene.imageryLayers; 205 | layers.remove(this.layer); 206 | app.sceneChanged = true; 207 | this.app = null; 208 | 209 | return this; 210 | }); 211 | 212 | 213 | exportSymbolL('WE.marker', function(pos, opt_iconUrl, opt_width, opt_height) { 214 | if (!goog.isArray(pos)) pos = [pos['lat'], pos['lng']]; 215 | var mark = new weapi.markers.PrettyMarker(goog.math.toRadians(pos[0]), 216 | goog.math.toRadians(pos[1]), 217 | opt_iconUrl, opt_width, opt_height); 218 | return mark; 219 | }); 220 | 221 | 222 | exportSymbolL('WebGLEarth.Marker.prototype.addTo', function(app) { 223 | app.markerManager.addMarker(goog.getUid(this).toString(), this); 224 | app.sceneChanged = true; 225 | return this; 226 | }); 227 | 228 | 229 | exportSymbolL('WebGLEarth.Marker.prototype.removeFrom', function(app) { 230 | app.markerManager.removeMarker(goog.getUid(this).toString()); 231 | app.sceneChanged = true; 232 | return this; 233 | }); 234 | 235 | 236 | exportSymbolL('WebGLEarth.Marker.prototype.bindPopup', 237 | function(content, maxWOrOpts, closeBtn) { 238 | if (!goog.isDefAndNotNull(maxWOrOpts) || goog.isNumber(maxWOrOpts)) { 239 | this.attachPopup(new weapi.markers.Popup(content, 240 | maxWOrOpts, closeBtn)); 241 | } else { 242 | var maxWidth = maxWOrOpts['maxWidth']; 243 | closeBtn = maxWOrOpts['closeButton']; 244 | this.attachPopup(new weapi.markers.Popup(content, maxWidth, closeBtn)); 245 | } 246 | return this; 247 | }); 248 | 249 | 250 | exportSymbolL('WebGLEarth.Marker.prototype.setLatLng', function(pos) { 251 | if (!goog.isArray(pos)) pos = [pos['lat'], pos['lng']]; 252 | this.setPosition(pos[0], pos[1]); 253 | }); 254 | 255 | 256 | exportSymbolL('WE.polygon', function(points, opts) { 257 | // our design is not prepared for polygons not assigned to any app -> hack 258 | return { 259 | 'addTo': /** @suppress {accessControls} */function(app) { 260 | //WARNING: addTo returns something different than WE.polygon ! 261 | var poly = new weapi.exports.Polygon(app); 262 | var points_ = []; 263 | goog.array.forEachRight(points, function(el, i, arr) { 264 | if (!goog.isArray(el)) el = [el['lat'], el['lng']]; 265 | points_.push([el[0], el[1]]); 266 | }); 267 | poly.addPoints(points_); 268 | opts = opts || {}; 269 | poly.showDraggers(opts['editable'] == true); 270 | poly.setStrokeColor(opts['color'] || '#03f', 271 | opts['opacity'] || 0.5); 272 | poly.setFillColor(opts['fillColor'] || '#03f', 273 | opts['fillOpacity'] || 0.2); 274 | poly.polygon_.primitiveLine.width = opts['weight'] || 5; //TODO: cleaner 275 | return poly; 276 | } 277 | }; 278 | }); 279 | -------------------------------------------------------------------------------- /src/api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebGL Earth API - Hello World 8 | 9 | 10 | 11 | 12 | 116 | 117 | 118 | 119 |
120 |
121 |
122 | 123 | 124 | 125 | 126 |
127 | 128 | 129 | 130 |
131 | 132 | 133 |
134 | 135 | 136 | 137 |
138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 |
146 | 147 | 148 | 149 | 150 | 151 |
152 | 153 |      154 | Pick color for position ,
155 |
156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.App'); 9 | 10 | goog.require('goog.dom'); 11 | 12 | goog.require('weapi.Camera'); 13 | goog.require('weapi.maps'); 14 | goog.require('weapi.markers.MarkerManager'); 15 | goog.require('weapi.markers.PrettyMarker'); 16 | goog.require('weapi.utils'); 17 | 18 | 19 | /** 20 | * @define {number} API version. 21 | */ 22 | weapi.VERSION = 2; 23 | 24 | 25 | /** 26 | * @define {string} . 27 | */ 28 | weapi.UA = 'UA-20846306-1'; 29 | 30 | 31 | 32 | /** 33 | * 34 | * @param {string} divid . 35 | * @param {Object=} opt_options Application options. 36 | * @constructor 37 | */ 38 | weapi.App = function(divid, opt_options) { 39 | var options = opt_options || {}; 40 | var container = goog.dom.getElement(divid); 41 | 42 | var webGLSupported = weapi.App.detectWebGLSupport(); 43 | 44 | this.isFileProtocol = window.location.protocol == 'file:'; 45 | this.resourceProtocol = this.isFileProtocol ? 'http:' : ''; 46 | 47 | if (weapi.UA && weapi.UA.length > 0) { 48 | var trackerVar = '__WE_ga'; //global variable 49 | (function(i, s, o, g, r) { 50 | i['GoogleAnalyticsObject'] = r; 51 | i[r] = i[r] || function() {(i[r]['q'] = i[r]['q'] || []).push(arguments)}; 52 | i[r]['l'] = 1 * new Date(); 53 | var a = s.createElement(o), m = s.getElementsByTagName(o)[0]; 54 | a.async = 1; 55 | a.src = g; 56 | m.parentNode.insertBefore(a, m); 57 | })(window, document, 'script', 58 | this.resourceProtocol + '//www.google-analytics.com/analytics.js', 59 | trackerVar); 60 | window[trackerVar]('create', weapi.UA, {'name': 'we0'}); 61 | window[trackerVar]('we0.send', 'event', weapi.VERSION.toString(), 62 | window.location.host, window.location.href, 63 | webGLSupported ? 1 : 0); 64 | } 65 | 66 | if (!webGLSupported) { 67 | var ifr = goog.dom.createDom('iframe', { 68 | 'src': this.resourceProtocol + '//www.webglearth.com/webgl-error.html' 69 | }); 70 | ifr.style.width = '100%'; 71 | ifr.style.height = '100%'; 72 | ifr.style.border = 'none'; 73 | container.appendChild(ifr); 74 | return; 75 | } 76 | 77 | weapi.utils.installStyles('.cesium-credit-textContainer:before{content:' + 78 | '\'WebGL Earth \\2022\\20 Cesium \\2022\\20\';}'); 79 | 80 | this.CORSErrorReported = false; 81 | weapi.maps.initStatics(this); 82 | 83 | container.style.position = 'relative'; 84 | container.style.overflow = 'hidden'; 85 | this.canvas = /** @type {!HTMLCanvasElement} */ 86 | (goog.dom.createElement('canvas')); 87 | this.canvas.style.width = '100%'; 88 | this.canvas.style.height = '100%'; 89 | this.canvas.oncontextmenu = function() {return false;}; 90 | container.appendChild(this.canvas); 91 | 92 | /** @type {?Function} */ 93 | this.afterFrameOnce = null; 94 | 95 | this.scene = new Cesium.Scene({ 96 | 'canvas': this.canvas, 97 | 'contextOptions': {'webgl': {'alpha': options['sky'] !== true}} 98 | }); 99 | 100 | /** @type {?weapi.MiniGlobe} */ 101 | this.miniglobe = null; 102 | 103 | /** @type {boolean} */ 104 | this.forcedPause = false; 105 | 106 | /** @type {boolean} */ 107 | this.sceneChanged = true; 108 | 109 | // Debug stats for the rendering: (uncomment here and stats[_]++ below) 110 | //var stats = [0, 0]; 111 | //setInterval(function() { 112 | // window['console']['log']('rendered:', stats[0], 'ommited:', stats[1], 113 | // 'rendered %:', stats[0] / (stats[0] + stats[1])); 114 | //}, 2000); 115 | 116 | /** @type {!Cesium.Matrix4} */ 117 | this.lastViewMatrix = new Cesium.Matrix4(); 118 | 119 | /** @type {?string} */ 120 | var proxyHost = options['proxyHost'] || null; 121 | 122 | /* type {{getURL: function(string) : string}} */ 123 | this.mapProxyObject = { 124 | 'getURL': function(url) { 125 | return goog.isDefAndNotNull(proxyHost) ? 126 | proxyHost + encodeURIComponent(url) : url; 127 | } 128 | }; 129 | 130 | if (options['atmosphere']) { 131 | this.scene.skyAtmosphere = new Cesium.SkyAtmosphere(); 132 | } 133 | if (options['sky']) { 134 | var baseUrl = goog.isString(options['sky']) ? 135 | options['sky'] : window['CESIUM_BASE_URL']; 136 | var skyBoxBaseUrl = (goog.DEBUG ? '../deploy/' : baseUrl) + 'SkyBox/'; 137 | this.scene.skyBox = new Cesium.SkyBox({ 138 | 'sources': { 139 | 'positiveX' : skyBoxBaseUrl + 'px.jpg', 140 | 'negativeX' : skyBoxBaseUrl + 'mx.jpg', 141 | 'positiveY' : skyBoxBaseUrl + 'py.jpg', 142 | 'negativeY' : skyBoxBaseUrl + 'my.jpg', 143 | 'positiveZ' : skyBoxBaseUrl + 'pz.jpg', 144 | 'negativeZ' : skyBoxBaseUrl + 'mz.jpg' 145 | } 146 | }); 147 | } else { 148 | this.scene.backgroundColor = new Cesium.Color(0, 0, 0, 0); 149 | } 150 | 151 | var primitives = this.scene.primitives; 152 | 153 | var ellipsoid = Cesium.Ellipsoid.WGS84; 154 | this.globe = new Cesium.Globe(ellipsoid); 155 | 156 | this.camera = new weapi.Camera(this.scene.camera, ellipsoid); 157 | 158 | this.scene.globe = this.globe; 159 | 160 | if (options['empty'] !== true) { 161 | // default layer -- OSM 162 | var secure = 'https:' == document.location.protocol; 163 | var protocol = (secure ? 'https:' : 'http:'); 164 | 165 | var mq = new Cesium.OpenStreetMapImageryProvider({ 166 | 'url': protocol + '//a.tile.openstreetmap.org/' 167 | }); 168 | this.scene.imageryLayers.addImageryProvider(mq); 169 | } 170 | this.withTerrain = !!options['terrain']; 171 | if (this.withTerrain) { 172 | var url = options['terrain']; 173 | if (url === true) { 174 | // for compatibility 175 | url = 'https://assets.agi.com/stk-terrain/v1/tilesets/world/tiles'; 176 | } 177 | var terrainProvider = new Cesium.CesiumTerrainProvider({ 178 | 'url': url, 179 | 'credit': options['terrainCredit'] 180 | }); 181 | this.scene.terrainProvider = terrainProvider; 182 | } 183 | 184 | /** 185 | * @type {!weapi.markers.MarkerManager} 186 | */ 187 | this.markerManager = new weapi.markers.MarkerManager(this, container); 188 | 189 | /** 190 | * @type {!Array.} 191 | */ 192 | this.composites = []; 193 | 194 | /** 195 | * @type {!Cesium.BillboardCollection} 196 | */ 197 | this.polyIconCollection = new Cesium.BillboardCollection(); 198 | primitives.add(this.polyIconCollection); 199 | 200 | var tick = goog.bind(function() { 201 | if (!this.forcedPause) { 202 | this.scene.initializeFrame(); // to update camera from animators and sscc 203 | 204 | // we need to use continous rendering with terrain -- 205 | // we can't tell when the terrain data are finished loading and processing 206 | var renderNeeded = this.withTerrain || this.sceneChanged; 207 | this.sceneChanged = false; 208 | if (!renderNeeded) { 209 | // extended sceneChanged detection 210 | var viewMatrix = this.camera.camera.viewMatrix; 211 | if (!this.lastViewMatrix || !this.lastViewMatrix.equals(viewMatrix)) { 212 | viewMatrix.clone(this.lastViewMatrix); 213 | renderNeeded = true; 214 | } 215 | } 216 | if (renderNeeded) { 217 | //stats[0]++; 218 | this.scene.render(); 219 | if (goog.isDefAndNotNull(this.miniglobe)) { 220 | this.miniglobe.draw(); 221 | } 222 | this.markerManager.updateMarkers(); 223 | if (goog.isDefAndNotNull(this.afterFrameOnce)) { 224 | this.afterFrameOnce(); 225 | this.afterFrameOnce = null; 226 | } 227 | } else { 228 | //stats[1]++; 229 | } 230 | } 231 | Cesium.requestAnimationFrame(tick); 232 | }, this); 233 | Cesium.requestAnimationFrame(tick); 234 | 235 | var handler = new Cesium.ScreenSpaceEventHandler(this.canvas); 236 | 237 | var stopAnim = goog.bind(function() {this.camera.animator.cancel();}, this); 238 | 239 | handler.setInputAction(stopAnim, Cesium.ScreenSpaceEventType.LEFT_DOWN); 240 | handler.setInputAction(stopAnim, Cesium.ScreenSpaceEventType.RIGHT_DOWN); 241 | handler.setInputAction(stopAnim, Cesium.ScreenSpaceEventType.MIDDLE_DOWN); 242 | handler.setInputAction(stopAnim, Cesium.ScreenSpaceEventType.WHEEL); 243 | handler.setInputAction(stopAnim, Cesium.ScreenSpaceEventType.PINCH_START); 244 | 245 | goog.events.listen(window, 'resize', this.handleResize, false, this); 246 | this.handleResize(); 247 | 248 | 249 | var pos = options['position']; 250 | var center = options['center']; 251 | if (goog.isDefAndNotNull(pos) && pos.length > 1) { 252 | this.camera.setPos(goog.math.toRadians(pos[0]), 253 | goog.math.toRadians(pos[1]), 254 | undefined); 255 | } else if (goog.isDefAndNotNull(center) && center.length > 1) { 256 | this.camera.setPos(goog.math.toRadians(center[0]), 257 | goog.math.toRadians(center[1]), 258 | undefined); 259 | } 260 | 261 | // TODO: zoom support 262 | var z = options['zoom']; 263 | if (goog.isDefAndNotNull(z)) window['console']['log']('zoom not supported'); 264 | 265 | var alt = options['altitude']; 266 | if (goog.isDefAndNotNull(alt)) this.camera.setPos(undefined, undefined, alt); 267 | 268 | var sscc = this.scene.screenSpaceCameraController; 269 | 270 | if (options['unconstrainedRotation'] !== true) { 271 | this.scene.camera.constrainedAxis = Cesium.Cartesian3.UNIT_Z; 272 | } 273 | 274 | //sscc.enableLook = false; 275 | if (options['panning'] === false || options['dragging'] === false) 276 | sscc.enableRotate = false; 277 | if (options['tilting'] === false) 278 | sscc.enableTilt = false; //TODO: fix axis 279 | if (options['zooming'] === false || options['scrollWheelZoom'] === false) 280 | sscc.enableZoom = false; 281 | 282 | sscc.minimumZoomDistance = options['minAltitude'] || 20; 283 | sscc.maximumZoomDistance = options['maxAltitude'] || Infinity; 284 | 285 | sscc['tiltEventTypes'].push({ 286 | 'eventType': Cesium['CameraEventType']['LEFT_DRAG'], 287 | 'modifier': Cesium['KeyboardEventModifier']['SHIFT'] 288 | }); 289 | 290 | sscc['lookEventTypes'] = { 291 | 'eventType': Cesium['CameraEventType']['LEFT_DRAG'], 292 | 'modifier': Cesium['KeyboardEventModifier']['ALT'] 293 | }; 294 | 295 | //HACK for color picking: 296 | // when Cesium creates texture from image, it discards original image. 297 | // It is however very unefficient to preform color picking on WebGLTexture. 298 | // Therefore, clone the image reference prior to creating the texture. 299 | var orig = Cesium['ImageryLayer'].prototype['_createTexture']; 300 | Cesium['ImageryLayer'].prototype['_createTexture'] = function(ctx, imgry) { 301 | imgry['__image__'] = imgry['image']; 302 | orig.call(this, ctx, imgry); 303 | }; 304 | 305 | // + HACK for sceneChange detection after loading tiles: 306 | var that = this; 307 | var orig2 = Cesium['GlobeSurfaceTile']['processStateMachine']; 308 | Cesium['GlobeSurfaceTile']['processStateMachine'] = 309 | function(t, c, cl, tp, ic) { 310 | /*if (this['isRenderable']) */that.sceneChanged = true; 311 | 312 | orig2(t, c, cl, tp, ic); 313 | }; 314 | 315 | setTimeout(function() { 316 | that.sceneChanged = true; 317 | }, 1); 318 | }; 319 | 320 | 321 | /** 322 | * @define {number} Maximum size of the group of primitives;. 323 | */ 324 | weapi.App.PRIMITIVE_GROUPING_SIZE = 10; 325 | 326 | 327 | /** 328 | * @param {HTMLCanvasElement=} opt_canvas 329 | * @param {Object=} opt_contextOpts 330 | * @return {?WebGLRenderingContext} 331 | */ 332 | weapi.App.detectWebGLSupport = function(opt_canvas, opt_contextOpts) { 333 | if (!!window['WebGLRenderingContext']) { 334 | var canvas = opt_canvas || goog.dom.createElement('canvas'), 335 | names = ['webgl', 'experimental-webgl']; //'moz-webgl', 'webkit-3d' 336 | for (var i = 0; i < names.length; i++) { 337 | try { 338 | var ctx = /** @type {?WebGLRenderingContext} */ 339 | (canvas.getContext(names[i], opt_contextOpts)); 340 | if (ctx && goog.isFunction(ctx['getParameter'])) return ctx; 341 | } catch (e) {} 342 | } 343 | return null; // supported but disabled 344 | } 345 | return null; //not supported 346 | }; 347 | 348 | 349 | /** 350 | * @param {Object} eventObj 351 | */ 352 | weapi.App.prototype.listenCORSErrors = function(eventObj) { 353 | if (!this.CORSErrorReported) { 354 | eventObj['addEventListener'](function(e) { 355 | if (!this.CORSErrorReported) { 356 | //window['console']['log'](e); 357 | //if (e['timesRetried'] > 1) { // not an isolated network error 358 | if (this.isFileProtocol && 359 | e['provider']['_url'].indexOf('http') !== 0) { 360 | alert('Tiles for WebGL must be accessed over http protocol.'); 361 | } else { 362 | var msg = 'An error occured while accessing the tiles. Cross-domain' + 363 | ' access restrictions are applied on map tiles for WebGL. ' + 364 | 'Either use CORS on remote domain (http://enable-cors.org/) or ' + 365 | 'place your application on the same domain as tiles (hosting ' + 366 | 'app and tiles on the same domain or running a tile proxy).'; 367 | if (window['console'] && window['console']['error']) { 368 | window['console']['error'](msg); 369 | } 370 | } 371 | this.CORSErrorReported = true; // report only once 372 | //} 373 | } 374 | }, this); 375 | } 376 | }; 377 | 378 | 379 | /** 380 | * @param {!Cesium.BillboardCollection|!Cesium.PrimitiveCollection| 381 | * !Cesium.Polygon|!Cesium.PolylineCollection} object . 382 | */ 383 | weapi.App.prototype.addPrimitive = function(object) { 384 | var composite = goog.array.findRight(this.composites, function(el, i, arr) { 385 | return el.length < weapi.App.PRIMITIVE_GROUPING_SIZE; 386 | }); 387 | 388 | if (!composite) { 389 | composite = new Cesium.PrimitiveCollection(); 390 | this.composites.push(composite); 391 | var primitives = this.scene.primitives; 392 | primitives.add(composite); 393 | primitives.raiseToTop(this.polyIconCollection); 394 | } 395 | 396 | composite.add(object); 397 | }; 398 | 399 | 400 | /** 401 | * @param {!Cesium.BillboardCollection|!Cesium.PrimitiveCollection| 402 | * !Cesium.Polygon|!Cesium.PolylineCollection} object . 403 | */ 404 | weapi.App.prototype.removePrimitive = function(object) { 405 | goog.array.forEach(this.composites, function(el, i, arr) { 406 | el['remove'](object); 407 | }); 408 | }; 409 | 410 | 411 | /** 412 | * 413 | */ 414 | weapi.App.prototype.handleResize = function() { 415 | var width = this.canvas.clientWidth; 416 | var height = this.canvas.clientHeight; 417 | 418 | if (this.canvas.width === width && this.canvas.height === height) { 419 | return; 420 | } 421 | 422 | this.canvas.width = width; 423 | this.canvas.height = height; 424 | this.scene.camera.frustum.aspectRatio = width / height; 425 | 426 | this.sceneChanged = true; 427 | }; 428 | 429 | 430 | /** 431 | * @param {!weapi.Map} map Map. 432 | */ 433 | weapi.App.prototype.setBaseMap = function(map) { 434 | var layers = this.scene.imageryLayers; 435 | //this.scene.imageryLayers.get(0) = map.layer; 436 | layers.remove(layers.get(0), false); 437 | layers.add(map.layer, 0); 438 | map.app = this; 439 | 440 | this.sceneChanged = true; 441 | }; 442 | 443 | 444 | /** 445 | * @param {weapi.Map} map Map. 446 | */ 447 | weapi.App.prototype.setOverlayMap = function(map) { 448 | var length = this.scene.imageryLayers.length; 449 | var layers = this.scene.imageryLayers; 450 | if (length > 1) { 451 | layers.remove(layers.get(1), false); 452 | } 453 | if (goog.isDefAndNotNull(map)) { 454 | layers.add(map.layer); 455 | map.app = this; 456 | } 457 | 458 | this.sceneChanged = true; 459 | }; 460 | 461 | 462 | /** 463 | * Register event listener. 464 | * @param {string} type Event type. 465 | * @param {function(Event)} listener Function to call back. 466 | * @return {goog.events.ListenableKey|null|number} listenKey. 467 | */ 468 | weapi.App.prototype.on = function(type, listener) { 469 | /** 470 | * Wraps the listener function with a wrapper function 471 | * that adds some extended event info. 472 | * @param {!weapi.App} app . 473 | * @param {function(Event)} listener Original listener function. 474 | * @return {function(Event)} Wrapper listener. 475 | * @private 476 | */ 477 | var wrap = function(app, listener) { 478 | return function(e) { 479 | e.target = app; 480 | e['latitude'] = null; 481 | e['longitude'] = null; 482 | 483 | var offsetX = e.offsetX, offsetY = e.offsetY; 484 | if (!goog.isDefAndNotNull(offsetX) || !goog.isDefAndNotNull(offsetY)) { 485 | var origE = e.getBrowserEvent(); 486 | var pageX = origE.pageX, pageY = origE.pageY, 487 | touches = origE['touches']; 488 | if (touches && touches[0] && (!pageX || !pageY)) { 489 | pageX = touches[0].pageX; 490 | pageY = touches[0].pageY; 491 | } 492 | var canvasOffset = goog.style.getPageOffset(app.canvas); 493 | offsetX = pageX - canvasOffset.x; 494 | offsetY = pageY - canvasOffset.y; 495 | } 496 | if (goog.isDefAndNotNull(offsetX) && goog.isDefAndNotNull(offsetY)) { 497 | var cartesian = app.camera.camera. 498 | pickEllipsoid(new Cesium.Cartesian2(offsetX, offsetY)); 499 | if (goog.isDefAndNotNull(cartesian)) { 500 | var carto = Cesium.Ellipsoid.WGS84.cartesianToCartographic(cartesian); 501 | 502 | var lat = goog.math.toDegrees(carto.latitude), 503 | lng = goog.math.toDegrees(carto.longitude); 504 | e['latlng'] = {'lat': lat, 'lng': lng}; 505 | e['latitude'] = lat; 506 | e['longitude'] = lng; 507 | e['altitude'] = carto.height; 508 | e['originalEvent'] = e.getBrowserEvent(); 509 | } 510 | } 511 | 512 | listener(e); 513 | }; 514 | }; 515 | var key = goog.events.listen(this.canvas, type, wrap(this, listener)); 516 | 517 | listener[goog.getUid(this) + '___eventKey_' + type] = key; 518 | 519 | return key; 520 | }; 521 | 522 | 523 | /** 524 | * Unregister event listener. 525 | * @param {string|number|null} typeOrKey Event type or listenKey. 526 | * @param {function(Event)} listener Function that was used to register. 527 | */ 528 | weapi.App.prototype.off = function(typeOrKey, listener) { 529 | if (goog.isDefAndNotNull(listener)) { 530 | var key = listener[goog.getUid(this) + '___eventKey_' + typeOrKey]; 531 | if (goog.isDefAndNotNull(key)) goog.events.unlistenByKey(key); 532 | } else if (!goog.isString(typeOrKey)) { 533 | goog.events.unlistenByKey(typeOrKey); 534 | } 535 | }; 536 | 537 | 538 | /** 539 | * Unregister all event listeners of certain type. 540 | * @param {string} type Event type. 541 | */ 542 | weapi.App.prototype.offAll = function(type) { 543 | goog.events.removeAll(this.canvas, type); 544 | }; 545 | 546 | 547 | /** 548 | * @param {number|!weapi.markers.AbstractMarker} latOrMark Latitude or marker. 549 | * @param {number} lon Longitude. 550 | * @param {string=} opt_iconUrl URL of the icon to use instead of the default. 551 | * @param {number=} opt_width Width of the icon. 552 | * @param {number=} opt_height Height of the icon. 553 | * @return {!weapi.markers.AbstractMarker} New marker. 554 | */ 555 | weapi.App.prototype.initMarker = function(latOrMark, lon, 556 | opt_iconUrl, opt_width, opt_height) { 557 | var mark; 558 | if (goog.isNumber(latOrMark)) { 559 | mark = new weapi.markers.PrettyMarker(goog.math.toRadians(latOrMark), 560 | goog.math.toRadians(lon), 561 | opt_iconUrl, opt_width, opt_height); 562 | } else { 563 | mark = latOrMark; 564 | } 565 | 566 | this.markerManager.addMarker(null, mark); 567 | 568 | this.sceneChanged = true; 569 | 570 | return mark; 571 | }; 572 | 573 | 574 | /** 575 | * @param {!weapi.markers.AbstractMarker} marker . 576 | */ 577 | weapi.App.prototype.removeMarker = function(marker) { 578 | this.markerManager.removeMarkerEx(marker); 579 | 580 | this.sceneChanged = true; 581 | }; 582 | 583 | 584 | /** 585 | * @param {number} lat Latitude in radians. 586 | * @param {number} lng Longitude in radians. 587 | * @return {Array.} Pixel data 588 | * [r 0-255, g 0-255, b 0-255, a 0-1, zoomLevel]. 589 | */ 590 | weapi.App.prototype.getBestAvailablePixelColorFromLayer = function(lat, lng) { 591 | var layer = this.scene.imageryLayers.get(0); 592 | var provider = layer.imageryProvider; 593 | var scheme = provider.tilingScheme; 594 | var position = new Cesium.Cartographic(lng, lat); 595 | 596 | var result = null; 597 | var zoom = provider.maximumLevel; 598 | 599 | while (zoom >= 0) { 600 | var tileXY = scheme['positionToTileXY'](position, zoom); 601 | var imagery = layer['getImageryFromCache'](tileXY.x, tileXY.y, 602 | zoom, undefined); 603 | if (imagery['__image__']) { 604 | var pixelX = Math.floor((tileXY.x - Math.floor(tileXY.x)) * 605 | provider.tileWidth); 606 | var pixelY = Math.floor((tileXY.y - Math.floor(tileXY.y)) * 607 | provider.tileHeight); 608 | 609 | var canvas = goog.dom.createElement('canvas'); 610 | canvas.width = 1; 611 | canvas.height = 1; 612 | var context = canvas.getContext('2d'); 613 | context.drawImage(imagery['__image__'], pixelX, pixelY, 1, 1, 0, 0, 1, 1); 614 | 615 | var data = context.getImageData(0, 0, 1, 1).data; 616 | 617 | result = [data[0], data[1], data[2], data[3] / 255, zoom]; 618 | } 619 | imagery['releaseReference'](); 620 | 621 | if (result) return result; 622 | 623 | zoom--; 624 | } 625 | 626 | return [0, 0, 0, 0, -1]; 627 | }; 628 | -------------------------------------------------------------------------------- /src/camera.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.Camera'); 9 | 10 | goog.require('goog.dom'); 11 | 12 | goog.require('weapi.CameraAnimator'); 13 | goog.require('weapi.utils'); 14 | 15 | 16 | 17 | /** 18 | * 19 | * @param {!Cesium.Camera} camera . 20 | * @param {Cesium.Ellipsoid} ellipsoid . 21 | * @constructor 22 | */ 23 | weapi.Camera = function(camera, ellipsoid) { 24 | this.camera = camera; 25 | this.ellipsoid = ellipsoid; 26 | this.animator = new weapi.CameraAnimator(this); 27 | }; 28 | 29 | 30 | /** 31 | * @return {Array.} [latitude, longitude, altitude]. 32 | */ 33 | weapi.Camera.prototype.getPos = function() { 34 | var carto = new Cesium.Cartographic(0, 0, 0); 35 | this.ellipsoid.cartesianToCartographic(this.camera.position, carto); 36 | 37 | return [carto.latitude, carto.longitude, carto.height]; 38 | }; 39 | 40 | 41 | /** 42 | * @param {number|undefined} latitude . 43 | * @param {number|undefined} longitude . 44 | * @param {number|undefined} altitude . 45 | */ 46 | weapi.Camera.prototype.setPos = function(latitude, longitude, altitude) { 47 | if (!goog.isDef(latitude) || 48 | !goog.isDef(longitude) || 49 | !goog.isDef(altitude)) { 50 | var oldPos = this.getPos(); 51 | latitude = goog.isDefAndNotNull(latitude) ? latitude : oldPos[0]; 52 | longitude = goog.isDefAndNotNull(longitude) ? longitude : oldPos[1]; 53 | altitude = altitude > 0 ? altitude : oldPos[2]; 54 | } 55 | var pos = Cesium.Cartesian3.fromRadians(longitude, latitude, altitude); 56 | this.camera.setView({ 57 | 'position': pos, 58 | 'heading': 0, 59 | 'pitch': -Math.PI / 2, 60 | 'roll': 0 61 | }); 62 | }; 63 | 64 | 65 | /** 66 | * @return {number} Heading in radians. 67 | */ 68 | weapi.Camera.prototype.getHeading = function() { 69 | var camera = this.camera; 70 | var pos = camera.positionWC; //this forces the update 71 | 72 | var normal = new Cesium.Cartesian3(-pos.y, pos.x, 0); 73 | // = Cesium.Cartesian3.UNIT_Z.cross(pos).normalize(); 74 | //var angle = (camera.right.angleBetween(normal.normalize())); 75 | var angle = Cesium.Cartesian3.angleBetween(camera.right, normal); 76 | var orientation = Cesium.Cartesian3.cross(pos, camera.up, 77 | new Cesium.Cartesian3()).z; 78 | 79 | return (orientation < 0 ? angle : -angle); 80 | }; 81 | 82 | 83 | /** 84 | * @return {number} Tilt in radians. 85 | */ 86 | weapi.Camera.prototype.getTilt = function() { 87 | var camera = this.camera; 88 | var pos = camera.positionWC; //this forces the update 89 | 90 | var angle = Math.acos(Cesium.Cartesian3.dot(camera.up, 91 | Cesium.Cartesian3.normalize(pos, new Cesium.Cartesian3()))); 92 | 93 | return -angle + Math.PI / 2; 94 | }; 95 | 96 | 97 | /** 98 | * @param {number} heading . 99 | */ 100 | weapi.Camera.prototype.setHeading = function(heading) { 101 | var heading_, tilt_ = this.getTilt(); 102 | 103 | heading_ = heading - this.getHeading(); 104 | this.camera.lookDown(tilt_); 105 | this.camera.twistLeft(heading_); 106 | this.camera.lookUp(tilt_); 107 | }; 108 | 109 | 110 | /** 111 | * @param {number} tilt . 112 | */ 113 | weapi.Camera.prototype.setTilt = function(tilt) { 114 | var tilt_ = tilt - this.getTilt(); 115 | 116 | var heading_ = this.getHeading(); 117 | this.camera.lookUp(tilt_); 118 | this.setHeading(heading_); //re-set the heading 119 | }; 120 | 121 | 122 | /** 123 | * @param {number} heading . 124 | * @param {number} tilt . 125 | */ 126 | weapi.Camera.prototype.setHeadingAndTilt = function(heading, tilt) { 127 | var heading_, tilt_ = this.getTilt(); 128 | 129 | heading_ = heading - this.getHeading(); 130 | this.camera.lookDown(tilt_); 131 | this.camera.twistLeft(heading_); 132 | this.camera.lookUp(tilt); 133 | }; 134 | 135 | 136 | /** 137 | * The most effective way to set complete camera position. 138 | * @param {number} lat . 139 | * @param {number} lng . 140 | * @param {number} alt . 141 | * @param {number} heading . 142 | * @param {number} tilt . 143 | */ 144 | weapi.Camera.prototype.setPosHeadingAndTilt = function(lat, lng, alt, 145 | heading, tilt) { 146 | var pos = Cesium.Cartesian3.fromRadians(lng, lat, alt); 147 | this.camera.setView({ 148 | 'position': pos, 149 | 'heading': 0, 150 | 'pitch': -Math.PI / 2, 151 | 'roll': 0 152 | }); 153 | this.camera.twistLeft(heading); 154 | this.camera.lookUp(tilt); 155 | }; 156 | 157 | 158 | /** 159 | * Calculates at what distance should given bounds be view to fit on screen. 160 | * @param {number} minlat . 161 | * @param {number} maxlat . 162 | * @param {number} minlon . 163 | * @param {number} maxlon . 164 | * @return {number} Proposed distance. 165 | */ 166 | weapi.Camera.prototype.calcDistanceToViewBounds = function(minlat, maxlat, 167 | minlon, maxlon) { 168 | var centerLat = (minlat + maxlat) / 2; 169 | 170 | var distEW = weapi.utils.calculateDistance(centerLat, minlon, 171 | centerLat, maxlon); 172 | 173 | var distNS = weapi.utils.calculateDistance(minlat, 0, 174 | maxlat, 0); 175 | 176 | var aspectR = 177 | Math.min(Math.max(this.camera.frustum.aspectRatio, distEW / distNS), 1.0); 178 | 179 | // Create a LookAt using the experimentally derived distance formula. 180 | var alpha = 181 | goog.math.toRadians(goog.math.toDegrees(this.camera.frustum.fovy) / 182 | (aspectR + 0.4) - 2.0); 183 | var expandToDistance = Math.max(distNS, distEW); 184 | 185 | var beta = 186 | Math.min(Math.PI / 2, 187 | alpha + expandToDistance / (2 * weapi.utils.EARTH_RADIUS)); 188 | 189 | var lookAtRange = 1.5 * weapi.utils.EARTH_RADIUS * 190 | (Math.sin(beta) * Math.sqrt(1 + 1 / Math.pow(Math.tan(alpha), 2)) - 1); 191 | 192 | return lookAtRange; 193 | }; 194 | 195 | 196 | /** 197 | * Calculates such coordinates of the camera, that when we place 198 | * the camera there with specified heading and tilt the original 199 | * [lat, lng] is in the center of the view and at specified distance. 200 | * @param {number} lat Latitude in radians. 201 | * @param {number} lng Longitude in radians. 202 | * @param {number} alt Altitude in meters. 203 | * @param {number=} opt_heading Heading in radians (otherwise 0). 204 | * @param {number=} opt_tilt Tilt in radians (otherwise 0). 205 | * @return {!Array.} Array [lat, lng] in radians. 206 | */ 207 | weapi.Camera.calculatePositionForGivenTarget = function(lat, lng, alt, 208 | opt_heading, 209 | opt_tilt) { 210 | alt /= weapi.utils.EARTH_RADIUS; 211 | var innerAngle = Math.PI - (opt_tilt || 0); 212 | 213 | // we have the following triangle: 214 | // side a=dist (desired distance of camera from the target) 215 | // side b=1 (Earth radius) 216 | // side c=1 + alt 217 | // angle gamma=innerAngle (between a and b) 218 | // Using the law of sines we can calculate beta: 219 | // sin(beta) / b = sin(gamma) / c 220 | var c = 1 + alt; 221 | var beta = Math.asin(Math.sin(innerAngle) / c); 222 | var alpha = Math.PI - beta - innerAngle; 223 | 224 | var head = opt_heading || 0; 225 | 226 | return [lat - Math.cos(head) * alpha, lng + Math.sin(head) * alpha]; 227 | }; 228 | 229 | 230 | /** 231 | * Calculates altitude from zoom (very rough approximation for deprecated API) 232 | * @param {!HTMLCanvasElement} canvas . 233 | * @param {number} fov Vertical fov in radians. 234 | * @param {number} zoom Zoom. 235 | * @param {number} latitude Latitude in radians. 236 | * @return {number} Calculated altitude. 237 | */ 238 | weapi.Camera.calcAltitudeForZoom = function(canvas, fov, zoom, latitude) { 239 | // 0.7 is old constant from WebGL Earth, 256 is tile size 240 | var tilesVertically = 0.7 * canvas.height / 256; 241 | 242 | var o = Math.cos(Math.abs(latitude)) * 2 * Math.PI; 243 | var thisPosDeformation = o / Math.pow(2, zoom); 244 | var sizeIWannaSee = thisPosDeformation * tilesVertically; 245 | return (1 / Math.tan(fov / 2)) * (sizeIWannaSee / 2) * 246 | weapi.utils.EARTH_RADIUS; 247 | }; 248 | 249 | 250 | /** 251 | * Calculates zoom from altitude (very rough approximation for deprecated API) 252 | * @param {!HTMLCanvasElement} canvas . 253 | * @param {number} fov Vertical fov in radians. 254 | * @param {number} altitude Altitude in meters. 255 | * @param {number} latitude Latitude in radians. 256 | * @return {number} Calculated zoom. 257 | */ 258 | weapi.Camera.calcZoomForAltitude = function(canvas, fov, altitude, latitude) { 259 | // 0.7 is old constant from WebGL Earth, 256 is tile size 260 | var tilesVertically = 0.7 * canvas.height / 256; 261 | 262 | var sizeISee = 2 * (altitude / weapi.utils.EARTH_RADIUS) * Math.tan(fov / 2); 263 | var sizeOfOneTile = sizeISee / tilesVertically; 264 | var o = Math.cos(Math.abs(latitude)) * 2 * Math.PI; 265 | 266 | return Math.log(o / sizeOfOneTile) / Math.LN2; 267 | }; 268 | -------------------------------------------------------------------------------- /src/cameraanimator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.CameraAnimator'); 9 | 10 | goog.require('goog.events'); 11 | goog.require('goog.fx.Animation'); 12 | goog.require('goog.fx.Animation.EventType'); 13 | goog.require('goog.fx.AnimationSerialQueue'); 14 | goog.require('goog.fx.Transition.EventType'); 15 | goog.require('goog.math'); 16 | 17 | goog.require('weapi.utils'); 18 | 19 | 20 | 21 | /** 22 | * @param {!weapi.Camera} camera Camera to be animated by this object. 23 | * @constructor 24 | */ 25 | weapi.CameraAnimator = function(camera) { 26 | 27 | /** 28 | * @type {!weapi.Camera} 29 | * @private 30 | */ 31 | this.camera_ = camera; 32 | 33 | /** 34 | * @type {goog.fx.AnimationSerialQueue} 35 | * @private 36 | */ 37 | this.animation_ = null; 38 | }; 39 | 40 | 41 | /** 42 | * @define {number} Animation duration in seconds. 43 | */ 44 | weapi.CameraAnimator.CAMERA_ANIMATION_DURATION = 3; 45 | 46 | 47 | /** 48 | * @define {number} How much can the camera climb (or descent) 49 | * (in meters of altitude per meters of surface distance). 50 | */ 51 | weapi.CameraAnimator.CAMERA_ANIMATION_MAX_ASCENT = 0.2; 52 | 53 | 54 | /** 55 | * TODO: use CameraFlightPath ? 56 | * Initiate and start the animation to given location. 57 | * @param {number} latitude Latitude in radians. 58 | * @param {number} longitude Longitude in radians. 59 | * @param {number=} opt_altitude Altitude (otherwise unchanged). 60 | * @param {number=} opt_heading Heading (otherwise 0). 61 | * @param {number=} opt_tilt Tilt (otherwise 0). 62 | * @param {boolean=} opt_targetPosition If true, the camera is position in 63 | * such a way, that the camera target is 64 | * [latitude, longitude] (default false). 65 | * @param {number=} opt_duration Duration of the animation in seconds. 66 | */ 67 | weapi.CameraAnimator.prototype.flyTo = function(latitude, longitude, 68 | opt_altitude, 69 | opt_heading, opt_tilt, 70 | opt_targetPosition, 71 | opt_duration) { 72 | var cam = this.camera_; 73 | 74 | if (opt_targetPosition) { 75 | var newPos = weapi.Camera.calculatePositionForGivenTarget( 76 | latitude, longitude, opt_altitude || cam.getPos()[2], 77 | opt_heading, opt_tilt); 78 | 79 | latitude = newPos[0]; 80 | longitude = newPos[1]; 81 | } 82 | 83 | if (goog.isDefAndNotNull(this.animation_)) { 84 | this.onEnd_(); 85 | } 86 | 87 | var srcPos = cam.getPos(); 88 | var curAlt = srcPos[2]; 89 | var dstAlt = opt_altitude || curAlt; 90 | 91 | // Validates start and end location so that 92 | // the animation goes through the shortest path. 93 | var lonStart = goog.math.modulo(srcPos[1], 2 * Math.PI); 94 | var lonEnd = goog.math.modulo(longitude, 2 * Math.PI); 95 | 96 | var lonDiff = lonStart - lonEnd; 97 | if (lonDiff < -Math.PI) { 98 | lonStart += 2 * Math.PI; 99 | } else if (lonDiff > Math.PI) { 100 | lonEnd += 2 * Math.PI; 101 | } 102 | 103 | 104 | var start_ = [srcPos[0], lonStart, curAlt, cam.getHeading(), cam.getTilt()]; 105 | var end_ = [latitude, lonEnd, dstAlt, opt_heading || 0, opt_tilt || 0]; 106 | 107 | //in-out quintic 108 | var supereasing = function(t) { 109 | var t2 = t * t; 110 | var t3 = t * t * t; 111 | return 6 * t3 * t2 + -15 * t2 * t2 + 10 * t3; 112 | }; 113 | var supereasing_f = function(t) {return 2 * supereasing(t / 2);}; 114 | var halfVal = supereasing(0.5); 115 | var supereasing_l = function(t) { 116 | return (supereasing(0.5 + t / 2) - halfVal) / (1 - halfVal); 117 | }; 118 | 119 | var animationAlteringEvents = [goog.fx.Transition.EventType.BEGIN, 120 | goog.fx.Animation.EventType.ANIMATE, 121 | goog.fx.Transition.EventType.END, 122 | goog.fx.Transition.EventType.FINISH]; 123 | 124 | this.animation_ = new goog.fx.AnimationSerialQueue(); 125 | 126 | var duration = 127 | 1000 * (opt_duration || weapi.CameraAnimator.CAMERA_ANIMATION_DURATION); 128 | 129 | if (opt_altitude) { 130 | var distance = weapi.utils.calculateDistance(srcPos[0], srcPos[1], 131 | latitude, longitude); 132 | 133 | var topPoint = Math.min(curAlt, dstAlt) + 134 | distance * weapi.CameraAnimator.CAMERA_ANIMATION_MAX_ASCENT; 135 | 136 | //Don't allow the topPoint to be lower than highest of the two points 137 | if (topPoint < Math.max(curAlt, dstAlt)) { 138 | topPoint = (curAlt + dstAlt) / 2; 139 | } 140 | 141 | var top = []; 142 | top[0] = (start_[0] + end_[0]) / 2; 143 | top[1] = (start_[1] + end_[1]) / 2; 144 | top[2] = topPoint; 145 | top[3] = (start_[3] + end_[3]) / 2; 146 | top[4] = (start_[4] + end_[4]) / 2; 147 | 148 | var ascentAnim = new goog.fx.Animation(start_, top, 149 | duration / 2, supereasing_f); 150 | 151 | var descentAnim = new goog.fx.Animation(top, end_, 152 | duration / 2, supereasing_l); 153 | 154 | goog.events.listen(ascentAnim, animationAlteringEvents, 155 | this.onEverythingAnimate_, false, this); 156 | 157 | goog.events.listen(descentAnim, animationAlteringEvents, 158 | this.onEverythingAnimate_, false, this); 159 | 160 | this.animation_.add(ascentAnim); 161 | this.animation_.add(descentAnim); 162 | } else { 163 | //single animation when altitude is not changing 164 | var anim = new goog.fx.Animation(start_, end_, duration, supereasing); 165 | 166 | goog.events.listen(anim, animationAlteringEvents, 167 | this.onEverythingAnimate_, false, this); 168 | 169 | this.animation_.add(anim); 170 | } 171 | 172 | 173 | goog.events.listen(this.animation_, goog.fx.Transition.EventType.END, 174 | this.onEnd_, false, this); 175 | 176 | this.animation_.play(); 177 | }; 178 | 179 | 180 | /** 181 | * Animate everything except altitude 182 | * @param {goog.events.Event} e The event. 183 | * @private 184 | */ 185 | weapi.CameraAnimator.prototype.onEverythingAnimate_ = function(e) { 186 | this.camera_.setPosHeadingAndTilt(e.coords[0], e.coords[1], e.coords[2], 187 | e.coords[3], e.coords[4]); 188 | }; 189 | 190 | 191 | /** 192 | * Called when animation ends 193 | * @private 194 | */ 195 | weapi.CameraAnimator.prototype.onEnd_ = function() { 196 | if (goog.isDefAndNotNull(this.animation_)) { 197 | this.animation_.dispose(); 198 | this.animation_ = null; 199 | } 200 | }; 201 | 202 | 203 | /** 204 | * If the animation is in progress, cancel it 205 | */ 206 | weapi.CameraAnimator.prototype.cancel = function() { 207 | this.onEnd_(); 208 | }; 209 | -------------------------------------------------------------------------------- /src/canvas2image.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 Klokan Technologies GmbH (info@klokantech.com) 3 | * 4 | * The JavaScript code in this page is free software: you can 5 | * redistribute it and/or modify it under the terms of the GNU 6 | * General Public License (GNU GPL) as published by the Free Software 7 | * Foundation, either version 3 of the License, or (at your option) 8 | * any later version. The code is distributed WITHOUT ANY WARRANTY; 9 | * without even the implied warranty of MERCHANTABILITY or FITNESS 10 | * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. 11 | * 12 | * USE OF THIS CODE OR ANY PART OF IT IN A NONFREE SOFTWARE IS NOT ALLOWED 13 | * WITHOUT PRIOR WRITTEN PERMISSION FROM KLOKAN TECHNOLOGIES GMBH. 14 | * 15 | * As additional permission under GNU GPL version 3 section 7, you 16 | * may distribute non-source (e.g., minimized or compacted) forms of 17 | * that code without the copy of the GNU GPL normally required by 18 | * section 4, provided you include this license notice and a URL 19 | * through which recipients can access the Corresponding Source. 20 | * 21 | */ 22 | 23 | /** 24 | * @fileoverview Serves for extended saving of canvas content as png. 25 | * 26 | * @author petr.sloup@klokantech.com (Petr Sloup) 27 | * 28 | */ 29 | 30 | goog.provide('we.canvas2image'); 31 | 32 | 33 | // Modified version of 34 | // http://purl.eligrey.com/github/canvas-toBlob.js/blob/master/canvas-toBlob.js 35 | (function(view) { 36 | 'use strict'; 37 | var is_base64_regex = /\s*;\s*base64\s*(?:;|$)/i, 38 | base64_ranks = new Uint8Array([ 39 | 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, 40 | -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 41 | 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 42 | -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 43 | 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 44 | ]), 45 | decode_base64 = function(base64) { 46 | var len = base64.length, 47 | buffer = new Uint8Array(len / 4 * 3 | 0), 48 | i = 0, 49 | outptr = 0, 50 | last = [0, 0], 51 | state = 0, 52 | save = 0; 53 | 54 | while (len--) { 55 | var code = base64.charCodeAt(i++); 56 | var rank = base64_ranks[code - 43]; 57 | if (rank !== 255 && rank !== undefined) { 58 | last[1] = last[0]; 59 | last[0] = code; 60 | save = (save << 6) | rank; 61 | state++; 62 | if (state === 4) { 63 | buffer[outptr++] = save >>> 16; 64 | if (last[1] !== 61 /* padding character */) { 65 | buffer[outptr++] = save >>> 8; 66 | } 67 | if (last[0] !== 61 /* padding character */) { 68 | buffer[outptr++] = save; 69 | } 70 | state = 0; 71 | } 72 | } 73 | } 74 | // 2/3 chance there's going to be some null bytes at the end, but that 75 | // doesn't really matter with most image formats. 76 | // If it somehow matters for you, truncate the buffer up outptr. 77 | return buffer.buffer; 78 | }; 79 | 80 | if (HTMLCanvasElement && !HTMLCanvasElement.prototype.toBlob) { 81 | HTMLCanvasElement.prototype.toBlob = function(callback, type) { 82 | if (!type) { 83 | type = 'image/png'; 84 | } if (this['mozGetAsFile']) { 85 | callback(this['mozGetAsFile']('canvas', type)); 86 | return; 87 | } 88 | var args = Array.prototype.slice.call(arguments, 1), 89 | dataURI = this.toDataURL.apply(this, args), 90 | header_end = dataURI.indexOf(','), 91 | data = dataURI.substring(header_end + 1), 92 | is_base64 = is_base64_regex.test(dataURI.substring(0, header_end)), 93 | BlobBuilder = view['BlobBuilder'] || 94 | view['WebKitBlobBuilder'] || view['MozBlobBuilder'], 95 | bb = new BlobBuilder; 96 | 97 | if (is_base64) { 98 | bb.append(decode_base64(data)); 99 | } else { 100 | bb.append(decodeURIComponent(data)); 101 | } 102 | 103 | callback(bb.getBlob(type)); 104 | }; 105 | } 106 | }(self)); 107 | 108 | // Modified version of (stripped IE support and general cleaning): 109 | // http://hackworthy.blogspot.cz/2012/05/savedownload-data-generated-in.html 110 | var showSave_; 111 | var BlobBuilder_ = window['BlobBuilder'] || 112 | window['WebKitBlobBuilder'] || window['MozBlobBuilder']; 113 | var URL_ = window['URL'] || window['webkitURL'] || window['mozURL']; 114 | var saveBlob_ = navigator['saveBlob'] || 115 | navigator['mozSaveBlob'] || navigator['webkitSaveBlob']; 116 | var saveAs_ = window['saveAs'] || window['webkitSaveAs'] || window['mozSaveAs']; 117 | 118 | if (BlobBuilder_ && (saveAs_ || saveBlob_)) { 119 | showSave_ = function(data, name, mimetype) { 120 | var builder = new BlobBuilder_(); 121 | builder.append(data); 122 | var blob = builder['getBlob'](mimetype || 'application/octet-stream'); 123 | if (!name) name = 'Download.bin'; 124 | if (saveAs_) { 125 | saveAs_(blob, name); 126 | } else { 127 | saveBlob_(blob, name); 128 | } 129 | }; 130 | } else if (BlobBuilder_ && URL_) { 131 | showSave_ = function(data, name, mimetype) { 132 | var blob, url, builder = new BlobBuilder_(); 133 | builder.append(data); 134 | if (!mimetype) mimetype = 'application/octet-stream'; 135 | if ('download' in document.createElement('a')) { 136 | blob = builder['getBlob'](mimetype); 137 | url = URL_['createObjectURL'](blob); 138 | var link = document.createElement('a'); 139 | link.setAttribute('href', url); 140 | link.setAttribute('download', name || 'Download.bin'); 141 | var event = document.createEvent('MouseEvents'); 142 | event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, 143 | false, false, false, false, 0, null); 144 | link.dispatchEvent(event); 145 | } else { 146 | // mime types that don't trigger a download when opened in a browser: 147 | var BrowserSupportedMimeTypes = { 148 | 'image/jpeg': true, 149 | 'image/png': true, 150 | 'image/gif': true, 151 | 'image/svg+xml': true, 152 | 'image/bmp': true, 153 | 'image/x-windows-bmp': true, 154 | 'image/webp': true, 155 | 'audio/wav': true, 156 | 'audio/mpeg': true, 157 | 'audio/webm': true, 158 | 'audio/ogg': true, 159 | 'video/mpeg': true, 160 | 'video/webm': true, 161 | 'video/ogg': true, 162 | 'text/plain': true, 163 | 'text/html': true, 164 | 'text/xml': true, 165 | 'application/xhtml+xml': true, 166 | 'application/json': true 167 | }; 168 | if (BrowserSupportedMimeTypes[mimetype.split(';')[0]] === true) { 169 | mimetype = 'application/octet-stream'; 170 | } 171 | blob = builder['getBlob'](mimetype); 172 | url = URL_['createObjectURL'](blob); 173 | window.open(url, '_blank', ''); 174 | } 175 | setTimeout(function() { 176 | URL_['revokeObjectURL'](url); 177 | }, 250); 178 | }; 179 | } 180 | 181 | 182 | /** 183 | * 184 | * @param {!HTMLCanvasElement} canvas . 185 | * @param {weapi.markers.MarkerManager=} opt_markerMgr . 186 | * @param {weapi.MiniGlobe=} opt_miniGlobe . 187 | * @return {!HTMLCanvasElement} Canvas with possibly additional content. 188 | */ 189 | we.canvas2image.prepareCanvas = function(canvas, opt_markerMgr, opt_miniGlobe) { 190 | var canvas_ = canvas; 191 | if (opt_markerMgr || opt_miniGlobe) { 192 | canvas_ = goog.dom.createElement('canvas'); 193 | canvas_.width = canvas.width; 194 | canvas_.height = canvas.height; 195 | var ctx = /** @type {!CanvasRenderingContext2D} */ 196 | (canvas_.getContext('2d')); 197 | ctx.drawImage(canvas, 0, 0); 198 | if (opt_markerMgr) { 199 | opt_markerMgr.forEach(function(marker) { 200 | marker.draw2D(ctx); 201 | }); 202 | } 203 | if (opt_miniGlobe) { 204 | opt_miniGlobe.drawToCanvas2D(ctx); 205 | } 206 | } 207 | return /** @type {!HTMLCanvasElement} */(canvas_); 208 | }; 209 | 210 | 211 | /** 212 | * Offers the user to save the png image 213 | * as if it was a regular download from the server. 214 | * Custom filename may not be supported by the browser. 215 | * @param {!HTMLCanvasElement} canvas . 216 | * @param {string} filename . 217 | */ 218 | we.canvas2image.saveCanvasAsPNG = function(canvas, filename) { 219 | if (showSave_ && canvas.toBlob) { 220 | canvas.toBlob(function(blob) { 221 | showSave_(blob, filename, 'image/png'); 222 | }, 'image/png'); 223 | } else if (canvas.toDataURL) { 224 | var strData = canvas.toDataURL(); 225 | document.location.href = strData.replace('image/png', 'image/octet-stream'); 226 | } 227 | }; 228 | 229 | 230 | /** 231 | * @param {!HTMLCanvasElement} canvas . 232 | * @return {string} 'data:image/png...' representation of the canvas content. 233 | */ 234 | we.canvas2image.getCanvasAsDataURL = function(canvas) { 235 | return canvas.toDataURL ? canvas.toDataURL() : 'data:image/png,'; 236 | }; 237 | -------------------------------------------------------------------------------- /src/custommap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.CustomMap'); 9 | 10 | 11 | 12 | /** 13 | * @param {Object.} opts . 14 | * @extends {Cesium.TileMapServiceImageryProvider} 15 | * @constructor 16 | */ 17 | weapi.CustomMap = function(opts) { 18 | this.setOptions(opts); 19 | }; 20 | goog.inherits(weapi.CustomMap, Cesium.TileMapServiceImageryProvider); 21 | 22 | 23 | /** 24 | * @param {Object.} opts . 25 | */ 26 | weapi.CustomMap.prototype.setOptions = function(opts) { 27 | this['_url'] = /** @type {string} */(opts['url']); 28 | 29 | this['_minimumLevel'] = /** @type {number} */(opts['minimumLevel'] || 0); 30 | this['_maximumLevel'] = /** @type {number} */(opts['maximumLevel'] || 18); 31 | 32 | this.tileSize = /** @type {number} */(opts['tileSize'] || 256); 33 | this['_tileWidth'] = this['_tileHeight'] = this.tileSize; 34 | 35 | var b = opts['bounds'], rectangle = null; 36 | if (b && b.length && b.length > 3) { 37 | rectangle = new Cesium.Rectangle(goog.math.toRadians(b[0]), 38 | goog.math.toRadians(b[1]), 39 | goog.math.toRadians(b[2]), 40 | goog.math.toRadians(b[3])); 41 | } 42 | 43 | this.flipY = /** @type {boolean} */(opts['flipY'] || false); 44 | 45 | this.subdomains = /** @type {Array.} */(opts['subdomains'] || []); 46 | 47 | this['_credit'] = new Cesium.Credit((opts['copyright'] || '').toString(), 48 | undefined, opts['copyrightLink'] ? opts['copyrightLink'].toString() : undefined); 49 | 50 | this['_proxy'] = opts['proxy'] || undefined; 51 | 52 | this.emptyTile = goog.dom.createElement('canvas'); 53 | this.emptyTile.width = this.tileSize; 54 | this.emptyTile.height = this.tileSize; 55 | 56 | this['_errorEvent'] = new Cesium.Event(); 57 | this['_tilingScheme'] = new Cesium.WebMercatorTilingScheme(); 58 | this['_rectangle'] = rectangle || this['_tilingScheme']['rectangle']; 59 | this['_ready'] = goog.isDefAndNotNull(this['_url']); 60 | }; 61 | 62 | 63 | /** 64 | * @param {number} zoom . 65 | * @param {number} x . 66 | * @param {number} y . 67 | * @return {string} . 68 | */ 69 | weapi.CustomMap.prototype.buildTileURL = function(zoom, x, y) { 70 | /** @type {string} */ 71 | var url = this.url.replace('{z}', zoom.toFixed(0)); 72 | url = url.replace('{x}', x.toFixed(0)); 73 | url = url.replace('{y}', (this.flipY ? ((1 << zoom) - y - 1) : y).toFixed(0)); 74 | if (this.subdomains.length > 0) { 75 | var subid = goog.math.modulo(x + y + zoom, this.subdomains.length); 76 | url = url.replace('{sub}', this.subdomains[subid]); 77 | } 78 | return this.proxy ? this.proxy['getURL'](url) : url; 79 | }; 80 | 81 | 82 | /** 83 | * @param {number} x . 84 | * @param {number} y . 85 | * @param {number} level . 86 | * @return {Object} . 87 | * @this {weapi.CustomMap} 88 | */ 89 | weapi.CustomMap.prototype['requestImage'] = function(x, y, level) { 90 | if (level < this.minimumLevel || 91 | level > this.maximumLevel) return this.emptyTile; 92 | var url = this.buildTileURL(level, x, y); 93 | return Cesium.ImageryProvider.loadImage(this, url); 94 | }; 95 | -------------------------------------------------------------------------------- /src/editablepolygon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.EditablePolygon'); 9 | 10 | goog.require('goog.color'); 11 | 12 | goog.require('weapi.PolyIcon'); 13 | goog.require('weapi.Polygon'); 14 | goog.require('weapi.markers.PolyDragger'); 15 | 16 | 17 | 18 | /** 19 | * @param {!weapi.App} app . 20 | * @param {!weapi.markers.MarkerManager} markermanager MarkerManager to use. 21 | * @constructor 22 | */ 23 | weapi.EditablePolygon = function(app, markermanager) { 24 | /** 25 | * @type {!weapi.App} 26 | * @protected 27 | */ 28 | this.app = app; 29 | 30 | /** 31 | * @type {!weapi.markers.MarkerManager} 32 | * @private 33 | */ 34 | this.markermanager_ = markermanager; 35 | 36 | /** 37 | * @type {!weapi.Polygon} 38 | * @private 39 | */ 40 | this.polygon_ = new weapi.Polygon(); 41 | 42 | this.app.addPrimitive(this.polygon_.primitive); 43 | this.app.addPrimitive(this.polygon_.primitiveLineCol); 44 | this.app.sceneChanged = true; 45 | 46 | /** 47 | * @type {!Object.} 48 | * @private 49 | */ 50 | this.draggers_ = {}; 51 | 52 | /** 53 | * @type {!Object.} 54 | * @private 55 | */ 56 | this.midMap_ = {}; 57 | 58 | /** 59 | * @type {!Object.} 60 | * @private 61 | */ 62 | this.midDraggers_ = {}; 63 | 64 | /** 65 | * @type {goog.events.ListenableKey|null|number} 66 | * @private 67 | */ 68 | this.clickListenKey_ = null; 69 | 70 | /** 71 | * @type {!weapi.PolyIcon} 72 | * @private 73 | */ 74 | this.icon_ = new weapi.PolyIcon(0, 0, this.app); 75 | //this.icon_.setImage('47.png', 100); 76 | 77 | /** 78 | * @type {!function()} 79 | * @private 80 | */ 81 | this.onchange_ = goog.nullFunction; 82 | 83 | /** 84 | * @type {number} 85 | * @private 86 | */ 87 | this.lastClickToAdd_ = 0; 88 | }; 89 | 90 | 91 | /** 92 | */ 93 | weapi.EditablePolygon.prototype.destroy = function() { 94 | this.disableClickToAdd(); 95 | this.app.removePrimitive(this.polygon_.primitive); 96 | this.app.removePrimitive(this.polygon_.primitiveLineCol); 97 | this.app.sceneChanged = true; 98 | this.onchange_ = goog.nullFunction; 99 | this.icon_.destroy(); 100 | goog.object.forEach(this.midDraggers_, function(el, key, obj) { 101 | this.markermanager_.removeMarker(el); 102 | }, this); 103 | goog.object.forEach(this.draggers_, function(el, key, obj) { 104 | this.markermanager_.removeMarker(el); 105 | }, this); 106 | delete this.midMap_; 107 | delete this.polygon_; 108 | }; 109 | 110 | 111 | /** 112 | */ 113 | weapi.EditablePolygon.prototype.enableClickToAdd = function() { 114 | if (goog.isDefAndNotNull(this.clickListenKey_)) return; 115 | // when mouse is down, wait for mouseup and check, if it wasn't a dragging.. 116 | this.clickListenKey_ = goog.events.listen(this.app.canvas, 117 | goog.events.EventType.MOUSEDOWN, function(e) { 118 | goog.events.listenOnce(this.app.canvas, 119 | goog.events.EventType.MOUSEUP, function(e_) { 120 | if (e_.button == 0 && !goog.isNull(this.clickListenKey_)) { 121 | if (Math.max(Math.abs(e.offsetX - e_.offsetX), 122 | Math.abs(e.offsetY - e_.offsetY)) <= 3) { 123 | var cartesian = this.app.camera.camera.pickEllipsoid( 124 | new Cesium.Cartesian2(e_.offsetX, e_.offsetY)); 125 | if (goog.isDefAndNotNull(cartesian)) { 126 | var carto = Cesium.Ellipsoid.WGS84. 127 | cartesianToCartographic(cartesian); 128 | this.addPoint(goog.math.toDegrees(carto.latitude), 129 | goog.math.toDegrees(carto.longitude)); 130 | e_.preventDefault(); 131 | this.lastClickToAdd_ = goog.now(); 132 | } 133 | } 134 | } 135 | }, false, this); 136 | }, false, this); 137 | }; 138 | 139 | 140 | /** 141 | */ 142 | weapi.EditablePolygon.prototype.disableClickToAdd = function() { 143 | goog.events.unlistenByKey(this.clickListenKey_); 144 | this.clickListenKey_ = null; 145 | }; 146 | 147 | 148 | /** 149 | * @param {string} hexColor #rrggbb. 150 | * @param {number=} opt_a [0-1], defaults to 1. 151 | */ 152 | weapi.EditablePolygon.prototype.setFillColor = function(hexColor, opt_a) { 153 | hexColor = goog.color.normalizeHex(hexColor); 154 | var r = parseInt(hexColor.substr(1, 2), 16) / 255; 155 | var g = parseInt(hexColor.substr(3, 2), 16) / 255; 156 | var b = parseInt(hexColor.substr(5, 2), 16) / 255; 157 | 158 | this.polygon_.primitive.material.uniforms['color'] = 159 | new Cesium.Color(r, g, b, opt_a); 160 | 161 | this.app.sceneChanged = true; 162 | }; 163 | 164 | 165 | /** 166 | * @param {string} hexColor #rrggbb. 167 | * @param {number=} opt_a [0-1], defaults to 1. 168 | */ 169 | weapi.EditablePolygon.prototype.setStrokeColor = function(hexColor, opt_a) { 170 | hexColor = goog.color.normalizeHex(hexColor); 171 | var r = parseInt(hexColor.substr(1, 2), 16) / 255; 172 | var g = parseInt(hexColor.substr(3, 2), 16) / 255; 173 | var b = parseInt(hexColor.substr(5, 2), 16) / 255; 174 | 175 | this.polygon_.primitiveLine.material.uniforms['color'] = 176 | new Cesium.Color(r, g, b, opt_a); 177 | 178 | this.app.sceneChanged = true; 179 | }; 180 | 181 | 182 | /** 183 | * @param {string} src URL of the image to use. 184 | * @param {number} width Desired width of the image in meters. 185 | * @param {number} height Desired height of the image in meters. 186 | */ 187 | weapi.EditablePolygon.prototype.setIcon = function(src, width, height) { 188 | this.icon_.setImage(src, width, height); 189 | this.repositionIcon_(); 190 | this.app.sceneChanged = true; 191 | }; 192 | 193 | 194 | /** 195 | * @param {!function()} onchange Function to be called whenever polygon changes. 196 | */ 197 | weapi.EditablePolygon.prototype.setOnChange = function(onchange) { 198 | this.onchange_ = onchange; 199 | }; 200 | 201 | 202 | /** 203 | * @return {boolean} Is the polygon valid (non self-intersecting,...) ? 204 | */ 205 | weapi.EditablePolygon.prototype.isValid = function() { 206 | return this.polygon_.isValid(); 207 | }; 208 | 209 | 210 | /** 211 | * @return {number} Rough area of the polygon in m^2. 212 | */ 213 | weapi.EditablePolygon.prototype.getRoughArea = function() { 214 | return this.polygon_.getRoughArea(); 215 | }; 216 | 217 | 218 | /** 219 | * in degrees 220 | * @return {{lat: number, lng: number}|null} 221 | * Centroid of the polygon or null if not valid. 222 | */ 223 | weapi.EditablePolygon.prototype.getCentroid = function() { 224 | var centroid = this.polygon_.calcCentroid(); 225 | return {'lat': centroid[1], 'lng': centroid[0]}; 226 | }; 227 | 228 | 229 | /** 230 | * in degrees 231 | * @param {number} lat . 232 | * @param {number} lng . 233 | * @return {boolean} True if inside the polygon. 234 | */ 235 | weapi.EditablePolygon.prototype.isPointIn = function(lat, lng) { 236 | //workaround: the mousedown/up events cause point adding, 237 | // but the click event can not be easily canceled so 238 | // it always causes polygon selection when click-to-adding 239 | if (goog.now() - this.lastClickToAdd_ < 100) return false; 240 | return this.polygon_.isPointIn(lat, lng); 241 | }; 242 | 243 | 244 | /** 245 | * @param {!weapi.EditablePolygon} other . 246 | * @return {boolean} True if the two polygons overlap. 247 | */ 248 | weapi.EditablePolygon.prototype.intersects = function(other) { 249 | return this.polygon_.intersects(other.polygon_); 250 | }; 251 | 252 | 253 | /** 254 | * @private 255 | */ 256 | weapi.EditablePolygon.prototype.repositionIcon_ = function() { 257 | var avg = this.polygon_.calcCentroid() || this.polygon_.calcAverage(); 258 | 259 | this.icon_.setLatLng(goog.math.toRadians(avg[1]), 260 | goog.math.toRadians(avg[0])); 261 | this.icon_.enable(this.polygon_.isValid()); 262 | }; 263 | 264 | 265 | /** 266 | * @param {boolean} visible . 267 | * @param {boolean=} opt_midOnly . 268 | */ 269 | weapi.EditablePolygon.prototype.showDraggers = function(visible, opt_midOnly) { 270 | goog.object.forEach(this.midMap_, function(el, key, obj) { 271 | el.enable(visible); 272 | }, this); 273 | if (opt_midOnly !== true) { 274 | goog.object.forEach(this.draggers_, function(el, key, obj) { 275 | this.markermanager_.getMarker(el).enable(visible); 276 | }, this); 277 | } 278 | this.app.sceneChanged = true; 279 | }; 280 | 281 | 282 | /** 283 | * @return {!Array.} . 284 | */ 285 | weapi.EditablePolygon.prototype.getPoints = function() { 286 | return this.polygon_.getAllCoords(); 287 | }; 288 | 289 | 290 | /** 291 | * Recalculates position of the two mid-edge draggers neighboring given point. 292 | * @param {number} fixedId . 293 | * @private 294 | */ 295 | weapi.EditablePolygon.prototype.repositionMidsAround_ = function(fixedId) { 296 | var neighs = this.polygon_.getNeighbors(fixedId); 297 | if (neighs.length > 0) { 298 | var coordsPrev = this.polygon_.getCoords(neighs[0]); 299 | var coordsHere = this.polygon_.getCoords(fixedId); 300 | var coordsNext = this.polygon_.getCoords(neighs[1]); 301 | this.midMap_[fixedId].lat = 302 | goog.math.toRadians((coordsHere[1] + coordsNext[1]) / 2); 303 | this.midMap_[fixedId].lon = 304 | goog.math.toRadians((coordsHere[0] + coordsNext[0]) / 2); 305 | this.midMap_[neighs[0]].lat = 306 | goog.math.toRadians((coordsPrev[1] + coordsHere[1]) / 2); 307 | this.midMap_[neighs[0]].lon = 308 | goog.math.toRadians((coordsPrev[0] + coordsHere[0]) / 2); 309 | } 310 | this.app.sceneChanged = true; 311 | }; 312 | 313 | 314 | /** 315 | * Checks, whether the polygon has just changed CW/CCW orientation 316 | * and performs necessary adjustments. 317 | * @private 318 | */ 319 | weapi.EditablePolygon.prototype.checkPointOrientationChange_ = function() { 320 | if (this.polygon_.orientationChanged()) { 321 | goog.object.forEach(this.midMap_, function(el, key, obj) { 322 | this.repositionMidsAround_(key); 323 | }, this); 324 | } 325 | }; 326 | 327 | 328 | /** 329 | * @param {!Array.} coords 330 | */ 331 | weapi.EditablePolygon.prototype.addPoints = function(coords) { 332 | var l = coords.length; 333 | for (var i = 0; i < l - 1; i++) { 334 | this.addPoint(coords[i][0], coords[i][1], undefined, undefined, true); 335 | } 336 | this.addPoint(coords[i][0], coords[i][1]); 337 | }; 338 | 339 | 340 | /** 341 | * @param {number} lat in degrees. 342 | * @param {number} lng in degrees. 343 | * @param {number=} opt_parent . 344 | * @param {boolean=} opt_fromMid . 345 | * @param {boolean=} opt_more More points coming? 346 | * @return {number} fixedId. 347 | */ 348 | weapi.EditablePolygon.prototype.addPoint = function(lat, lng, 349 | opt_parent, opt_fromMid, 350 | opt_more) { 351 | var fixedId = this.polygon_.addPoint(lat, lng, opt_parent, opt_more); 352 | 353 | if (opt_fromMid && goog.isDefAndNotNull(opt_parent)) { 354 | this.draggers_[fixedId] = this.midDraggers_[opt_parent]; 355 | delete this.midDraggers_[opt_parent]; 356 | } else { 357 | var dragger = new weapi.markers.PolyDragger( 358 | goog.math.toRadians(lat), goog.math.toRadians(lng), this.app, fixedId, 359 | goog.bind(this.movePoint, this), goog.bind(this.removePoint, this)); 360 | this.draggers_[fixedId] = this.markermanager_.addMarker(null, dragger); 361 | } 362 | this.repositionIcon_(); 363 | 364 | var neighs = this.polygon_.getNeighbors(fixedId); 365 | if (neighs.length > 0) { 366 | var adderAfter = goog.bind(function(parentP) { 367 | return goog.bind(function(lat, lng) { 368 | return this.addPoint(lat, lng, parentP, true); 369 | }, this); 370 | }, this); 371 | var mid1 = new weapi.markers.PolyDragger( 372 | goog.math.toRadians(lat), goog.math.toRadians(lng), this.app, null, 373 | goog.bind(this.movePoint, this), 374 | goog.bind(this.removePoint, this), 375 | adderAfter(fixedId)); 376 | this.midMap_[fixedId] = mid1; 377 | this.midDraggers_[fixedId] = this.markermanager_.addMarker(null, mid1); 378 | 379 | if (opt_fromMid) { 380 | var mid2 = new weapi.markers.PolyDragger( 381 | goog.math.toRadians(lat), goog.math.toRadians(lng), this.app, null, 382 | goog.bind(this.movePoint, this), 383 | goog.bind(this.removePoint, this), 384 | adderAfter(neighs[0])); 385 | this.midMap_[neighs[0]] = mid2; 386 | this.midDraggers_[neighs[0]] = this.markermanager_.addMarker(null, mid2); 387 | } 388 | this.repositionMidsAround_(neighs[0]); 389 | this.repositionMidsAround_(fixedId); 390 | this.repositionMidsAround_(neighs[1]); 391 | } 392 | 393 | this.checkPointOrientationChange_(); 394 | this.onchange_(); 395 | 396 | this.app.sceneChanged = true; 397 | 398 | return fixedId; 399 | }; 400 | 401 | 402 | /** 403 | * @param {number} fixedId . 404 | * @param {number} lat in degrees. 405 | * @param {number} lng in degrees. 406 | */ 407 | weapi.EditablePolygon.prototype.movePoint = function(fixedId, lat, lng) { 408 | this.polygon_.movePoint(fixedId, lat, lng); 409 | var marker = this.markermanager_.getMarker(this.draggers_[fixedId]); 410 | marker.lat = goog.math.toRadians(lat); 411 | marker.lon = goog.math.toRadians(lng); 412 | this.checkPointOrientationChange_(); 413 | this.repositionMidsAround_(fixedId); 414 | this.repositionIcon_(); 415 | 416 | this.app.sceneChanged = true; 417 | 418 | this.onchange_(); 419 | }; 420 | 421 | 422 | /** 423 | * @param {number} fixedId . 424 | */ 425 | weapi.EditablePolygon.prototype.removePoint = function(fixedId) { 426 | var neighs = this.polygon_.getNeighbors(fixedId); 427 | 428 | this.polygon_.removePoint(fixedId); 429 | 430 | this.repositionIcon_(); 431 | if (goog.isDefAndNotNull(this.draggers_[fixedId])) { 432 | this.markermanager_.removeMarker(this.draggers_[fixedId]); 433 | delete this.draggers_[fixedId]; 434 | 435 | delete this.midMap_[fixedId]; 436 | if (goog.isDefAndNotNull(this.midDraggers_[fixedId])) { 437 | this.markermanager_.removeMarker(this.midDraggers_[fixedId]); 438 | delete this.midDraggers_[fixedId]; 439 | } 440 | } 441 | 442 | this.checkPointOrientationChange_(); 443 | 444 | if (neighs.length > 0) { 445 | this.repositionMidsAround_(neighs[0]); 446 | this.repositionMidsAround_(fixedId); 447 | this.repositionMidsAround_(neighs[1]); 448 | } 449 | 450 | this.app.sceneChanged = true; 451 | 452 | this.onchange_(); 453 | }; 454 | -------------------------------------------------------------------------------- /src/map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.Map'); 9 | 10 | 11 | 12 | /** 13 | * @param {!Cesium.ImageryLayer} layer . 14 | * @constructor 15 | */ 16 | weapi.Map = function(layer) { 17 | /** 18 | * @type {!Cesium.ImageryLayer} 19 | */ 20 | this.layer = layer; 21 | 22 | /** 23 | * @type {?weapi.App} 24 | */ 25 | this.app = null; 26 | }; 27 | 28 | 29 | /** 30 | * @param {number} minLat Minimal latitude in degrees. 31 | * @param {number} maxLat Maximal latitude in degrees. 32 | * @param {number} minLon Minimal longitude in degrees. 33 | * @param {number} maxLon Maximal longitude in degrees. 34 | */ 35 | weapi.Map.prototype.setBoundingBox = function(minLat, maxLat, 36 | minLon, maxLon) { 37 | var extent = this.layer.imageryProvider['_rectangle']; 38 | extent.west = goog.math.toRadians(minLon); 39 | extent.south = goog.math.toRadians(goog.math.clamp(minLat, -85.051, 85.051)); 40 | extent.east = goog.math.toRadians(maxLon); 41 | extent.north = goog.math.toRadians(goog.math.clamp(maxLat, -85.051, 85.051)); 42 | if (this.app) this.app.sceneChanged = true; 43 | }; 44 | 45 | 46 | /** 47 | * @param {number} opacity Opacity. 48 | */ 49 | weapi.Map.prototype.setOpacity = function(opacity) { 50 | this.layer.alpha = opacity; 51 | if (this.app) this.app.sceneChanged = true; 52 | }; 53 | 54 | 55 | /** 56 | * @return {number} Opacity. 57 | */ 58 | weapi.Map.prototype.getOpacity = function() { 59 | return this.layer.alpha; 60 | }; 61 | -------------------------------------------------------------------------------- /src/maps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.maps'); 9 | goog.provide('weapi.maps.MapType'); 10 | 11 | goog.require('goog.Uri.QueryData'); 12 | goog.require('goog.structs.Map'); 13 | 14 | goog.require('weapi.CustomMap'); 15 | goog.require('weapi.Map'); 16 | 17 | 18 | /** 19 | * Constants for map names. 20 | * @enum {string} 21 | */ 22 | weapi.maps.MapType = { 23 | 'OSM': 'osm', 24 | 'BING': 'bing', 25 | 'WMS': 'wms', 26 | 'CUSTOM': 'custom' 27 | }; 28 | 29 | 30 | /** 31 | * @type {!goog.structs.Map} 32 | */ 33 | weapi.maps.mapMap = new goog.structs.Map(); 34 | 35 | 36 | /** 37 | * TODO: cleanup 38 | * @param {?weapi.App} app . 39 | * @param {!weapi.maps.MapType} type Type of the map. 40 | * @param {!Object.|!Array.=} opt_opts Map options. 41 | * @return {weapi.Map} Initialized TileProvider. 42 | */ 43 | weapi.maps.initMap = function(app, type, opt_opts) { 44 | 45 | /** @type {string} */ 46 | var key = type; 47 | 48 | var mapopts = null; 49 | var aropts = null; 50 | if (goog.isDefAndNotNull(opt_opts)) { 51 | if (goog.isArray(opt_opts)) { 52 | if (opt_opts.length > 0) key += opt_opts[0]; 53 | aropts = opt_opts; 54 | //alert(type + ' got array'); 55 | } else { 56 | key += (opt_opts['name'] || opt_opts['url']); 57 | mapopts = opt_opts; 58 | //alert(type + ' got object'); 59 | } 60 | } 61 | 62 | var secure = 'https:' == document.location.protocol; 63 | var protocol = (secure ? 'https:' : 'http:'); 64 | 65 | var tileProvider; 66 | 67 | switch (type) { 68 | case weapi.maps.MapType.OSM: 69 | if (!mapopts) { 70 | mapopts = {}; 71 | mapopts['url'] = protocol + '//a.tile.openstreetmap.org'; 72 | } 73 | tileProvider = new Cesium.OpenStreetMapImageryProvider(mapopts); 74 | break; 75 | case weapi.maps.MapType.BING: 76 | if (aropts) { 77 | mapopts = {}; 78 | mapopts['url'] = protocol + '//dev.virtualearth.net'; 79 | 80 | if (aropts[0] == 'Aerial') 81 | mapopts['mapStyle'] = Cesium.BingMapsStyle.AERIAL; 82 | if (aropts[0] == 'AerialWithLabels') 83 | mapopts['mapStyle'] = Cesium.BingMapsStyle.AERIAL_WITH_LABELS; 84 | if (aropts[0] == 'Road') 85 | mapopts['mapStyle'] = Cesium.BingMapsStyle.ROAD; 86 | 87 | mapopts['key'] = aropts[1]; 88 | } 89 | tileProvider = new Cesium.BingMapsImageryProvider(mapopts); 90 | break; 91 | case weapi.maps.MapType.WMS: 92 | //tileProvider = new Cesium.WebMapServiceImageryProvider(mapopts); 93 | if (aropts) { 94 | mapopts = {}; 95 | mapopts['parameters'] = {}; 96 | mapopts['url'] = aropts[1]; 97 | if (aropts[2] && aropts[2].length > 0) { 98 | mapopts['parameters']['version'] = aropts[2]; 99 | } 100 | mapopts['layers'] = aropts[3]; 101 | mapopts['parameters']['crs'] = aropts[4]; 102 | if (aropts[5] && aropts[5].length > 0) { 103 | mapopts['parameters']['format'] = aropts[5]; 104 | } 105 | if (aropts[6] && aropts[6].length > 0) { 106 | mapopts['parameters']['styles'] = aropts[6]; 107 | } 108 | if (aropts[7] && aropts[7].length > 0) { 109 | var q = new goog.Uri.QueryData(aropts[7]); 110 | goog.array.forEach(q.getKeys(), function(el, i, arr) { 111 | mapopts['parameters'][el] = q.get(el); 112 | }); 113 | } 114 | if (aropts[4] == 'EPSG:900913' || 115 | aropts[4] == 'EPSG:3857') { 116 | mapopts['tilingScheme'] = new Cesium.WebMercatorTilingScheme(); 117 | } 118 | // ignore minzoom aropts[8]; 119 | mapopts['maximumLevel'] = aropts[9]; 120 | if (app) mapopts['proxy'] = app.mapProxyObject; 121 | } 122 | tileProvider = new Cesium.WebMapServiceImageryProvider(mapopts); 123 | break; 124 | case weapi.maps.MapType.CUSTOM: 125 | if (aropts) { 126 | mapopts = {}; 127 | mapopts['url'] = aropts[1]; 128 | mapopts['maximumLevel'] = aropts[3]; 129 | mapopts['tileSize'] = aropts[4]; 130 | mapopts['flipY'] = aropts[5]; 131 | mapopts['subdomains'] = aropts[6]; 132 | mapopts['copyright'] = aropts[7]; 133 | if (app) mapopts['proxy'] = app.mapProxyObject; 134 | } 135 | tileProvider = new weapi.CustomMap(mapopts); 136 | break; 137 | default: 138 | alert('Unknown MapType \'' + type + '\' !'); 139 | return null; 140 | break; 141 | } 142 | 143 | var map = new weapi.Map(new Cesium.ImageryLayer( 144 | /** @type {!Cesium.ImageryProvider} */(tileProvider))); 145 | 146 | if (mapopts && mapopts['opacity']) { 147 | map.setOpacity(parseFloat(mapopts['opacity'])); 148 | } 149 | if (mapopts && mapopts['bounds']) { 150 | var b = mapopts['bounds']; 151 | map.setBoundingBox(b[0], b[1], b[2], b[3]); 152 | } 153 | 154 | weapi.maps.mapMap.set(key, map); 155 | if (app) app.listenCORSErrors(map.layer.imageryProvider['errorEvent']); 156 | return map; 157 | }; 158 | 159 | 160 | /** 161 | * @param {!weapi.maps.MapType} type Type of the map. 162 | * @param {string=} opt_subtype Optional subtype of the map. 163 | * @return {weapi.Map} TileProvider. 164 | */ 165 | weapi.maps.getMap = function(type, opt_subtype) { 166 | /** @type {string} */ 167 | var key = type; 168 | if (goog.isDefAndNotNull(opt_subtype)) 169 | key += opt_subtype; 170 | 171 | return /** @type {weapi.Map} */ (weapi.maps.mapMap.get(key)); 172 | }; 173 | 174 | 175 | /** 176 | * @param {!weapi.App} app . 177 | * Initializes maps that does not require any special parameters (keys etc.). 178 | */ 179 | weapi.maps.initStatics = function(app) { 180 | weapi.maps.initMap(app, weapi.maps.MapType.OSM); 181 | }; 182 | -------------------------------------------------------------------------------- /src/markers/abstractmarker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.markers.AbstractMarker'); 9 | 10 | goog.require('goog.dom'); 11 | 12 | 13 | 14 | /** 15 | * @param {number} lat Latitude to be displayed at. 16 | * @param {number} lon Longitude to be displayed at. 17 | * @param {!HTMLElement} element Element representing this marker. 18 | * @constructor 19 | */ 20 | weapi.markers.AbstractMarker = function(lat, lon, element) { 21 | /** 22 | * @type {number} 23 | */ 24 | this.lat = lat; 25 | 26 | /** 27 | * @type {number} 28 | */ 29 | this.lon = lon; 30 | 31 | /** 32 | * @type {!HTMLElement} 33 | * @protected 34 | */ 35 | this.element = element; 36 | 37 | /** 38 | * @type {Element} 39 | * @protected 40 | */ 41 | this.parentElement = null; 42 | 43 | /** 44 | * @type {boolean} 45 | * @protected 46 | */ 47 | this.enabled = true; 48 | 49 | /** 50 | * @type {boolean} 51 | * @protected 52 | */ 53 | this.visible = false; 54 | }; 55 | 56 | 57 | /** 58 | * Attaches this marker to given element. 59 | * Detaches if already attached somewhere else. 60 | * @param {!Element} parentElement Element to attach to. 61 | */ 62 | weapi.markers.AbstractMarker.prototype.attach = function(parentElement) { 63 | if (this.parentElement) { 64 | this.detach(); 65 | } 66 | this.parentElement = parentElement; 67 | this.show(true); 68 | }; 69 | 70 | 71 | /** 72 | * Detaches this marker from it's parent 73 | */ 74 | weapi.markers.AbstractMarker.prototype.detach = function() { 75 | if (this.parentElement) { 76 | this.show(false); 77 | this.parentElement = null; 78 | } 79 | }; 80 | 81 | 82 | /** 83 | * Enables/disables the marker. 84 | * @param {boolean=} opt_enabled Whether this marker is enabled or not. 85 | * Default true. 86 | */ 87 | weapi.markers.AbstractMarker.prototype.enable = function(opt_enabled) { 88 | this.enabled = opt_enabled || false; 89 | if (!this.enabled) this.show(false); 90 | }; 91 | 92 | 93 | /** 94 | * @return {boolean} Whether this marker is enabled or not. 95 | */ 96 | weapi.markers.AbstractMarker.prototype.isEnabled = function() { 97 | return this.enabled; 98 | }; 99 | 100 | 101 | /** 102 | * @return {boolean} Whether this marker is enabled or not. 103 | */ 104 | weapi.markers.AbstractMarker.prototype.isVisible = function() { 105 | return this.visible; 106 | }; 107 | 108 | 109 | /** 110 | * Shows/hides the marker. 111 | * @param {boolean=} opt_visible Whether this marker is visible or not. 112 | * Default true. 113 | */ 114 | weapi.markers.AbstractMarker.prototype.show = function(opt_visible) { 115 | var newVal = !(opt_visible === false); 116 | var change = !(this.visible == newVal); 117 | this.visible = newVal; 118 | 119 | if (change) { 120 | if (this.visible && this.parentElement) { 121 | goog.dom.appendChild(this.parentElement, this.element); 122 | } else { 123 | goog.dom.removeNode(this.element); 124 | } 125 | } 126 | }; 127 | 128 | 129 | /** 130 | * 131 | * @param {number} x X. 132 | * @param {number} y Y. 133 | */ 134 | weapi.markers.AbstractMarker.prototype.setXY = function(x, y) { 135 | this.element.style.left = x.toFixed() + 'px'; 136 | this.element.style.top = y.toFixed() + 'px'; 137 | if (this.enabled) this.show(true); 138 | }; 139 | 140 | 141 | /** 142 | * Override this method, if you want your marker 143 | * to be renderable onto 2d canvas for screenshot purposes. 144 | * @param {!CanvasRenderingContext2D} ctx . 145 | */ 146 | weapi.markers.AbstractMarker.prototype.draw2D = goog.nullFunction; 147 | -------------------------------------------------------------------------------- /src/markers/basicmarker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.markers.BasicMarker'); 9 | 10 | goog.require('goog.dom'); 11 | 12 | goog.require('weapi.markers.AbstractMarker'); 13 | 14 | 15 | 16 | /** 17 | * @inheritDoc 18 | * @extends {we.ui.markers.AbstractMarker} 19 | * @constructor 20 | */ 21 | weapi.markers.BasicMarker = function(lat, lon) { 22 | 23 | var image = 24 | 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3Zn' + 25 | 'PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjMy' + 26 | 'IiB3aWR0aD0iMTYiIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUo' + 27 | 'MCwtMTAyMC4zNjIyKSI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgZD0ibTE1' + 28 | 'LDcuODdjMCwzLjQ3LTIuNzcsNi4yOC02LjE5LDYuMjhzLTYuMTktMi44MS02LjE5LTYu' + 29 | 'MjgsMi43Ny02LjI4LDYuMTktNi4yOCw2LjE5LDIuODEsNi4xOSw2LjI4em0tNi4xNiwy' + 30 | 'My4ydi0xNi41IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0idHJhbnNsYXRl' + 31 | 'KC0wLjcwNzEwNjc4LDEwMjAuNzE1OCkiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVj' + 32 | 'YXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjFweCIgZmlsbD0iI2UxMzUxZSIvPjwvZz48' + 33 | 'L3N2Zz4='; 34 | 35 | var elementStyle = 'position:absolute;width:16px;height:32px;' + 36 | 'background-image:url(data:image/svg+xml;base64,' + image + 37 | ');margin:-32px 0 0 -8px;opacity:0.8;'; 38 | 39 | var el = goog.dom.createDom('div', {style: elementStyle}); 40 | 41 | goog.base(this, lat, lon, /** @type {!HTMLElement} */ (el)); 42 | 43 | this.show(false); 44 | }; 45 | goog.inherits(weapi.markers.BasicMarker, weapi.markers.AbstractMarker); 46 | 47 | -------------------------------------------------------------------------------- /src/markers/markermanager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.markers.MarkerManager'); 9 | 10 | goog.require('goog.structs.Map'); 11 | 12 | goog.require('weapi.markers.AbstractMarker'); 13 | goog.require('weapi.utils'); 14 | 15 | 16 | 17 | /** 18 | * @param {!weapi.App} app . 19 | * @param {!Element} element Element where markers should be 20 | * created and positioned. 21 | * @constructor 22 | */ 23 | weapi.markers.MarkerManager = function(app, element) { 24 | /** 25 | * @type {!weapi.App} 26 | * @private 27 | */ 28 | this.app_ = app; 29 | 30 | /** 31 | * @type {!Element} 32 | * @private 33 | */ 34 | this.element_ = element; 35 | 36 | /** 37 | * @type {!goog.structs.Map} 38 | * @private 39 | */ 40 | this.markerMap_ = new goog.structs.Map(); 41 | }; 42 | 43 | 44 | /** 45 | * Adds marker. 46 | * @param {?string} key Key of the marker. If null, random string is generated. 47 | * @param {!weapi.markers.AbstractMarker} marker Marker to be added. 48 | * @return {string} Key that was actually used. 49 | */ 50 | weapi.markers.MarkerManager.prototype.addMarker = function(key, marker) { 51 | var realKey = key || goog.string.getRandomString(); 52 | marker.attach(this.element_); 53 | this.markerMap_.set(realKey, marker); 54 | return realKey; 55 | }; 56 | 57 | 58 | /** 59 | * Returns marker with the given key. 60 | * @param {string} key Key of the marker. 61 | * @return {weapi.markers.AbstractMarker} Marker or undefined if key is 62 | * not present in the collection. 63 | */ 64 | weapi.markers.MarkerManager.prototype.getMarker = function(key) { 65 | return /** @type {weapi.markers.AbstractMarker}*/ (this.markerMap_.get(key)); 66 | }; 67 | 68 | 69 | /** 70 | * Removes marker, does NOT dispose of it. 71 | * @param {string} key Key of the marker. 72 | * @return {weapi.markers.AbstractMarker} Marker that was removed or undefined 73 | * if key was not present. 74 | */ 75 | weapi.markers.MarkerManager.prototype.removeMarker = function(key) { 76 | var marker = /** @type {weapi.markers.AbstractMarker}*/ 77 | (this.markerMap_.get(key)); 78 | if (goog.isDef(marker)) { 79 | marker.detach(); 80 | this.markerMap_.remove(key); 81 | } 82 | return marker; 83 | }; 84 | 85 | 86 | /** 87 | * Removes marker, does NOT dispose of it. 88 | * @param {weapi.markers.AbstractMarker} marker . 89 | */ 90 | weapi.markers.MarkerManager.prototype.removeMarkerEx = function(marker) { 91 | goog.structs.forEach(this.markerMap_, function(val, key, col) { 92 | if (val == marker) this.removeMarker(key); 93 | }, this); 94 | }; 95 | 96 | 97 | /** 98 | * Updates all markers it controls. 99 | */ 100 | weapi.markers.MarkerManager.prototype.updateMarkers = function() { 101 | goog.array.forEach(this.markerMap_.getKeys(), this.updateMarker, this); 102 | }; 103 | 104 | 105 | /** 106 | * Updates all markers it controls. 107 | * @param {string} key Key of marker to update. 108 | */ 109 | weapi.markers.MarkerManager.prototype.updateMarker = function(key) { 110 | var marker = /** @type {weapi.markers.AbstractMarker}*/ 111 | (this.markerMap_.get(key)); 112 | 113 | if (marker.isEnabled()) { 114 | //window['console']['log'](marker.lat, marker.lon); 115 | var pos = weapi.utils.getXYForLatLng(this.app_, 116 | marker.lat, marker.lon); 117 | //window['console']['log'](pos); 118 | //window.title = pos; 119 | if (goog.isDefAndNotNull(pos)) { 120 | marker.setXY(pos[0], pos[1]); 121 | marker.show(pos[2] > 0); 122 | } else { 123 | marker.show(false); 124 | } 125 | } 126 | }; 127 | 128 | 129 | /** 130 | * @param {function(!weapi.markers.AbstractMarker)} func Callback. 131 | */ 132 | weapi.markers.MarkerManager.prototype.forEach = function(func) { 133 | goog.array.forEach(this.markerMap_.getKeys(), function(el, i, arr) { 134 | var marker = /** @type {weapi.markers.AbstractMarker}*/ 135 | (this.markerMap_.get(el)); 136 | if (marker) func(marker); 137 | }, this); 138 | }; 139 | -------------------------------------------------------------------------------- /src/markers/polydragger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.markers.PolyDragger'); 9 | 10 | goog.require('goog.dom'); 11 | goog.require('goog.style'); 12 | 13 | goog.require('weapi.markers.AbstractMarker'); 14 | goog.require('weapi.utils'); 15 | 16 | 17 | 18 | /** 19 | * @inheritDoc 20 | * @param {number} lat . 21 | * @param {number} lon . 22 | * @param {!weapi.App} app . 23 | * @param {?number} fixedId . 24 | * @param {!function(number, number, number)} updateFunc . 25 | * @param {!function(number)} deleteFunc . 26 | * @param {(function(number, number) : number)=} opt_createFunc . 27 | * @extends {weapi.markers.AbstractMarker} 28 | * @constructor 29 | */ 30 | weapi.markers.PolyDragger = function(lat, lon, app, fixedId, 31 | updateFunc, deleteFunc, opt_createFunc) { 32 | var marker = goog.dom.createDom('div', {'class': 33 | 'we-polydragger-' + (goog.isDefAndNotNull(fixedId) ? 'a' : 'b')}); 34 | 35 | goog.base(this, lat, lon, /** @type {!HTMLElement} */ (marker)); 36 | 37 | this.show(false); 38 | 39 | goog.events.listen(marker, goog.events.EventType.MOUSEDOWN, function(e_) { 40 | if (e_.button == 0) { 41 | goog.events.listen(app.canvas, 42 | goog.events.EventType.MOUSEMOVE, function(e) { 43 | var carte = app.camera.camera. 44 | pickEllipsoid(new Cesium.Cartesian2(e.offsetX, e.offsetY)); 45 | if (goog.isDefAndNotNull(carte)) { 46 | var carto = Cesium.Ellipsoid.WGS84.cartesianToCartographic(carte); 47 | this.lat = goog.math.toDegrees(carto.latitude); 48 | this.lon = goog.math.toDegrees(carto.longitude); 49 | this.setXY(e.offsetX, e.offsetY); //for smoother dragging 50 | if (goog.isDefAndNotNull(fixedId)) { 51 | updateFunc(fixedId, this.lat, this.lon); 52 | } else if (opt_createFunc) { 53 | fixedId = opt_createFunc(this.lat, this.lon); 54 | marker.className = 'we-polydragger-a'; 55 | } 56 | e.preventDefault(); 57 | } 58 | }, false, this); 59 | e_.preventDefault(); 60 | } 61 | }, false, this); 62 | 63 | goog.events.listen(marker, goog.events.EventType.CLICK, function(e) { 64 | if (e.altKey && goog.isDefAndNotNull(fixedId)) { 65 | deleteFunc(fixedId); 66 | e.preventDefault(); 67 | } 68 | }, false, this); 69 | 70 | goog.events.listen(marker, goog.events.EventType.MOUSEUP, function(e) { 71 | goog.events.removeAll(app.canvas, 72 | goog.events.EventType.MOUSEMOVE); 73 | }, false, this); 74 | }; 75 | goog.inherits(weapi.markers.PolyDragger, weapi.markers.AbstractMarker); 76 | 77 | 78 | weapi.utils.installStyles( 79 | '.we-polydragger-a{position:absolute;width:8px;height:8px;z-index:100;' + 80 | 'margin-left:-4px;margin-top:-4px;background-color:#36f;' + 81 | 'cursor:pointer;border:1px solid blue;}' 82 | ); 83 | weapi.utils.installStyles( 84 | '.we-polydragger-b{position:absolute;width:6px;height:6px;z-index:99;' + 85 | 'margin-left:-3px;margin-top:-3px;background-color:rgba(180,220,250,0.9);' + 86 | 'cursor:pointer;border:1px solid blue;}' 87 | ); 88 | -------------------------------------------------------------------------------- /src/markers/popup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.markers.Popup'); 9 | 10 | goog.require('goog.dom'); 11 | goog.require('goog.style'); 12 | 13 | goog.require('weapi.utils'); 14 | 15 | 16 | 17 | /** 18 | * @param {string} contentHTML HTML content of the popup. 19 | * @param {number=} opt_maxWidth Maximal width of the popup (default = 300). 20 | * @param {boolean=} opt_closeButton Create close button? (default = true). 21 | * @constructor 22 | */ 23 | weapi.markers.Popup = function(contentHTML, opt_maxWidth, opt_closeButton) { 24 | 25 | 26 | var content = goog.dom.createDom('div', {'class': 'we-pp-content'}); 27 | content.innerHTML = contentHTML; 28 | 29 | var contentwrap = goog.dom.createDom('div', 30 | {'class': 'we-pp-wrapper'}, content); 31 | 32 | var tipcontainer = goog.dom.createDom( 33 | 'div', {'class': 'we-pp-tip-cont'}, 34 | goog.dom.createDom('div', {'class': 'we-pp-tip'})); 35 | 36 | /** 37 | * @type {!HTMLElement} 38 | * @private 39 | */ 40 | this.popup_ = /** @type {!HTMLElement} */ 41 | (goog.dom.createDom('div', {'class': 'we-pp'})); 42 | 43 | if (opt_closeButton !== false) { 44 | var closebutton = goog.dom.createDom('a', 45 | {'class': 'we-pp-close', 'href': '#'}); 46 | closebutton.onclick = goog.bind(this.show, this, false); 47 | goog.dom.appendChild(this.popup_, closebutton); 48 | } 49 | goog.dom.appendChild(this.popup_, contentwrap); 50 | goog.dom.appendChild(this.popup_, tipcontainer); 51 | 52 | var width = (opt_maxWidth || 300) + 2 * 20; // compensation for margin+border 53 | this.popup_.style.width = width.toFixed(0) + 'px'; 54 | this.popup_.style.left = (-width / 2).toFixed(0) + 'px'; 55 | 56 | this.show(false); 57 | }; 58 | 59 | 60 | /** 61 | * @return {!HTMLElement} Element. 62 | */ 63 | weapi.markers.Popup.prototype.getElement = function() { 64 | return this.popup_; 65 | }; 66 | 67 | 68 | /** 69 | * Adjust some properties of this popup. 70 | * @param {number} markerHeight Height of the marker this popup is attached to. 71 | */ 72 | weapi.markers.Popup.prototype.adjust = function(markerHeight) { 73 | this.popup_.style.bottom = markerHeight.toFixed(0) + 'px'; 74 | }; 75 | 76 | 77 | /** 78 | * Shows or hides the popup. 79 | * @param {boolean=} opt_visible Visible? If not given, toggle. 80 | */ 81 | weapi.markers.Popup.prototype.show = function(opt_visible) { 82 | var visible = goog.isDefAndNotNull(opt_visible) ? 83 | opt_visible : parseFloat(this.popup_.style.opacity) !== 1; 84 | if (visible) { 85 | this.popup_.style.opacity = 1.0; 86 | this.popup_.style.visibility = 'visible'; 87 | } else { 88 | this.popup_.style.opacity = 0.0; 89 | 90 | //this breaks the fade-out animation, but is important to fix dragging 91 | //TODO: timeout and then visibility:hidden ? 92 | this.popup_.style.visibility = 'hidden'; 93 | } 94 | }; 95 | 96 | 97 | weapi.utils.installStyles( 98 | '.we-pp-content p{margin:18px 0;text-align:justify;}' + 99 | '.we-pp-wrapper{padding:1px;text-align:left;border-radius:12px;}' + 100 | '.we-pp{z-index:100;-webkit-transition:opacity 0.2s linear;' + 101 | '-moz-transition:opacity 0.2s linear;-o-transition:opacity 0.2s linear;' + 102 | 'transition:opacity 0.2s linear;position:absolute;}' + 103 | '.we-pp-wrapper,.we-pp-tip{background:white;box-shadow:0 1px 10px #888;' + 104 | '-moz-box-shadow:0 1px 10px #888;-webkit-box-shadow:0 1px 14px #999;}' + 105 | '.we-pp-close{background-image:url(data:image/png;base64,iVBORw0KGgoAAAAN' + 106 | 'SUhEUgAAAAoAAAAKCAMAAAC67D+PAAAAk1BMVEX////Ny8vNy8vNy8vNy8vNy8vNy8vNy8vN' + 107 | 'y8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vN' + 108 | 'y8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vNy8vN' + 109 | 'y8vNy8vNy8vNy8vNy8sw0horAAAAMHRSTlMA/bGBFK1LgjellwUx2+VZo73SE6z5fQYOtzLR' + 110 | 'pxI22CC4g6ieV+yAEc8/ZocEHTzU+GNbAAAAV0lEQVQI1wXBBQKDMADAwLRl7szdgCks/3/d' + 111 | '7hh0ehl0W21IwZgNR44nTNVZribmC1WXqzVstmpIALu9Gg5HOJ1VNV64arjd1YKy8sEz+nrD' + 112 | '51tD0//xB/w6CnrIHetcAAAAAElFTkSuQmCC);position:absolute;top:9px;' + 113 | 'right:9px;width:10px;height:10px;overflow:hidden;}' + 114 | '.we-pp-content{display:inline-block;margin:13px 19px;' + 115 | 'font:12px/1.4 "Helvetica Neue",Arial,Helvetica,sans-serif;}' + 116 | '.we-pp-tip-cont{margin:0 auto;width:40px;height:16px;position:relative;' + 117 | 'overflow:hidden;}' + 118 | '.we-pp-tip{width:15px;height:15px;padding:1px;margin:-8px auto 0;' + 119 | '-moz-transform:rotate(45deg);-webkit-transform:rotate(45deg);' + 120 | '-o-transform:rotate(45deg);transform:rotate(45deg);}' 121 | ); 122 | -------------------------------------------------------------------------------- /src/markers/prettymarker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.markers.PrettyMarker'); 9 | 10 | goog.require('goog.dom'); 11 | goog.require('goog.style'); 12 | 13 | goog.require('weapi.markers.AbstractMarker'); 14 | goog.require('weapi.markers.Popup'); 15 | goog.require('weapi.utils'); 16 | 17 | 18 | 19 | /** 20 | * @inheritDoc 21 | * @param {number} lat . 22 | * @param {number} lon . 23 | * @param {string=} opt_iconUrl URL of the icon to use instead of the default. 24 | * @param {number=} opt_width Width of the icon. 25 | * @param {number=} opt_height Height of the icon. 26 | * @extends {weapi.markers.AbstractMarker} 27 | * @constructor 28 | */ 29 | weapi.markers.PrettyMarker = function(lat, lon, 30 | opt_iconUrl, opt_width, opt_height) { 31 | 32 | var marker = goog.dom.createDom('div', {'class': 'we-pm-icon'}); 33 | 34 | if (goog.isString(opt_iconUrl)) 35 | marker.style.backgroundImage = 'url(' + opt_iconUrl + ')'; 36 | 37 | this.width_ = opt_width || 25; 38 | this.height_ = opt_height || 41; 39 | 40 | marker.style.width = this.width_.toFixed(0) + 'px'; 41 | marker.style.height = this.height_.toFixed(0) + 'px'; 42 | marker.style.marginLeft = (-this.width_ / 2).toFixed(0) + 'px'; 43 | marker.style.marginTop = (-this.height_).toFixed(0) + 'px'; 44 | 45 | //wrapper for marker and popup 46 | var elwrap = goog.dom.createDom('div', {style: 'position:absolute;'}, 47 | marker); 48 | 49 | goog.base(this, lat, lon, /** @type {!HTMLElement} */ (elwrap)); 50 | 51 | /** 52 | * @type {weapi.markers.Popup} 53 | * @private 54 | */ 55 | this.popup_ = null; 56 | 57 | this.show(false); 58 | this.showPopup(false); 59 | 60 | marker.onclick = goog.bind(this.showPopup, this, undefined); 61 | 62 | }; 63 | goog.inherits(weapi.markers.PrettyMarker, weapi.markers.AbstractMarker); 64 | 65 | 66 | /** 67 | * Bind the popup to this marker. 68 | * @param {!weapi.markers.Popup} popup Popup to bind. 69 | */ 70 | weapi.markers.PrettyMarker.prototype.attachPopup = function(popup) { 71 | if (this.popup_) goog.dom.removeNode(this.popup_.getElement()); 72 | 73 | this.popup_ = popup; 74 | goog.dom.appendChild(this.element, this.popup_.getElement()); 75 | popup.adjust(this.height_); 76 | }; 77 | 78 | 79 | /** 80 | * Shows or hides the popup. 81 | * @param {boolean=} opt_visible Visible? If not given, toggle. 82 | */ 83 | weapi.markers.PrettyMarker.prototype.showPopup = function(opt_visible) { 84 | if (this.popup_) { 85 | //var popup = this.popup_.getElement(); 86 | //center the popup 87 | //popup.style.left = -Math.round((popup.offsetWidth - 88 | // this.element.offsetWidth) / 2) + 'px'; 89 | 90 | this.popup_.show(opt_visible); 91 | } 92 | }; 93 | 94 | 95 | weapi.utils.installStyles( 96 | '.we-pm-icon{position:absolute;z-index:64;background-image:url(data:image' + 97 | '/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFtklEQVR42p3W' + 98 | 'a0xTZxgH8CYkS5aQLGFxcXNZNNu4FIFCK9eCWOQm3lDizLYP+zTnxLk5wQpICxRaLuWi3FpQ' + 99 | 'FMU55xDkjiBTFMeA9Zy2JzMxWUZisoS4TyRuQvG/pydQKb3Q2uSXw3nf93n+5z2nLRVIiga8' + 100 | '8RERL/MlAgDrosJ+d/zISTJMEFt2B9LyEV5kySBo7G+iI9vdhyj7nfEhcjKfUvcAma0z+Pwa' + 101 | '5+BwuxG7myaRUH4XtHaAhLgI6VvLjwwn14zj0CUWn13lkHXZhPQWBml6e3svGq3zvIyGX0F1' + 102 | 'z0mWQ4hY0buaH3mcUvsAn17hcKDNhFQdsy4K5Nfv108jsXIM1OOoqxAfMppMAYepIF3PIrmZ' + 103 | '8cqhy2ZkXjBYQyxE9iqksGeFXKa9h0PtHF0di6Qmxhk+fKeLOcLX72r8DdRvjvjxIRGFt638' + 104 | 'yPz+Vha7W0zY0cDYydAbkXWJ4+1rYWx/H2gzO6wl/JysehzUUw0+5Oxtq+zEyns42MYhsZ6x' + 105 | 'Q2N0C0yQae9DouhFZt1dOvYgnt5RGU3T/Dw1tqtJ1xn5Gur7D/ERRBR0W43vapzhd5FwnrHZ' + 106 | '02Kmt+jvkBTeRsOdPzD/7wJWXkPGp0hSD2BnzUMcuMjZ1RF+LFY1BOotFYQXdPkSy/5WDvF1' + 107 | 'DKSr8GOau7g1/RecvWafzSOuuBf79CYkNxodandox0G9VYLw/FvCSGUf9rZQci1js6PeiPRG' + 108 | 'Aw7WjTgNePnyJa9mgJ5h1QPsWVOf0kTBdZOg/p0CUV6nNKp4ALv1HKJrGJu0Znqo1RNQds64' + 109 | 'DLBYLBjjniK6ZMihXtZgRPK5KVD/UYHozM+R1p1k6DhEaRmblAYK0U4g/8aUy4DFxUUMMLOI' + 110 | 'UQ071CfVm5BUOwnq30shNzeJC7qwq5nDtirGJq6GRWoDixR1HxYsS3YhS0tLWFhY4OVdf4TE' + 111 | 'qgmkNprt6ukiafwhqL9OECb/yYfMpzaaEKllIa5kbNKa6D6XjqK632i3C9oBH3DPPIttyn5+' + 112 | 'XUy1Y22cegzUO18QdvqGVWdC5SNK5xBezqyg3dB9pW1Hl4wgu20cM3/O4b+FRTx++gwNgywk' + 113 | 'eTfhrE5cwfBj4oJuUG+hIDT3RyuZpLAXO+s5hGkYOzHVRn5cWv4Q4rM91iJE5HchpnQMsjrW' + 114 | 'WY1tPfUd5j/xoTnXV0zEV05Cds6MUDVjR6RhkXSecxBTbXJYG0ZojnbRA+opXQ75YUWmpLCf' + 115 | 'ro7D1lLmdfH1Us0EqN+o7Vs45NS1FT7kSXzVDGK1ZghVzGtJpJAI2gX1Sn4V8n3HakcliiEk' + 116 | '1HIILGG8FldtRqxmEtRn2u6f1taTHau9SebitCzCNUb4FzNekdZwCC/oA/XIsg/57upaKrFi' + 117 | 'BHFaDh8rGY9FVZgRrZ4C1T8mPnYhwd9eWWsDeR5TaUKwyogPFYxHYqo4iPL7QbVf8M3tQ9qd' + 118 | '0UUoxhBdyWFLIbOucLUJkeoZUN0secMx5MRlZwKIhW4BXSWLzWcZt6IqrLsYBNWccPq7S/jN' + 119 | 'JVe6wxX3sU1jxgf5BpeExUZIygyg9XPE13nI8TZXtm89dRNiCnk/z+AKPx+aNwhaX+jyZ2pQ' + 120 | '9kV3JkTKRxCVmrDpjMGBv4JFRBkLWjdP3nITcsGdT4JzuiAqM+NduWEtfjzkzBBoXQXf0GXI' + 121 | 'sVZ3fMiTsKIpBBYZsfG0wWZzAQuRygSaf0E2ug0J/LplPdnBOT0IVZnxTq5hBX8eLB8Gzev4' + 122 | 'Zu5D9OvxJXMhRQZsoavfkEPPQ84gpMQEGreQLeuGBBzVe0IVlDuA4GIz3j5l4I9B8hHQeIfb' + 123 | 'AFvIVzpPbCQvhEoT3pOzEBaZEZh9BTQW4mFIs6d0gbl3EKSkAPkY6LyXCDwLOdLsKWHAsXYE' + 124 | 'KswIOH4NdC4lnoX4H2nyRnfA6THQ8Zflcw9Dvmz0xnb/7A7QMW353OMQb7USgTch/wNGSWfR' + 125 | 'l/HE1wAAAABJRU5ErkJggg==);}' 126 | ); 127 | -------------------------------------------------------------------------------- /src/miniglobe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.MiniGlobe'); 9 | 10 | goog.require('goog.dom'); 11 | 12 | 13 | 14 | /** 15 | * @param {!weapi.App} app . 16 | * @param {number} latBands . 17 | * @param {number} lngBands . 18 | * @param {string} textureUrl . 19 | * @constructor 20 | */ 21 | weapi.MiniGlobe = function(app, latBands, lngBands, textureUrl) { 22 | /** 23 | * @type {!weapi.App} 24 | * @private 25 | */ 26 | this.app_ = app; 27 | 28 | /** 29 | * @type {!HTMLCanvasElement} 30 | */ 31 | this.canvas = 32 | /** @type {!HTMLCanvasElement} */(goog.dom.createElement('canvas')); 33 | 34 | var par = this.app_.canvas.parentElement || window.document; 35 | goog.dom.append(par, this.canvas); 36 | 37 | var opts = {'depth': false, 'preserveDrawingBuffer': true}; 38 | 39 | /** 40 | * @type {?WebGLRenderingContext} 41 | */ 42 | this.gl = /** @type {?WebGLRenderingContext} */ 43 | (this.canvas.getContext('webgl', opts) || 44 | this.canvas.getContext('experimental-webgl', opts)); 45 | 46 | var gl = this.gl; 47 | 48 | gl.enable(gl.CULL_FACE); 49 | gl.cullFace(gl.BACK); 50 | gl.clearColor(0.0, 0.0, 0.0, 0.0); 51 | 52 | this.vertexBuffer_ = gl.createBuffer(); 53 | 54 | var radius = 1; 55 | var vertexPositionData = [], textureCoordData = [], indexData = []; 56 | for (var latNumber = 0; latNumber <= latBands; latNumber++) { 57 | var theta = latNumber * Math.PI / latBands; 58 | var sinTheta = Math.sin(theta); 59 | var cosTheta = Math.cos(theta); 60 | 61 | for (var longNumber = 0; longNumber <= lngBands; longNumber++) { 62 | var phi = longNumber * 2 * Math.PI / lngBands; 63 | phi -= Math.PI / 2; 64 | var sinPhi = Math.sin(phi); 65 | var cosPhi = Math.cos(phi); 66 | 67 | var x = cosPhi * sinTheta; 68 | var y = cosTheta; 69 | var z = sinPhi * sinTheta; 70 | var u = 1 - (longNumber / lngBands); 71 | var v = 1 - (latNumber / latBands); 72 | 73 | textureCoordData.push(u); 74 | textureCoordData.push(v); 75 | vertexPositionData.push(radius * x); 76 | vertexPositionData.push(radius * y); 77 | vertexPositionData.push(radius * z); 78 | } 79 | } 80 | 81 | for (var latNumber = 0; latNumber < latBands; latNumber++) { 82 | for (var longNumber = 0; longNumber < lngBands; longNumber++) { 83 | var first = (latNumber * (lngBands + 1)) + longNumber; 84 | var second = first + lngBands + 1; 85 | indexData.push(first + 1); 86 | indexData.push(second); 87 | indexData.push(first); 88 | 89 | indexData.push(first + 1); 90 | indexData.push(second + 1); 91 | indexData.push(second); 92 | } 93 | } 94 | 95 | this.vertexBuffer_ = gl.createBuffer(); 96 | gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer_); 97 | gl.bufferData(gl.ARRAY_BUFFER, 98 | new Float32Array(vertexPositionData), gl.STATIC_DRAW); 99 | this.vertexBuffer_.itemSize = 3; 100 | this.vertexBuffer_.numItems = vertexPositionData.length / 3; 101 | 102 | this.texCoordBuffer_ = gl.createBuffer(); 103 | gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer_); 104 | gl.bufferData(gl.ARRAY_BUFFER, 105 | new Float32Array(textureCoordData), gl.STATIC_DRAW); 106 | this.texCoordBuffer_.itemSize = 2; 107 | this.texCoordBuffer_.numItems = textureCoordData.length / 2; 108 | 109 | this.indexBuffer_ = gl.createBuffer(); 110 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer_); 111 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, 112 | new Uint16Array(indexData), gl.STATIC_DRAW); 113 | this.indexBuffer_.itemSize = 1; 114 | this.indexBuffer_.numItems = indexData.length; 115 | 116 | /** 117 | * @type {?WebGLTexture} 118 | * @private 119 | */ 120 | this.texture_ = null; 121 | 122 | var image_ = new Image(); 123 | image_.onload = goog.bind(function() { 124 | this.texture_ = gl.createTexture(); 125 | gl.activeTexture(gl.TEXTURE0); 126 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); 127 | gl.bindTexture(gl.TEXTURE_2D, this.texture_); 128 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image_); 129 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 130 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 131 | }, this); 132 | image_.src = textureUrl; 133 | 134 | var fsCode = 'precision lowp float;' + 135 | 'varying vec2 vTextureCoord;' + 136 | 'uniform sampler2D uSampler;' + 137 | 'void main(){gl_FragColor=texture2D(uSampler,vTextureCoord);}'; 138 | var vsCode = 'precision mediump float;' + 139 | 'attribute vec3 aVertexPosition;' + 140 | 'attribute vec2 aTextureCoord;' + 141 | 'uniform mat4 uMVMatrix;' + 142 | 'uniform mat4 uPMatrix;' + 143 | 'uniform float uAspect;' + 144 | 'varying vec2 vTextureCoord;' + 145 | 'void main(){' + 146 | 'gl_Position=uPMatrix*uMVMatrix*vec4(aVertexPosition,1.0);' + 147 | 'gl_Position.x*=uAspect;' + 148 | 'gl_Position.z=0.0;' + 149 | 'vTextureCoord=aTextureCoord;' + 150 | '}'; 151 | 152 | var createShader = function(shaderCode, shaderType) { 153 | var shader = gl.createShader(shaderType); 154 | 155 | gl.shaderSource(shader, shaderCode); 156 | gl.compileShader(shader); 157 | 158 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 159 | throw Error('Shader err: ' + gl.getShaderInfoLog(shader)); 160 | } else if (goog.isNull(shader)) { 161 | throw Error('Unknown'); 162 | } 163 | 164 | return shader; 165 | }; 166 | 167 | var fsshader = createShader(fsCode, gl.FRAGMENT_SHADER); 168 | var vsshader = createShader(vsCode, gl.VERTEX_SHADER); 169 | 170 | this.program_ = gl.createProgram(); 171 | if (goog.isNull(this.program_)) { 172 | throw Error('Unknown'); 173 | } 174 | gl.attachShader(this.program_, vsshader); 175 | gl.attachShader(this.program_, fsshader); 176 | 177 | gl.bindAttribLocation(this.program_, 0, 'aVertexPosition'); 178 | 179 | gl.linkProgram(this.program_); 180 | 181 | if (!gl.getProgramParameter(this.program_, gl.LINK_STATUS)) { 182 | throw Error('Shader program err: ' + 183 | gl.getProgramInfoLog(this.program_)); 184 | } 185 | 186 | gl.useProgram(this.program_); 187 | 188 | this.vertexPositionAttribute = 189 | gl.getAttribLocation(this.program_, 'aVertexPosition'); 190 | this.textureCoordAttribute = 191 | gl.getAttribLocation(this.program_, 'aTextureCoord'); 192 | 193 | this.aspectUniform = gl.getUniformLocation(this.program_, 'uAspect'); 194 | this.pMatrixUniform = gl.getUniformLocation(this.program_, 'uPMatrix'); 195 | this.mvMatrixUniform = gl.getUniformLocation(this.program_, 'uMVMatrix'); 196 | this.samplerUniform = gl.getUniformLocation(this.program_, 'uSampler'); 197 | 198 | /** 199 | * @type {number} 200 | * @private 201 | */ 202 | this.size_ = 128; 203 | 204 | /** 205 | * @type {number} 206 | * @private 207 | */ 208 | this.padding_ = 0.1; 209 | }; 210 | 211 | 212 | /** 213 | * @param {number} size Size of the mini globe in pixels. 214 | * @param {number=} opt_padding Relative padding (0.1 ~= 10% padding). 215 | */ 216 | weapi.MiniGlobe.prototype.setSize = function(size, opt_padding) { 217 | this.size_ = size; 218 | if (goog.isDefAndNotNull(opt_padding)) this.padding_ = opt_padding; 219 | this.canvas.width = this.size_; 220 | this.canvas.height = this.size_; 221 | this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); 222 | 223 | var pad = this.padding_ * this.size_; 224 | this.canvas.style.cssText = 'position:absolute;z-index:10000;' + 225 | 'pointer-events:none;' + 226 | 'right:' + pad + 'px;bottom:' + pad + 'px;'; 227 | }; 228 | 229 | 230 | /** 231 | * @param {!CanvasRenderingContext2D} dst . 232 | */ 233 | weapi.MiniGlobe.prototype.drawToCanvas2D = function(dst) { 234 | var cornerOff = this.size_ * (1 + this.padding_); 235 | var x = this.app_.canvas.width - cornerOff; 236 | var y = this.app_.canvas.height - cornerOff; 237 | dst.drawImage(this.canvas, x, y); 238 | }; 239 | 240 | 241 | /** 242 | * Draw 243 | */ 244 | weapi.MiniGlobe.prototype.draw = function() { 245 | if (!goog.isDefAndNotNull(this.texture_)) return; 246 | var gl = this.gl; 247 | gl.useProgram(this.program_); 248 | 249 | gl.activeTexture(gl.TEXTURE0); 250 | gl.bindTexture(gl.TEXTURE_2D, this.texture_); 251 | gl.uniform1i(this.samplerUniform, 0); 252 | 253 | var rotate100 = function(what, angle) { 254 | var c = Math.cos(angle), s = Math.sin(angle); 255 | 256 | Cesium.Matrix4.multiply(what, new Cesium.Matrix4( 257 | 1, 0, 0, 0, 258 | 0, c, -s, 0, 259 | 0, s, c, 0, 260 | 0, 0, 0, 1 261 | ), what); 262 | }; 263 | 264 | var rotate010 = function(what, angle) { 265 | var c = Math.cos(angle), s = Math.sin(angle); 266 | 267 | Cesium.Matrix4.multiply(what, new Cesium.Matrix4( 268 | c, 0, s, 0, 269 | 0, 1, 0, 0, 270 | -s, 0, c, 0, 271 | 0, 0, 0, 1 272 | ), what); 273 | }; 274 | 275 | var pos = this.app_.camera.getPos(); 276 | var frustum = this.app_.camera.camera.frustum; 277 | 278 | var mvm = Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3( 279 | 0, 0, -1.3 / Math.tan(frustum.fovy / 2))); 280 | rotate100(mvm, pos[0]); 281 | rotate010(mvm, -pos[1]); 282 | 283 | 284 | var pm = new Float32Array(goog.array.flatten( 285 | Cesium.Matrix4.toArray(frustum.projectionMatrix))); 286 | 287 | gl.uniformMatrix4fv(this.mvMatrixUniform, false, new Float32Array( 288 | goog.array.flatten(Cesium.Matrix4.toArray(mvm)))); 289 | gl.uniformMatrix4fv(this.pMatrixUniform, false, pm); 290 | gl.uniform1f(this.aspectUniform, frustum.aspectRatio); 291 | 292 | gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer_); 293 | gl.vertexAttribPointer(this.vertexPositionAttribute, 294 | this.vertexBuffer_.itemSize, gl.FLOAT, false, 0, 0); 295 | 296 | gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer_); 297 | gl.vertexAttribPointer(this.textureCoordAttribute, 298 | this.texCoordBuffer_.itemSize, gl.FLOAT, false, 0, 0); 299 | 300 | gl.enableVertexAttribArray(this.vertexPositionAttribute); 301 | gl.enableVertexAttribArray(this.textureCoordAttribute); 302 | 303 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer_); 304 | gl.drawElements(gl.TRIANGLES, this.indexBuffer_.numItems, 305 | gl.UNSIGNED_SHORT, 0); 306 | 307 | gl.disableVertexAttribArray(this.vertexPositionAttribute); 308 | gl.disableVertexAttribArray(this.textureCoordAttribute); 309 | }; 310 | -------------------------------------------------------------------------------- /src/polygon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebGL Earth API - Polygon Example 8 | 9 | 10 | 11 | 12 | 137 | 138 | 139 | 140 |
141 |
142 |
143 |
144 |
145 | 146 | 147 | 148 |
149 | 150 | 151 | 152 | 153 |
154 | Change fill color: 155 | 163 | Change border color: 164 | 172 |
173 | 174 | 175 |
176 | 177 | 178 |
179 | 180 | 181 | 182 |
183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /src/polygon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @fileoverview Object representing single polygon + useful operations. 4 | * The polygon can even be concave, the class calculates 5 | * a triangulation of the polygon to be able to render it. 6 | * 7 | * @author petr.sloup@klokantech.com (Petr Sloup) 8 | * 9 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 10 | */ 11 | 12 | goog.provide('weapi.Polygon'); 13 | 14 | 15 | 16 | /** 17 | * @constructor 18 | */ 19 | weapi.Polygon = function() { 20 | /** 21 | * @type {!Cesium.Polygon} 22 | */ 23 | this.primitive = new Cesium.Polygon({'asynchronous': false}); 24 | 25 | /** 26 | * @type {!Cesium.PolylineCollection} 27 | */ 28 | this.primitiveLineCol = new Cesium.PolylineCollection(); 29 | 30 | /** 31 | * @type {!Cesium.Polyline} 32 | */ 33 | this.primitiveLine = this.primitiveLineCol.add(); 34 | 35 | /** 36 | * @type {?weapi.Polygon.Node} 37 | * @private 38 | */ 39 | this.head_ = null; 40 | 41 | /** 42 | * @type {!Array.} 43 | * @private 44 | */ 45 | this.vertices_ = []; 46 | 47 | /** 48 | * @type {number} 49 | * @private 50 | */ 51 | this.numVertices_ = 0; 52 | 53 | this.primitive.material.uniforms['color'] = new Cesium.Color(1, 0, 0, .8); 54 | 55 | this.primitiveLine.material.uniforms['color'] = 56 | new Cesium.Color(0, 0, 0, 1); 57 | this.primitiveLine.width = 2; 58 | 59 | /** 60 | * @type {number} 61 | * @private 62 | */ 63 | this.roughArea_ = 0; 64 | 65 | /** 66 | * @type {boolean} 67 | * @private 68 | */ 69 | this.valid_ = false; 70 | 71 | /** 72 | * @type {boolean} 73 | * @private 74 | */ 75 | this.pointSwitchFlag_ = false; 76 | 77 | /** 78 | * @type {!Array.>} 79 | * @private 80 | */ 81 | this.triangulation_ = []; 82 | }; 83 | 84 | 85 | /** 86 | * @return {boolean} True if the polygon is valid (non self-intersecting). 87 | */ 88 | weapi.Polygon.prototype.isValid = function() { 89 | return this.valid_; 90 | }; 91 | 92 | 93 | /** 94 | * @return {boolean} True if the polygon CCW/CW orientation was just changed. 95 | */ 96 | weapi.Polygon.prototype.orientationChanged = function() { 97 | var oldVal = this.pointSwitchFlag_; 98 | this.pointSwitchFlag_ = false; 99 | return oldVal; 100 | }; 101 | 102 | 103 | /** 104 | * @return {number} Rough area of the polygon in m^2. 105 | */ 106 | weapi.Polygon.prototype.getRoughArea = function() { 107 | return this.roughArea_; 108 | }; 109 | 110 | 111 | /** 112 | * @param {number} lat in degrees. 113 | * @param {number} lng in degrees. 114 | * @param {number=} opt_parent Defaults to the last point. 115 | * @param {boolean=} opt_more More points coming? 116 | * @return {number} Fixed ID of the new point. 117 | */ 118 | weapi.Polygon.prototype.addPoint = function(lat, lng, opt_parent, opt_more) { 119 | var vert = new weapi.Polygon.Node(lat, lng); 120 | 121 | if (this.numVertices_ == 0) { 122 | this.head_ = vert; 123 | vert.next = vert; 124 | vert.prev = vert; 125 | } else { 126 | var parent = this.vertices_[ 127 | goog.math.clamp(goog.isDefAndNotNull(opt_parent) ? 128 | opt_parent : Number.MAX_VALUE, 129 | 0, this.vertices_.length - 1)]; 130 | if (!parent) { 131 | parent = this.head_.prev; 132 | } 133 | vert.next = parent.next; 134 | parent.next = vert; 135 | vert.prev = parent; 136 | vert.next.prev = vert; 137 | } 138 | this.vertices_.push(vert); 139 | vert.fixedId = this.vertices_.length - 1; 140 | this.numVertices_++; 141 | 142 | if (opt_more !== true) { 143 | this.rebufferPoints_(); 144 | this.solveTriangles_(); 145 | } 146 | 147 | return vert.fixedId; 148 | }; 149 | 150 | 151 | /** 152 | * @param {number} fixedId . 153 | * @return {!Array.} . 154 | */ 155 | weapi.Polygon.prototype.getNeighbors = function(fixedId) { 156 | var vert = this.vertices_[fixedId]; 157 | if (!vert) return []; 158 | 159 | return [vert.prev.fixedId, vert.next.fixedId]; 160 | }; 161 | 162 | 163 | /** 164 | * in degrees 165 | * @param {number} fixedId . 166 | * @return {!Array.} . 167 | */ 168 | weapi.Polygon.prototype.getCoords = function(fixedId) { 169 | var vert = this.vertices_[fixedId]; 170 | if (!vert) return []; 171 | 172 | var mod = 180 / Math.PI; 173 | return [vert.x * mod, vert.y * mod]; 174 | }; 175 | 176 | 177 | /** 178 | * in degrees 179 | * @return {!Array.} . 180 | */ 181 | weapi.Polygon.prototype.getAllCoords = function() { 182 | var mod = 180 / Math.PI; 183 | var result = []; 184 | 185 | var vrt = this.head_; 186 | do { 187 | result.push({'lat': vrt.y * mod, 'lng': vrt.x * mod}); 188 | vrt = vrt.next; 189 | } while (vrt != this.head_); 190 | 191 | return result; 192 | }; 193 | 194 | 195 | /** 196 | * @param {number} fixedId . 197 | * @param {number} lat in degrees. 198 | * @param {number} lng in degrees. 199 | */ 200 | weapi.Polygon.prototype.movePoint = function(fixedId, lat, lng) { 201 | var vert = this.vertices_[fixedId]; 202 | if (!vert) return; 203 | 204 | vert.setLatLng(lat, lng); 205 | 206 | this.rebufferPoints_(); 207 | this.solveTriangles_(); 208 | }; 209 | 210 | 211 | /** 212 | * @param {number} fixedId . 213 | */ 214 | weapi.Polygon.prototype.removePoint = function(fixedId) { 215 | var vert = this.vertices_[fixedId]; 216 | if (vert) { 217 | if (this.head_ == vert) { 218 | this.head_ = (vert == vert.next) ? null : vert.next; 219 | } 220 | vert.next.prev = vert.prev; 221 | vert.prev.next = vert.next; 222 | delete this.vertices_[fixedId]; 223 | this.numVertices_--; 224 | 225 | this.rebufferPoints_(); 226 | this.solveTriangles_(); 227 | } 228 | }; 229 | 230 | 231 | /** 232 | * @return {!Array.} Coords of the average of the nodes. 233 | */ 234 | weapi.Polygon.prototype.calcAverage = function() { 235 | if (!this.head_) return [0, 0]; 236 | var x = 0, y = 0, i = 0; 237 | var vrt = this.head_; 238 | do { 239 | x += vrt.x; 240 | y += vrt.y; 241 | i++; 242 | vrt = vrt.next; 243 | } while (vrt != this.head_); 244 | i = i / 180 * Math.PI; 245 | return [x/i, y/i]; 246 | }; 247 | 248 | 249 | /** 250 | * @return {Array.} Centroid of the polygon or null if not valid. 251 | */ 252 | weapi.Polygon.prototype.calcCentroid = function() { 253 | if (!this.isValid()) return null; 254 | 255 | var x = 0, y = 0, area = 0; 256 | var vrt = this.head_; 257 | do { 258 | var p1 = vrt, p2 = vrt.next; 259 | var f = p1.x * p2.y - p2.x * p1.y; 260 | x += (p1.x + p2.x) * f; 261 | y += (p1.y + p2.y) * f; 262 | area += f; 263 | 264 | vrt = vrt.next; 265 | } while (vrt != this.head_); 266 | area /= 2; 267 | 268 | var f = (6 * area) / (180 / Math.PI); 269 | return [x / f, y / f]; 270 | }; 271 | 272 | 273 | /** 274 | * @param {number} lat in degrees. 275 | * @param {number} lng in degrees. 276 | * @return {boolean} True if inside the polygon. 277 | */ 278 | weapi.Polygon.prototype.isPointIn = function(lat, lng) { 279 | var p1x = lng / 180 * Math.PI; 280 | var p1y = lat / 180 * Math.PI; 281 | 282 | var sign_ = function(p2, p3) { 283 | return (p1x - p3.x) * (p2.y - p3.y) - 284 | (p2.x - p3.x) * (p1y - p3.y); 285 | }; 286 | var found = false; 287 | goog.array.forEach(this.triangulation_, function(el, i, arr) { 288 | var b1 = sign_(el[0], el[1]) < 0; 289 | var b2 = sign_(el[1], el[2]) < 0; 290 | var b3 = sign_(el[2], el[0]) < 0; 291 | 292 | if ((b1 == b2) && (b2 == b3)) found = true; 293 | }); 294 | return found; 295 | }; 296 | 297 | 298 | /** 299 | * Test polygon<->polygon intersection based on the triangulations. 300 | * TODO: Could be optimized, but seems to be performing very well. 301 | * @param {!weapi.Polygon} other . 302 | * @return {boolean} True if the two polygons overlap. 303 | */ 304 | weapi.Polygon.prototype.intersects = function(other) { 305 | var lineInter = function(a, b, c, d) { 306 | //test ab<->cd intersection 307 | var denom = (d.y - c.y) * (b.x - a.x) - (d.x - c.x) * (b.y - a.y); 308 | var aycy = (a.y - c.y), axcx = (a.x - c.x); 309 | var p = ((d.x - c.x) * aycy - (d.y - c.y) * axcx) / denom; 310 | var t = ((b.x - a.x) * aycy - (b.y - a.y) * axcx) / denom; 311 | 312 | return (p > 0 && p < 1 && t > 0 && t < 1); 313 | }; 314 | var isPointIn = function(p, t) { 315 | var sign_ = function(p2, p3) { 316 | return (p.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p.y - p3.y); 317 | }; 318 | 319 | var b1 = sign_(t[0], t[1]) < 0; 320 | var b2 = sign_(t[1], t[2]) < 0; 321 | var b3 = sign_(t[2], t[0]) < 0; 322 | 323 | return (b1 == b2) && (b2 == b3); 324 | }; 325 | return goog.array.find(this.triangulation_, function(triA, iA, arrA) { 326 | return goog.array.find(other.triangulation_, function(triB, iB, arrB) { 327 | // any of the edges intersect (3x3 possibilities) 328 | if (lineInter(triA[0], triA[1], triB[0], triB[1]) || 329 | lineInter(triA[0], triA[1], triB[1], triB[2]) || 330 | lineInter(triA[0], triA[1], triB[2], triB[0]) || 331 | lineInter(triA[1], triA[2], triB[0], triB[1]) || 332 | lineInter(triA[1], triA[2], triB[1], triB[2]) || 333 | lineInter(triA[1], triA[2], triB[2], triB[0]) || 334 | lineInter(triA[2], triA[0], triB[0], triB[1]) || 335 | lineInter(triA[2], triA[0], triB[1], triB[2]) || 336 | lineInter(triA[2], triA[0], triB[2], triB[0])) return true; 337 | 338 | // all points of A are inside B 339 | if (isPointIn(triA[0], triB) && 340 | isPointIn(triA[1], triB) && 341 | isPointIn(triA[2], triB)) return true; 342 | 343 | // all points of B are inside A 344 | if (isPointIn(triB[0], triA) && 345 | isPointIn(triB[1], triA) && 346 | isPointIn(triB[2], triA)) return true; 347 | 348 | return false; 349 | }) !== null; 350 | }) !== null; 351 | }; 352 | 353 | 354 | /** 355 | * Buffers the points into GPU buffer. 356 | * @private 357 | */ 358 | weapi.Polygon.prototype.rebufferPoints_ = function() { 359 | var vertices = new Array(); 360 | 361 | if (!this.head_) return; 362 | if (this.numVertices_ < 3) return; 363 | // recalc temporary ids 364 | /*var vrt = this.head_; 365 | var nextId = 0; 366 | do { 367 | vrt.tmpId = nextId++; 368 | vrt = vrt.next; 369 | } while (vrt != this.head_); 370 | 371 | goog.array.forEach(this.vertices_, function(el, i, arr) { 372 | if (!el) return; 373 | vertices[3 * el.tmpId + 0] = el.projX; 374 | vertices[3 * el.tmpId + 1] = el.projY; 375 | vertices[3 * el.tmpId + 2] = el.projZ; 376 | }); 377 | 378 | var gl = this.gl; 379 | gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer_); 380 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);*/ 381 | 382 | var cartoArray = []; 383 | var vrt = this.head_; 384 | do { 385 | cartoArray.push(new Cesium.Cartographic(vrt.x, vrt.y)); 386 | vrt = vrt.next; 387 | } while (vrt != this.head_); 388 | 389 | var carteArray = 390 | Cesium.Ellipsoid.WGS84.cartographicArrayToCartesianArray(cartoArray); 391 | this.primitive.positions = carteArray; 392 | //this.primitive.update(); 393 | carteArray.push(carteArray[0]); 394 | this.primitiveLine.positions = carteArray; 395 | }; 396 | 397 | 398 | /** 399 | * @private 400 | */ 401 | weapi.Polygon.prototype.solveTriangles_ = function() { 402 | //return; 403 | var n = this.numVertices_; 404 | this.roughArea_ = 0; 405 | this.triangulation_ = []; 406 | 407 | //this.context.sceneChanged = true; 408 | 409 | this.valid_ = false; 410 | //test intersection of segments 411 | if (n > 2) { 412 | //point p such that p<->p.next is not intersected by any other 413 | var cleanpoint = null; 414 | this.valid_ = true; 415 | var a = this.head_; 416 | do { 417 | var localValid = true; 418 | var b = a.next; 419 | var c = this.head_; 420 | do { 421 | var d = c.next; 422 | //test ab<->cd intersection 423 | var denom = (d.y - c.y) * (b.x - a.x) - (d.x - c.x) * (b.y - a.y); 424 | var p = ((d.x - c.x) * (a.y - c.y) - (d.y - c.y) * (a.x - c.x)) / denom; 425 | var t = ((b.x - a.x) * (a.y - c.y) - (b.y - a.y) * (a.x - c.x)) / denom; 426 | 427 | if (p > 0 && p < 1 && t > 0 && t < 1) { 428 | localValid = false; 429 | } 430 | 431 | c = d; 432 | } while (localValid && c != this.head_); 433 | if (localValid) cleanpoint = a; 434 | this.valid_ = this.valid_ && localValid; 435 | a = b; 436 | } while (a != this.head_); 437 | 438 | if (!this.valid_ && n == 4) { 439 | //although some lines intersect, we can still solve this for 4 points 440 | // by simply swapping some points (unrolling around the clean edge) 441 | var swapPoints = function(p1, p2) { 442 | var p1_prev = p1.prev; 443 | var p2_next = p2.next; 444 | p1.prev = p2; 445 | p2.next = p1; 446 | 447 | p1.next = p2_next; 448 | p2.prev = p1_prev; 449 | 450 | p2_next.prev = p1; 451 | p1_prev.next = p2; 452 | }; 453 | if (cleanpoint) swapPoints(cleanpoint, cleanpoint.next); 454 | 455 | this.rebufferPoints_(); 456 | this.valid_ = true; 457 | this.pointSwitchFlag_ = true; 458 | } 459 | } 460 | this.primitive.show = this.valid_; 461 | if (!this.valid_) return; 462 | 463 | var signedArea = 0; 464 | if (n > 0) { 465 | var a = this.head_; 466 | do { 467 | var b = a.next; 468 | signedArea += a.x * b.y - a.y * b.x; 469 | a = b; 470 | } while (a != this.head_); 471 | } 472 | 473 | //NOTE: this area is wrong, but the sign is correct 474 | if (signedArea > 0) { 475 | //CCW ! reverse the points 476 | this.pointSwitchFlag_ = true; 477 | for (var i = 0; i < this.vertices_.length; ++i) { 478 | var v = this.vertices_[i]; 479 | if (v) { 480 | var tmp = v.next; 481 | v.next = v.prev; 482 | v.prev = tmp; 483 | } 484 | } 485 | this.rebufferPoints_(); 486 | } 487 | 488 | var triangles = []; 489 | var addTriangle = goog.bind(function(v1, v2, v3) { 490 | triangles.push([v1.tmpId, v2.tmpId, v3.tmpId]); 491 | this.triangulation_.push([v1, v2, v3]); 492 | 493 | // Calculate triangle area using Heron's formula 494 | var len = function(u, v) { 495 | var x_ = u.projX - v.projX; 496 | var y_ = u.projY - v.projY; 497 | var z_ = u.projZ - v.projZ; 498 | return Math.sqrt(x_ * x_ + y_ * y_ + z_ * z_); 499 | }; 500 | var a = len(v1, v2), b = len(v2, v3), c = len(v3, v1); 501 | var s = (a + b + c) / 2; 502 | var T = Math.sqrt(s * (s - a) * (s - b) * (s - c)); 503 | this.roughArea_ += T; 504 | }, this); 505 | 506 | // Triangulation -- ear clipping method 507 | if (n < 3) { 508 | //triangles = []; 509 | } else if (n == 3) { 510 | addTriangle(this.head_, this.head_.prev, this.head_.next); 511 | } else { 512 | var head = this.head_; 513 | 514 | var Area2 = function(a, b, c) { //Calulates signed area via cross product 515 | return -((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)); 516 | }; 517 | 518 | var Left = function(a, b, c) {return (Area2(a, b, c) > 0);}; //c left of ab 519 | var LeftOn = function(a, b, c) {return (Area2(a, b, c) >= 0);}; 520 | var Collinear = function(a, b, c) {return (Area2(a, b, c) == 0);}; 521 | var XOR = function(a, b) {return (a || b) && !(a && b);}; 522 | 523 | var IntersectProp = function(a, b, c, d) { //Check proper intersection 524 | if (Collinear(a, b, c) || Collinear(a, b, d) || 525 | Collinear(c, d, a) || Collinear(c, d, b)) 526 | return false; 527 | return XOR(Left(a, b, c), Left(a, b, d)) && 528 | XOR(Left(c, d, a), Left(c, d, b)); 529 | }; 530 | 531 | var InCone = function(a, b) { //Is line ab interal 532 | var a0 = a._prev, a1 = a._next; 533 | if (LeftOn(a, a1, a0)) 534 | return Left(a, b, a0) && Left(b, a, a1); 535 | return !(LeftOn(a, b, a1) && LeftOn(b, a, a0)); 536 | }; 537 | 538 | var Diagonalie = function(a, b) { 539 | var c = head, c1; 540 | do { 541 | c1 = c._next; 542 | if ((c != a) && (c1 != a) && (c != b) && (c1 != b) && 543 | IntersectProp(a, b, c, c1)) { 544 | return false; 545 | } 546 | c = c._next; 547 | } while (c != head); 548 | return true; 549 | }; 550 | 551 | var Diagonal = function(a, b) { 552 | return InCone(a, b) && InCone(b, a) && Diagonalie(a, b); 553 | }; 554 | 555 | var v0, v1, v2, v3, v4; 556 | 557 | goog.array.forEach(this.vertices_, function(el, i, arr) { 558 | if (el) { 559 | el._next = el.next; 560 | el._prev = el.prev; 561 | } 562 | }); 563 | 564 | v1 = this.head_; 565 | do { 566 | v2 = v1._next; 567 | v0 = v1._prev; 568 | v1._ear = Diagonal(v0, v2); 569 | v1 = v1._next; 570 | } while (v1 != this.head_); 571 | 572 | var z = 99; 573 | while (z > 0 && n > 3) { 574 | z--; 575 | v2 = head; 576 | var y = 99; 577 | var broke; 578 | do { 579 | broke = false; 580 | if (v2._ear) { 581 | v3 = v2._next; 582 | v4 = v3._next; 583 | v1 = v2._prev; 584 | v0 = v1._prev; 585 | addTriangle(v3, v2, v1); 586 | v1._ear = Diagonal(v0, v3); 587 | v3._ear = Diagonal(v1, v4); 588 | v1._next = v3; 589 | v3._prev = v1; 590 | head = v3; //In case we cut out the head! 591 | n--; 592 | broke = true; 593 | } 594 | v2 = v2._next; 595 | y--; 596 | } while (y > 0 && !broke && v2 != head); 597 | } 598 | if (v1 && v3 && v4) { 599 | addTriangle(v4, v3, v1); 600 | } 601 | } 602 | 603 | this.roughArea_ *= weapi.utils.EARTH_RADIUS * weapi.utils.EARTH_RADIUS; 604 | }; 605 | 606 | 607 | 608 | /** 609 | * @param {number} lat in degrees. 610 | * @param {number} lng in degrees. 611 | * @param {weapi.Polygon.Node=} opt_next . 612 | * @param {weapi.Polygon.Node=} opt_prev . 613 | * @constructor 614 | */ 615 | weapi.Polygon.Node = function(lat, lng, opt_next, opt_prev) { 616 | /** @type {number} */ 617 | this.x = 0; 618 | /** @type {number} */ 619 | this.y = 0; 620 | 621 | /** @type {number} */ 622 | this.projX = 0; 623 | /** @type {number} */ 624 | this.projY = 0; 625 | /** @type {number} */ 626 | this.projZ = 0; 627 | 628 | this.setLatLng(lat, lng); 629 | 630 | /** @type {?weapi.Polygon.Node} */ 631 | this.next = opt_next || null; 632 | /** @type {?weapi.Polygon.Node} */ 633 | this.prev = opt_prev || null; 634 | 635 | /** @type {number} */ 636 | this.fixedId = -1; 637 | /** @type {number} */ 638 | this.tmpId = -1; 639 | 640 | /** @type {boolean} */ 641 | this._ear = false; 642 | /** @type {?weapi.Polygon.Node} */ 643 | this._next = this.next; 644 | /** @type {?weapi.Polygon.Node} */ 645 | this._prev = this.prev; 646 | }; 647 | 648 | 649 | /** 650 | * @param {number} lat in degrees. 651 | * @param {number} lng in degrees. 652 | */ 653 | weapi.Polygon.Node.prototype.setLatLng = function(lat, lng) { 654 | this.x = lng / 180 * Math.PI; 655 | this.y = lat / 180 * Math.PI; 656 | 657 | var cosy = Math.cos(this.y); 658 | this.projX = Math.sin(this.x) * cosy; 659 | this.projY = Math.sin(this.y); 660 | this.projZ = Math.cos(this.x) * cosy; 661 | }; 662 | -------------------------------------------------------------------------------- /src/polyicon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.PolyIcon'); 9 | 10 | goog.require('goog.dom'); 11 | goog.require('weapi.markers.Popup'); 12 | 13 | 14 | 15 | /** 16 | * @param {number} lat in radians. 17 | * @param {number} lng in radians. 18 | * @param {!weapi.App} app . 19 | * @constructor 20 | */ 21 | weapi.PolyIcon = function(lat, lng, app) { 22 | /** 23 | * @type {!weapi.App} 24 | */ 25 | this.app = app; 26 | 27 | /** 28 | * @type {?Cesium.Billboard} 29 | */ 30 | this.billboard = null; 31 | 32 | /** 33 | * @type {number} 34 | * @private 35 | */ 36 | this.lat_ = 0; 37 | 38 | /** 39 | * @type {number} 40 | * @private 41 | */ 42 | this.lng_ = 0; 43 | 44 | this.setLatLng(lat, lng); 45 | 46 | /** 47 | * @type {string} 48 | */ 49 | this.src = ''; 50 | }; 51 | 52 | 53 | /** 54 | * @define {number} Reference distance (in meters) for the icon size. 55 | */ 56 | weapi.PolyIcon.REFERENCE_DISTANCE = 1000; 57 | 58 | 59 | /** 60 | * @define {number} Reference canvas height (in pixels). 61 | */ 62 | weapi.PolyIcon.REFERENCE_CANVAS_HEIGHT = 768; 63 | 64 | 65 | /** 66 | * @param {number} lat in radians. 67 | * @param {number} lng in radians. 68 | */ 69 | weapi.PolyIcon.prototype.setLatLng = function(lat, lng) { 70 | this.lat_ = lat; 71 | this.lng_ = lng; 72 | 73 | if (this.billboard) { 74 | var position = Cesium.Ellipsoid.WGS84.cartographicToCartesian( 75 | new Cesium.Cartographic(this.lng_, this.lat_)); 76 | 77 | this.billboard.position = position; 78 | } 79 | }; 80 | 81 | 82 | /** 83 | * @param {boolean} enable . 84 | */ 85 | weapi.PolyIcon.prototype.enable = function(enable) { 86 | if (this.billboard) { 87 | this.billboard.show = enable; 88 | } 89 | }; 90 | 91 | 92 | /** 93 | */ 94 | weapi.PolyIcon.prototype.destroy = function() { 95 | if (this.billboard) { 96 | this.app.polyIconCollection.remove(this.billboard); 97 | this.billboard = null; 98 | } 99 | }; 100 | 101 | 102 | /** 103 | * @param {string} src URL of the image to use. 104 | * @param {number} width Desired width of the image in meters. 105 | * @param {number} height Desired height of the image in meters. 106 | */ 107 | weapi.PolyIcon.prototype.setImage = function(src, width, height) { 108 | if (src.length > 0) { 109 | if (!this.billboard) { 110 | this.billboard = this.app.polyIconCollection.add(); 111 | this.setLatLng(this.lat_, this.lng_); 112 | } 113 | this.billboard.image = src; 114 | this.billboard.width = width; 115 | this.billboard.height = height; 116 | this.billboard.sizeInMeters = true; 117 | this.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM; 118 | 119 | this.setLatLng(this.lat_, this.lng_); 120 | this.app.sceneChanged = true; 121 | } else { 122 | if (this.billboard) { 123 | this.app.polyIconCollection.remove(this.billboard); 124 | this.billboard = null; 125 | } 126 | } 127 | }; 128 | 129 | 130 | /** 131 | * @param {number} x . 132 | * @param {number} y . 133 | * @return {boolean} . 134 | */ 135 | weapi.PolyIcon.prototype.isPointIn = function(x, y) { 136 | //TODO: (?) 137 | return false; 138 | }; 139 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author petr.sloup@klokantech.com (Petr Sloup) 4 | * 5 | * Copyright 2013 Klokan Technologies Gmbh (www.klokantech.com) 6 | */ 7 | 8 | goog.provide('weapi.utils'); 9 | 10 | 11 | /** 12 | * @const {number} Simplified Earth radius 13 | */ 14 | weapi.utils.EARTH_RADIUS = 6378137; 15 | 16 | 17 | /** 18 | * TODO: fix for other ellipsoids 19 | * Calculates distance of two points on the surface. 20 | * @param {number} lat1 . 21 | * @param {number} lon1 . 22 | * @param {number} lat2 . 23 | * @param {number} lon2 . 24 | * @return {number} Calculated distance. 25 | */ 26 | weapi.utils.calculateDistance = function(lat1, lon1, lat2, lon2) { 27 | var sindlathalf = Math.sin((lat2 - lat1) / 2); 28 | var sindlonhalf = Math.sin((lon2 - lon1) / 2); 29 | var a = sindlathalf * sindlathalf + 30 | Math.cos(lat1) * Math.cos(lat2) * sindlonhalf * sindlonhalf; 31 | var angle = 2 * Math.asin(Math.sqrt(a)); 32 | return weapi.utils.EARTH_RADIUS * angle; 33 | }; 34 | 35 | 36 | /** 37 | * @param {!weapi.App} app . 38 | * @param {number} lat Latitude in radians. 39 | * @param {number} lng Longitude in radians. 40 | * @param {number=} opt_alt Altitude in meters. 41 | * @return {Array.} Array [x, y, visibility] or null. 42 | */ 43 | weapi.utils.getXYForLatLng = function(app, lat, lng, opt_alt) { 44 | var cam = app.camera.camera; 45 | var pos = new Cesium.Cartographic(lng, lat, opt_alt || 0); 46 | var cartes3 = Cesium.Ellipsoid.WGS84.cartographicToCartesian(pos); 47 | var cartes4 = new Cesium.Cartesian4(cartes3.x, cartes3.y, cartes3.z, 1); 48 | 49 | var mvp = app.scene.context.uniformState.modelViewProjection; 50 | 51 | var proj = Cesium.Matrix4.multiplyByVector(mvp, cartes4, 52 | new Cesium.Cartesian4()); 53 | 54 | if (!goog.isDefAndNotNull(proj)) return null; 55 | 56 | var w = 1 / proj.w; 57 | var x = (proj.x * w + 1) / 2; 58 | var y = 1 - ((proj.y * w) + 1) / 2; 59 | 60 | var visibility = 1; 61 | if (x < -0.1 || x > 1.1 || 62 | y < -0.1 || y > 1.1) { 63 | visibility = 0; 64 | } else { 65 | var direction = Cesium.Cartesian3.subtract(cartes3, cam.position, cartes3); 66 | var distance = Cesium.Cartesian3.magnitude(direction); 67 | 68 | var ldotc = -Cesium.Cartesian3.dot( 69 | Cesium.Cartesian3.normalize(direction, new Cesium.Cartesian3()), 70 | cam.position); 71 | var cdotc = Cesium.Cartesian3.dot(cam.position, cam.position); 72 | 73 | var r = weapi.utils.EARTH_RADIUS; 74 | var val = ldotc * ldotc - cdotc + r * r; 75 | 76 | var d1, d2; 77 | if (val < 0) { 78 | return null; 79 | } else { 80 | d1 = Math.min(ldotc + Math.sqrt(val), ldotc - Math.sqrt(val)); 81 | d2 = Math.max(ldotc + Math.sqrt(val), ldotc - Math.sqrt(val)); 82 | } 83 | 84 | visibility = (Math.abs(distance - d1) < Math.abs(distance - d2)) ? 1 : 0; 85 | } 86 | 87 | return [x * app.canvas.width, y * app.canvas.height, visibility]; 88 | }; 89 | 90 | 91 | /** 92 | * TODO: Use goog.style.installStyles after updating the closure-library. 93 | * @param {string} stylesString The style string to install. 94 | * @param {Node=} opt_node Node whose parent document should have the 95 | * styles installed. 96 | * @return {Element|StyleSheet} The style element created. 97 | */ 98 | weapi.utils.installStyles = function(stylesString, opt_node) { 99 | var dh = goog.dom.getDomHelper(opt_node); 100 | var styleSheet = null; 101 | 102 | // IE < 11 requires createStyleSheet. Note that doc.createStyleSheet will be 103 | // undefined as of IE 11. 104 | var doc = dh.getDocument(); 105 | if (goog.userAgent.IE && doc.createStyleSheet) { 106 | styleSheet = doc.createStyleSheet(); 107 | goog.style.setStyles(styleSheet, stylesString); 108 | } else { 109 | var head = dh.getElementsByTagNameAndClass('head')[0]; 110 | 111 | // In opera documents are not guaranteed to have a head element, thus we 112 | // have to make sure one exists before using it. 113 | if (!head) { 114 | var body = dh.getElementsByTagNameAndClass('body')[0]; 115 | head = dh.createDom('head'); 116 | body.parentNode.insertBefore(head, body); 117 | } 118 | styleSheet = dh.createDom('style'); 119 | // NOTE(user): Setting styles after the style element has been appended 120 | // to the head results in a nasty Webkit bug in certain scenarios. Please 121 | // refer to https://bugs.webkit.org/show_bug.cgi?id=26307 for additional 122 | // details. 123 | goog.style.setStyles(styleSheet, stylesString); 124 | dh.appendChild(head, styleSheet); 125 | } 126 | return styleSheet; 127 | }; 128 | --------------------------------------------------------------------------------