diff --git a/package.json b/package.json index 1ce0f911..3ac7bc84 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vts-browser-js", - "version": "2.15.16", + "version": "2.16.0", "description": "JavaScript WebGL 3D maps rendering engine", "main": "src/browser/index.js", "scripts": { diff --git a/src/browser/browser.css b/src/browser/browser.css index 4340ad05..df33fbfc 100755 --- a/src/browser/browser.css +++ b/src/browser/browser.css @@ -522,6 +522,17 @@ /*background-color: rgba(255, 255, 255, 0.9);*/ } +.vts-measure-tools div { + margin-right: 2px; +} + +.vts-measure-tools { + font-family: Verdana, Tahoma, Geneva, Arial, Sans-serif; + font-size: 12px; + margin-top: 2px; + /*background-color: rgba(255, 255, 255, 0.9);*/ +} + .vts-measure-tools-button { border-radius: 2px; border: 1px solid rgba(0, 0, 0, 0.7); @@ -531,11 +542,34 @@ cursor: pointer; } -.vts-measure-tools-button:hover { +.vts-measure-tools-button-selected { + border-radius: 2px; + display: inline-block; + padding: 0px 5px; + background-color: #fff; + cursor: pointer; color: rgba(0, 102, 255, 1); border: 1px solid rgba(0, 102, 255, 1); } +.vts-measure-compute { + position: absolute; + font-family: Verdana, Tahoma, Geneva, Arial, Sans-serif; + font-size: 12px; + top: 0px; + left: 0px; + display: none; + border-radius: 2px; + background-color: #aaa; + border: solid 1px #000; + white-space: nowrap; + padding: 3px 1px 3px 3px; +} + +.vts-measure-compute div { + margin-right: 2px; +} + /* * FULLSCREEN */ diff --git a/src/browser/browser.js b/src/browser/browser.js index 6190fcba..4db9c115 100755 --- a/src/browser/browser.js +++ b/src/browser/browser.js @@ -108,7 +108,7 @@ Browser.prototype.getControlMode = function() { Browser.prototype.on = function(name, listener) { - this.core.on(name, listener); + return this.core.on(name, listener); }; @@ -310,6 +310,7 @@ Browser.prototype.initConfig = function() { controlSearchUrl : null, controlSearchFilter : true, controlMeasure : false, + controlMeasureLite : false, controlLink : false, controlGithub : false, controlScale : true, @@ -391,13 +392,14 @@ Browser.prototype.setConfigParam = function(key, value, ignoreCore) { case 'controlSearchFilter': this.config.controlSearchFilter = utils.validateBool(value, true); break; case 'controlSearchElement': this.config.controlSearchElement = value; this.updateUI(key); break; case 'controlSearchValue': this.config.controlSearchValue = /*utils.validateString(*/value/*, null)*/; this.updateUI(key); break; - case 'controlLink': this.config.controlLink = utils.validateBool(value, false); this.updateUI(key); break; - case 'controlGithub': this.config.controlGithub = utils.validateBool(value, false); this.updateUI(key); break; - case 'controlMeasure': this.config.controlMeasure = utils.validateBool(value, false); this.updateUI(key); break; - case 'controlLogo': this.config.controlLogo = utils.validateBool(value, false); this.updateUI(key); break; - case 'controlFullscreen': this.config.controlFullscreen = utils.validateBool(value, true); this.updateUI(key); break; - case 'controlCredits': this.config.controlCredits = utils.validateBool(value, true); this.updateUI(key); break; - case 'controlLoading': this.config.controlLoading = utils.validateBool(value, true); this.updateUI(key); break; + case 'controlLink': this.config.controlLink = utils.validateBool(value, false); this.updateUI(key); break; + case 'controlGithub': this.config.controlGithub = utils.validateBool(value, false); this.updateUI(key); break; + case 'controlMeasure': this.config.controlMeasure = utils.validateBool(value, false); this.updateUI(key); break; + case 'controlMeasureLite': this.config.controlMeasureLite = utils.validateBool(value, false); this.updateUI(key); break; + case 'controlLogo': this.config.controlLogo = utils.validateBool(value, false); this.updateUI(key); break; + case 'controlFullscreen': this.config.controlFullscreen = utils.validateBool(value, true); this.updateUI(key); break; + case 'controlCredits': this.config.controlCredits = utils.validateBool(value, true); this.updateUI(key); break; + case 'controlLoading': this.config.controlLoading = utils.validateBool(value, true); this.updateUI(key); break; case 'minViewExtent': this.config.minViewExtent = utils.validateNumber(value, 0.01, Number.MAXINTEGER, 100); break; case 'maxViewExtent': this.config.maxViewExtent = utils.validateNumber(value, 0.01, Number.MAXINTEGER, Number.MAXINTEGER); break; case 'sensitivity': this.config.sensitivity = utils.validateNumberArray(value, 3, [0,0,0], [10, 10, 10], [1, 0.12, 0.05]); break; @@ -486,6 +488,7 @@ Browser.prototype.getConfigParam = function(key) { case 'controlLink': return this.config.controlLink; case 'controlGithub': return this.config.controlGithub; case 'controlMeasure': return this.config.controlMeasure; + case 'controlMeasureLite': return this.config.controlMeasureLite; case 'controlLogo': return this.config.controlLogo; case 'controlFullscreen': return this.config.controlFullscreen; case 'controlCredits': return this.config.controlCredits; diff --git a/src/browser/interface.js b/src/browser/interface.js index cc57a856..8eafc31e 100755 --- a/src/browser/interface.js +++ b/src/browser/interface.js @@ -95,8 +95,8 @@ BrowserInterface.prototype.destroyMap = function() { BrowserInterface.prototype.on = function(eventName, call) { if (this.killed) return; - this.core.on(eventName, call); - return this; + return this.core.on(eventName, call); + //return this; }; diff --git a/src/browser/ui/control/loading.js b/src/browser/ui/control/loading.js index efaab249..9563e662 100755 --- a/src/browser/ui/control/loading.js +++ b/src/browser/ui/control/loading.js @@ -39,6 +39,7 @@ UIControlLoading.prototype.show = function() { this.ui.setControlVisible('link', false); this.ui.setControlVisible('github', false); this.ui.setControlVisible('measure', false); + this.ui.setControlVisible('measure2', false); this.ui.setControlVisible('fullscreen', false); this.ui.setControlVisible('credits', false); this.ui.setControlVisible('loading', true); @@ -73,6 +74,7 @@ UIControlLoading.prototype.hide = function() { this.ui.setControlVisible('link', this.ui.config.controlLink, false); this.ui.setControlVisible('github', this.ui.config.controlGithub, false); this.ui.setControlVisible('measure', this.ui.config.controlMeasure, false); + this.ui.setControlVisible('measure2', this.ui.config.controlMeasureLite, false); this.ui.setControlVisible('fullscreen', this.ui.config.controlFullscreen, false); this.ui.setControlVisible('credits', this.ui.config.controlCredits, false); this.ui.setControlVisible('loading', false); diff --git a/src/browser/ui/control/measure-lite.js b/src/browser/ui/control/measure-lite.js new file mode 100644 index 00000000..a9dfdda8 --- /dev/null +++ b/src/browser/ui/control/measure-lite.js @@ -0,0 +1,236 @@ + +import Dom_ from '../../utility/dom'; +import {UIControlMeasureIcon as UIControlMeasureIcon_, UIControlMeasureIcon2 as UIControlMeasureIcon2_} from './measure'; + +//get rid of compiler mess +var dom = Dom_, + UIControlMeasureIcon = UIControlMeasureIcon_, + UIControlMeasureIcon2 = UIControlMeasureIcon2_; + + +var UIControlMeasureLite = function(ui, visible, visibleLock) { + this.ui = ui; + this.browser = ui.browser; + this.control = this.ui.addControl('measure2', + '
' + + + '' + + + '' + + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
Clear
' + + '
' + + '
' + + '
' + + + '
' + + '
' + + + '
', visible, visibleLock); + + this.div = this.control.getElement('vts-measure'); + + this.buttonOff = this.control.getElement('vts-measure-button'); + this.buttonOff.on('click', this.onSwitch.bind(this)); + this.buttonOff.on('dblclick', this.onDoNothing.bind(this)); + + this.buttonOn = this.control.getElement('vts-measure-button2'); + this.buttonOn.on('click', this.onSwitch.bind(this)); + this.buttonOn.on('dblclick', this.onDoNothing.bind(this)); + + this.info = this.control.getElement('vts-measure-info'); + + var clearButton = this.control.getElement('vts-measure-clear'); + clearButton.on('click', this.onClear.bind(this)); + clearButton.on('dblclick', this.onDoNothing.bind(this)); + + this.measuring = false; + this.counter = 1; + this.lastCoords = null; + + this.listPanel = this.control.getElement('vts-measure-text-holder'); + this.list = this.control.getElement('vts-measure-text-input'); + + if (this.measuring) { + this.buttonOn.setStyle('display', 'block'); + this.buttonOff.setStyle('display', 'none'); + } else { + this.buttonOn.setStyle('display', 'none'); + this.buttonOff.setStyle('display', 'block'); + } + + this.onMouseMoveCall = this.onMouseMove.bind(this); + this.onMouseLeaveCall = this.onMouseLeave.bind(this); + this.onMouseClickCall = this.onMouseClick.bind(this); + + this.update(); +}; + + +UIControlMeasureLite.prototype.onDoNothing = function(event) { + dom.stopPropagation(event); +}; + +UIControlMeasureLite.prototype.onMouseLeave = function(event) { + this.info.setStyle('display', 'none'); +}; + + +UIControlMeasureLite.prototype.onMouseClick = function(event) { + var map = this.browser.getMap(); + if (!map) { + return; + } + + var mapElement = this.ui.getMapElement(); + var state = mapElement.getDraggingState(); + + //if (state['dragging']) { //TODO: why does not work this parameter? Fix it once you have time + // return; + //} + var delta = state['absMoved']; + + if ((delta[0]+delta[1]) > 0) { + return; + } + + var coords = event.getMouseCoords(); + var clickCoords = map.getHitCoords(coords[0], coords[1], 'fix'); + + if (!clickCoords) { + return; + } + + clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); + + var str = '#' + this.counter + ' ' + clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + clickCoords[2].toFixed(2) + 'm'; + + if (this.lastCoords) { + var res = map.getDistance(this.lastCoords, clickCoords, false, true); + var space = '\n '; + + for (var i = 0, li = ('' + this.counter).length; i < li; i++) { + space += ' '; + } + + str += space + 'great-circle distance: '; + + if (res[0] > 100000) { + str += '' + (res[0]*0.001).toFixed(2) + 'km'; + } else { + str += '' + res[0].toFixed(2) + 'm'; + } + + str += space + 'elevation difference: ' + (clickCoords[2] - this.lastCoords[2]).toFixed(2) + 'm'; + str += space + 'euclidean distance: '; + + if (res[2] > 100000) { + str += '' + (res[2]*0.001).toFixed(2) + 'km'; + } else { + str += '' + res[2].toFixed(2) + 'm'; + } + } + + this.counter++; + this.lastCoords = clickCoords; + + var listElement = this.list.getElement(); + listElement.value += str + '\n'; + listElement.scrollTop = listElement.scrollHeight; //scroll list to the last line +}; + +UIControlMeasureLite.prototype.onMouseMove = function(event) { + var map = this.browser.getMap(); + if (!map) { + return; + } + + var coords = event.getMouseCoords(); + var clickCoords = map.getHitCoords(coords[0], coords[1], 'fix'); + + if (!clickCoords) { + this.info.setStyle('display', 'none'); + return; + } + + clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); + + var str = clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + clickCoords[2].toFixed(2) + 'm'; + + coords[0] -= this.divRect.left; + coords[1] -= this.divRect.top; + + this.info.setStyle('display', 'block'); + this.info.setStyle('left', (coords[0]+20)+'px'); + this.info.setStyle('top', (coords[1]+10)+'px'); + this.info.setHtml(str); +}; + +UIControlMeasureLite.prototype.onSwitch = function() { + this.measuring = !this.measuring; + + var mapElement = this.ui.getMapElement(); + + if (this.measuring) { + this.buttonOn.setStyle('display', 'block'); + this.buttonOff.setStyle('display', 'none'); + + this.divRect = this.div.getRect(); + + mapElement.on('mousemove', this.onMouseMoveCall); + mapElement.on('mouseleave', this.onMouseLeaveCall); + mapElement.on('click', this.onMouseClickCall); + + } else { + this.buttonOn.setStyle('display', 'none'); + this.buttonOff.setStyle('display', 'block'); + + mapElement.off('mousemove', this.onMouseMoveCall); + mapElement.off('mouseleave', this.onMouseLeaveCall); + mapElement.off('click', this.onMouseClickCall); + } + + this.updateLink(); + this.update(); +}; + +UIControlMeasureLite.prototype.onClear = function() { + this.counter = 1; + this.lastCoords = null; + + var listElement = this.list.getElement(); + listElement.value = ''; + listElement.scrollTop = 0; +}; + +UIControlMeasureLite.prototype.update = function() { + //var button = this.control.getElement('vts-measure-button'); + + var left = 10 + (this.ui.config.controlZoom ? 70 : 0) + + (this.ui.config.controlSpace ? 35 : 0); + + this.div.setStyle('left', left + 'px'); + this.listPanel.setStyle('display', this.measuring ? 'block' : 'none'); +}; + + +UIControlMeasureLite.prototype.updateLink = function() { + /* + var linkValue = this.browser.getLinkWithCurrentPos(); + if (this.list.getElement().value != linkValue) { + this.list.getElement().value = linkValue; + }*/ +}; + + +export default UIControlMeasureLite; diff --git a/src/browser/ui/control/measure.js b/src/browser/ui/control/measure.js index aee2306b..102c8130 100644 --- a/src/browser/ui/control/measure.js +++ b/src/browser/ui/control/measure.js @@ -1,8 +1,14 @@ import Dom_ from '../../utility/dom'; +import {utils as utils_} from '../../../core/utils/utils'; +import {vec3 as vec3_} from '../../../core/utils/matrix'; //get rid of compiler mess -var dom = Dom_; +var dom = Dom_, + utils = utils_, + vec3 = vec3_, + UIControlMeasureIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2lpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NDkxMSwgMjAxMy8xMC8yOS0xMTo0NzoxNiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpkNjI1MjFjMi1mYzE5LTcyNDUtOTI5My1kNTU3MmE5N2E1MjgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODJGRUI2NzE2NzkwMTFFN0EzRUZFNzQ1NEFCMkVFQUQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODJGRUI2NzA2NzkwMTFFN0EzRUZFNzQ1NEFCMkVFQUQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjRBMjkwN0JENjc4QzExRTc5QTQwRjk4NjQzOEI4RDczIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjRBMjkwN0JFNjc4QzExRTc5QTQwRjk4NjQzOEI4RDczIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+k3ySjQAAAdJJREFUeNrclk0oBGEYgHemjZOLm8tSyt9BqT2I1O7JSdSSkpsDxZGLi5MTjn5K4SDh4OfiaPfkIDebOPiPUqKUiLKet96pzzT7MztxMPX0le+b95n3Z2ZZmUwm9NuXHfqLK1smsVhsED5hKkhswc4mYJmHVxgNIvIslyE4hnLYDSqycwiaoCGVSnUFFdk5BH2wKHtBRbYKqlVwbQhGoNU5GETkZHKpggjMqaBFhaGgIltv/mKphRuQss1oZkPuG4oRWTLHlmU5fSlhOYNKSEK7xIUugj+6hmSHpROm2RvL9Q7arqf80IykdDEdgll4IWi/Bm+ECT8Z2R7l+DBKV6N/PoRnEbBuwIKf0nm+8S7RGuzDLaxLMuw/5OuR+bn60ROPt9/pUUQnUEa6A56SyeSWeTYejzs9GkC8ZIpySjxEy1APbdCM6MAlGmeZdIahYIlLVAU9kIA7KZFxrJrA5+bU8RBjBf+eGD26gk0oI4AItiU4dMOqHh+GTylbQT3JU7oj2IMLmNBJrIATKIUoD5L2LfEQJbRPvXAP0iPZj5J92vNl9Fk6mSCZsBV4dwuyflb8XJrRqX6C3iDsFhSdiXOzZlSnwxD2yuBHJv/iX6JvAQYAPSICqA82OnoAAAAASUVORK5CYII=', + UIControlMeasureIcon2 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyFpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NDkxMSwgMjAxMy8xMC8yOS0xMTo0NzoxNiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2Nzg3NzczMzY3OEMxMUU3QjU2QUUxNTNCNzc4MzVBQiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2Nzg3NzczNDY3OEMxMUU3QjU2QUUxNTNCNzc4MzVBQiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY3ODc3NzMxNjc4QzExRTdCNTZBRTE1M0I3NzgzNUFCIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjY3ODc3NzMyNjc4QzExRTdCNTZBRTE1M0I3NzgzNUFCIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+owD8TQAAAepJREFUeNrcls8rBGEYx993lIMfxR4kF/+AKMqBg7K1ajelLEWK025tKOTnjVykuAklNgcHuzmQg4MiTtz4Aza3TeGActDr+45nZt41s7tmBwdbn2l35933M9/3eead5UII9tsvjf3FK2uSiGgBKdDtZW6JllXA2DG4A3teRM7LZQl2QRuY9irScghGQRXb5KteRVoOQSMY0895FGkk8JHgSBH0gllzpAeRkeSRBAEwQIIZUJMxukARly3GOZdpivA5DnpAM2iQ5zFx3KE55DIugT6cT+a7PTTlKt9xHAT74AzU6vWJiEVQ4iWR9uXHhkgu3QhYB/JK3zBZE6WoBkE3Ivt9YolOQBd9uwJedQFjC+A8XyJ1J3G+4zNFV+CCmmMejINnY8sQG0wVzZnFlnW2Fd55ezGaIUAtPgzqwAsmv8mYMCLW8DaqdyY1g73w+RMFQRh0glse1RtDHRvDcdJp6XInsScKU3u3gnukSfCoOcoH0YPa3jif/P7zxEqUoPqUkWBZ398YqwdTNHqLdu+YuyTONToEl+AaTIAhUE61qwTtuJC0e4klkssQAn7QTy3+BA5AhRQgfdoovHvJp6iYli5EdTqlBKZA7a7CJFaiHdABUqBUFfyMxBJt06PBrwpskn/xl+hDgAEAH0j0b9rsgVUAAAAASUVORK5CYII='; var UIControlMeasure = function(ui, visible, visibleLock) { @@ -13,25 +19,36 @@ var UIControlMeasure = function(ui, visible, visibleLock) { + '' + + ' src="' + UIControlMeasureIcon + '">' + '' + + ' src="' + UIControlMeasureIcon2 + '">' + '
' + '
' + '
' - + '' + + '' + '
' + '
' - + '
Clear
' + + '
Position
' + + '
Length
' + + '
Track Length
' + + '
Area
' + + '
Volume
' + + '
Clear Log
' + + '
Units: Meters
' + '
' + '
' + '
' + '
' + '
' + + + '
' + + '
Undo
' + + '
Compute
' + + '
' + ' ', visible, visibleLock); @@ -46,14 +63,55 @@ var UIControlMeasure = function(ui, visible, visibleLock) { this.buttonOn.on('dblclick', this.onDoNothing.bind(this)); this.info = this.control.getElement('vts-measure-info'); + this.compute = this.control.getElement('vts-measure-buttons'); + + var computeButton = this.control.getElement('vts-measure-undo'); + computeButton.on('click', this.onCompute.bind(this, 0)); + computeButton.on('dblclick', this.onDoNothing.bind(this)); + computeButton = this.control.getElement('vts-measure-compute'); + computeButton.on('click', this.onCompute.bind(this, 1)); + computeButton.on('dblclick', this.onDoNothing.bind(this)); var clearButton = this.control.getElement('vts-measure-clear'); clearButton.on('click', this.onClear.bind(this)); clearButton.on('dblclick', this.onDoNothing.bind(this)); + this.toolButtons = []; + var toolButton = this.control.getElement('vts-measure-position'); + toolButton.on('click', this.onTool.bind(this, 0)); + toolButton.on('dblclick', this.onDoNothing.bind(this)); + this.toolButtons.push(toolButton); + toolButton = this.control.getElement('vts-measure-length'); + toolButton.on('click', this.onTool.bind(this, 1)); + toolButton.on('dblclick', this.onDoNothing.bind(this)); + this.toolButtons.push(toolButton); + toolButton = this.control.getElement('vts-measure-track'); + toolButton.on('click', this.onTool.bind(this, 2)); + toolButton.on('dblclick', this.onDoNothing.bind(this)); + this.toolButtons.push(toolButton); + toolButton = this.control.getElement('vts-measure-area'); + toolButton.on('click', this.onTool.bind(this, 3)); + toolButton.on('dblclick', this.onDoNothing.bind(this)); + this.toolButtons.push(toolButton); + toolButton = this.control.getElement('vts-measure-volume'); + toolButton.on('click', this.onTool.bind(this, 4)); + toolButton.on('dblclick', this.onDoNothing.bind(this)); + this.toolButtons.push(toolButton); + toolButton = this.control.getElement('vts-measure-metric'); + toolButton.on('click', this.onTool.bind(this, 5)); + toolButton.on('dblclick', this.onDoNothing.bind(this)); + this.toolButtons.push(toolButton); + this.metricButton = toolButton; + + this.measuring = false; this.counter = 1; + this.renderCounter = 1; this.lastCoords = null; + this.navCoords = null; + this.tool = 0; + this.metric = true; + this.mapUpdateDestructor = null; this.listPanel = this.control.getElement('vts-measure-text-holder'); this.list = this.control.getElement('vts-measure-text-input'); @@ -69,6 +127,7 @@ var UIControlMeasure = function(ui, visible, visibleLock) { this.onMouseMoveCall = this.onMouseMove.bind(this); this.onMouseLeaveCall = this.onMouseLeave.bind(this); this.onMouseClickCall = this.onMouseClick.bind(this); + this.onMapUpdateCall = this.onMapUpdate.bind(this); this.update(); }; @@ -78,6 +137,7 @@ UIControlMeasure.prototype.onDoNothing = function(event) { dom.stopPropagation(event); }; + UIControlMeasure.prototype.onMouseLeave = function(event) { this.info.setStyle('display', 'none'); }; @@ -104,48 +164,99 @@ UIControlMeasure.prototype.onMouseClick = function(event) { var coords = event.getMouseCoords(); var clickCoords = map.getHitCoords(coords[0], coords[1], 'fix'); + map.redraw(); + if (!clickCoords) { return; } - clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); + var i, li, res, str; + var space = ' '; - var str = '#' + this.counter + ' ' + clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + clickCoords[2].toFixed(2) + 'm'; + for (i = 0, li = ('' + this.counter).length; i < li; i++) { + space += ' '; + } - if (this.lastCoords) { - var res = map.getDistance(this.lastCoords, clickCoords, false, true); - var space = '\n '; + if (this.tool == 0) { + this.navCoords = clickCoords; + clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); - for (var i = 0, li = ('' + this.counter).length; i < li; i++) { - space += ' '; - } + str = '------------------------------------------------------\n'; + str += '#' + this.counter + ' Position: '; + str += '\n' + space + clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + this.getTextNumber(clickCoords[2]); + this.counter++; - str += space + 'great-circle distance: '; + } else if (this.tool == 1) { + if (!this.navCoords || this.navCoords.length == 2) { + this.navCoords = [clickCoords]; + clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); - if (res[0] > 100000) { - str += '' + (res[0]*0.001).toFixed(2) + 'km'; + str = '------------------------------------------------------\n'; + str += '#' + this.counter + ' Length: '; + str += '\n' + space + 'p1: ' + clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + this.getTextNumber(clickCoords[2]); } else { - str += '' + res[0].toFixed(2) + 'm'; + this.navCoords.push(clickCoords); + clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); + + str = space + 'p2: ' + clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + this.getTextNumber(clickCoords[2]); + str += '\n' + space + '------------------------'; + + res = map.getDistance(this.lastCoords, clickCoords, false, true); + str += '\n' + space + 'great-circle distance: ' + this.getTextNumber(res[0]); + str += '\n' + space + 'euclidean distance: ' + this.getTextNumber(res[2]); + str += '\n' + space + 'elevation difference: ' + this.getTextNumber(clickCoords[2] - this.lastCoords[2]); + str += '\n' + space + 'azimuth: ' + res[1].toFixed(2) + ' deg'; + + this.counter++; + } + } else if (this.tool == 2) { + if (this.renderCounter != this.counter) { + this.renderCounter = this.counter; + this.onTool(2); } - str += space + 'elevation difference: ' + (clickCoords[2] - this.lastCoords[2]).toFixed(2) + 'm'; - str += space + 'euclidean distance: '; + if (!this.navCoords) { + this.navCoords = [clickCoords]; + clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); + str = '------------------------------------------------------\n'; + str += '#' + this.counter + ' Track Length: '; + str += '\n' + space + 'p1: ' + clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + this.getTextNumber(clickCoords[2]); + } else { + this.navCoords.push(clickCoords); + clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); - if (res[2] > 100000) { - str += '' + (res[2]*0.001).toFixed(2) + 'km'; + str = space + 'p' + this.navCoords.length + ': ' + clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + this.getTextNumber(clickCoords[2]); + } + } else if (this.tool == 3 || this.tool == 4) { + if (this.renderCounter != this.counter) { + this.renderCounter = this.counter; + this.onTool(this.tool); + } + + if (!this.navCoords) { + this.navCoords = [clickCoords]; + clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); + str = '------------------------------------------------------\n'; + str += '#' + this.counter + ((this.tool == 3) ? ' Area: ' : ' Volume: '); + str += '\n' + space + 'p1: ' + clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + this.getTextNumber(clickCoords[2]); } else { - str += '' + res[2].toFixed(2) + 'm'; + this.navCoords.push(clickCoords); + clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); + + str = space + 'p' + this.navCoords.length + ': ' + clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + this.getTextNumber(clickCoords[2]); } } - this.counter++; this.lastCoords = clickCoords; - var listElement = this.list.getElement(); - listElement.value += str + '\n'; - listElement.scrollTop = listElement.scrollHeight; //scroll list to the last line + if (str) { + var listElement = this.list.getElement(); + listElement.value += str + '\n'; + listElement.scrollTop = listElement.scrollHeight; //scroll list to the last line + } }; + UIControlMeasure.prototype.onMouseMove = function(event) { var map = this.browser.getMap(); if (!map) { @@ -162,7 +273,7 @@ UIControlMeasure.prototype.onMouseMove = function(event) { clickCoords = map.convertCoordsFromNavToPublic(clickCoords, 'fix'); - var str = clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + clickCoords[2].toFixed(2) + 'm'; + var str = clickCoords[0].toFixed(7) + ', ' + clickCoords[1].toFixed(7) + ', ' + this.getTextNumber(clickCoords[2]); coords[0] -= this.divRect.left; coords[1] -= this.divRect.top; @@ -173,6 +284,7 @@ UIControlMeasure.prototype.onMouseMove = function(event) { this.info.setHtml(str); }; + UIControlMeasure.prototype.onSwitch = function() { this.measuring = !this.measuring; @@ -187,6 +299,7 @@ UIControlMeasure.prototype.onSwitch = function() { mapElement.on('mousemove', this.onMouseMoveCall); mapElement.on('mouseleave', this.onMouseLeaveCall); mapElement.on('click', this.onMouseClickCall); + this.mapUpdateDestructor = this.browser.on('map-update', this.onMapUpdateCall); } else { this.buttonOn.setStyle('display', 'none'); @@ -195,21 +308,423 @@ UIControlMeasure.prototype.onSwitch = function() { mapElement.off('mousemove', this.onMouseMoveCall); mapElement.off('mouseleave', this.onMouseLeaveCall); mapElement.off('click', this.onMouseClickCall); + + if (this.mapUpdateDestructor) { + this.mapUpdateDestructor(); + } + } + + this.onTool(this.tool); + + this.compute.setStyle('display', 'none'); + + var map = this.browser.getMap(); + if (map) { + map.redraw(); } - this.updateLink(); this.update(); }; + +UIControlMeasure.prototype.onTool = function(tool) { + if (tool == 5) { + this.metric = !this.metric; + this.metricButton.setHtml(this.metric ? 'Units: Meters' : 'Units: Feets'); + return; + } + + this.tool = tool; + this.navCoords = null; + + this.compute.setStyle('display', 'none'); + + for (var i = 0; i < 5; i++) { + this.toolButtons[i].setClass('vts-measure-tools-button'); + } + + this.toolButtons[tool].setClass('vts-measure-tools-button-selected'); + + var map = this.browser.getMap(); + if (map) { + map.redraw(); + } +}; + + +UIControlMeasure.prototype.onCompute = function(button) { + if (!this.navCoords) { + return; + } + + var map = this.browser.getMap(); + if (!map) { + return; + } + + var str, i, li, space; + var listElement = this.list.getElement(); + + if (button == 0) { //undo button + this.navCoords.pop(); + } else { //compute button + if (this.tool == 2) { + + var distance = 0; + var distance2 = 0, coords, coords2, res; + var emin = Number.POSITIVE_INFINITY; + var emax = Number.NEGATIVE_INFINITY; + + space = ' '; + + for (i = 0, li = ('' + this.counter).length; i < li; i++) { + space += ' '; + } + + str = space + '------------------------'; + + for (i = 0, li = this.navCoords.length; i < li; i++) { + coords = map.convertCoordsFromNavToPublic(this.navCoords[i], 'fix'); + + if (coords[2] > emax) { + emax = coords[2]; + } + + if (coords[2] < emin) { + emin = coords[2]; + } + } + + for (i = 0, li = this.navCoords.length - 1; i < li; i++) { + coords = map.convertCoordsFromNavToPublic(this.navCoords[i], 'fix'); + coords2 = map.convertCoordsFromNavToPublic(this.navCoords[i+1], 'fix'); + res = map.getDistance(coords, coords2, false, true); + distance += res[0]; + distance2 += res[2]; + } + + str += '\n' + space + 'great-circle distance: ' + this.getTextNumber(distance); + str += '\n' + space + 'euclidean distance: ' + this.getTextNumber(distance2); + str += '\n' + space + 'max elevation: ' + this.getTextNumber(emax); + str += '\n' + space + 'min elevation: ' + this.getTextNumber(emin); + str += '\n' + space + 'elevation difference: ' + this.getTextNumber(emax - emin); + + this.counter++; + } + + if (this.tool == 3) { + var geodata = map.createGeodata(); + geodata.addPolygon3(this.navCoords, [], null, 'fix', {}, 'tmp-polygon'); + geodata.processHeights('node-by-lod', 62, (function(){ + + space = ' '; + + for (i = 0, li = ('' + this.counter).length; i < li; i++) { + space += ' '; + } + + str = space + '------------------------'; + + var poly = geodata.extractGeometry('tmp-polygon'); + + var area = poly.getSurfaceArea() + + if (this.metric) { + str += '\n' + space + 'area: ' + area.toFixed(2) + ' m\u00B2'; + + if (area > 100) { + str += '\n' + space + ' ' + (area/100).toFixed(2) + ' ares'; + } + + if (area > 10000) { + str += '\n' + space + ' ' + (area/10000).toFixed(2) + ' hectares'; + } + + if (area > 1000000) { + str += '\n' + space + ' ' + (area/1000000).toFixed(2) + ' km\u00B2'; + } + } else { + //str += '\n' + space + 'area: ' + (area / 0.09290304).toFixed(2) + ' ft²'; + str += '\n' + space + 'area: ' + (area / 0.83612736).toFixed(2) + ' yd\u00B2'; + + if ((area / 4046.8564224) >= 1) { + str += '\n' + space + ' ' + (area / 4046.8564224).toFixed(2) + ' acres'; + } + + if ((area / 2589988.110346) >= 1) { + str += '\n' + space + ' ' + (area / 2589988.110346).toFixed(2) + ' mi\u00B2'; + } + } + + this.counter++; + + }).bind(this)); + } + + if (this.tool == 4) { + + var center = [0,0,0]; + + for (i = 0, li = this.navCoords.length; i < li; i++) { + coords = map.convertCoordsFromNavToPhys(this.navCoords[i], 'fix'); + center[0] += coords[0]; + center[1] += coords[1]; + center[2] += coords[2]; + } + + center[0] /= li; + center[1] /= li; + center[2] /= li; + + var radius = 0, dx, dy, dz, distance; + + for (i = 0, li = this.navCoords.length; i < li; i++) { + coords = map.convertCoordsFromNavToPhys(this.navCoords[i], 'fix'); + dx = (center[0] - coords[0]); + dy = (center[1] - coords[1]); + dz = (center[2] - coords[2]); + distance = Math.sqrt(dx*dx + dy*dy + dz*dz); + + if (distance > radius) { + radius = distance; + } + } + + var geodata = map.createGeodata(); + + if (radius > 30000) { + geodata.addPolygon3(this.navCoords, [], null, 'fix', {}, 'tmp-polygon'); + } else { + geodata.addPolygon(this.navCoords, [], null, 'fix', {}, 'tmp-polygon'); + } + + geodata.processHeights('node-by-lod', 62, (function(){ + + if (this.navCoords.length) { + + space = ' '; + + for (i = 0, li = ('' + this.counter).length; i < li; i++) { + space += ' '; + } + + str = space + '------------------------'; + + var poly = geodata.extractGeometry('tmp-polygon'); + + var faces = new Array(poly.getElements()); + + for (i = 0, li = faces.length; i < li; i++) { + faces[i] = poly.getElement(i); + } + + var renderer = this.browser.getRenderer(); + var x, y, north, east; + + coords = map.convertCoordsFromPhysToNav(center, 'fix'); + + var texelSize = radius * 0.0030; //0.15 texel size for 100m diameter + var core = this.browser.getCore(); + + var traceVolumeCall = (function(terrain){ + + str = listElement.value; + str = str.substr(0, str.lastIndexOf('loading data ...')); + str += 'computation progress: 0%'; + + if (!terrain) { + str += '\n some error ocurred. Try it again.'; + return; + } + + var octree = renderer.buildOctreeFromGeometry(terrain); + + var ned = map.getNED(coords, false); + north = ned.direction; + east = ned.east; + + var steps = 25, l, sx, sy, res2, dir = [0,0,0], delta; + var sampleArea = (1.0 / steps) * radius; + var volumeAbove = 0; + var volumeBelow = 0; + + sampleArea *= sampleArea; + + var y = -steps; + + var traceVolumeLine = (function(){ + + for (x = -steps; x <= steps; x++) { + + sx = (1.0 / steps) * x * radius; + sy = (1.0 / steps) * y * radius; + coords[0] = center[0] * 1.0001 + north[0] * sy + east[0] * sx; + coords[1] = center[1] * 1.0001 + north[1] * sy + east[1] * sx; + coords[2] = center[2] * 1.0001 + north[2] * sy + east[2] * sx; + + vec3.normalize(coords, dir); // TODO: add support for projected systems + dir[0] = -dir[0]; + dir[1] = -dir[1]; + dir[2] = -dir[2]; + + res = this.hitFaces(coords, dir, faces); + + if (res[0]) { + res2 = renderer.raycastOctreeGeometry(octree, coords, dir); + + if (res2.length > 0) { + delta = (res[1] - res2[0]) * sampleArea; + + if (delta >= 0) { + volumeAbove += delta; + } else { + volumeBelow += -delta; + } + + ///console.log("T" + JSON.stringify(res2)); + } + } + } + + if (y < steps) { + str = str.substr(0, str.lastIndexOf('computation progress:')); + str += 'computation progress: ' + (((y + steps) / (steps*2))*100).toFixed(1) + ' %'; + } else { + str = str.substr(0, str.lastIndexOf('computation progress:')); + + if (this.metric) { + str += 'volume above: ' + volumeAbove.toFixed(2) + ' m\u00B3'; + str += '\n' + space + 'volume below: ' + volumeBelow.toFixed(2) + ' m\u00B3'; + str += '\n' + space + 'volume combined: ' + (volumeAbove + volumeBelow).toFixed(2) + ' m\u00B3' + '\n'; + } else { + var yd2m = 0.764554857984; + str += 'volume above: ' + (volumeAbove/yd2m).toFixed(2) + ' yd\u00B3'; + str += '\n' + space + 'volume below: ' + (volumeBelow/yd2m).toFixed(2) + ' yd\u00B3'; + str += '\n' + space + 'volume combined: ' + ((volumeAbove + volumeBelow)/yd2m).toFixed(2) + ' yd\u00B3' + '\n'; + } + } + + if (str) { + listElement.value = str; + listElement.scrollTop = listElement.scrollHeight; //scroll list to the last line + } + + if (y < steps) { + core.once('tick', traceVolumeLine, 1); + } + + y++; + + //console.log("*"); + }).bind(this); + + core.once('tick', traceVolumeLine, 1); + + }).bind(this); + + str += '\n' + space + 'loading data ...'; + listElement.value += str; + listElement.scrollTop = listElement.scrollHeight; //scroll list to the last line + str = null; + + var destructor = map.getSurfaceAreaGeometry(coords, radius, 'texelSize', texelSize, traceVolumeCall, false); + + } + + }).bind(this)); + + this.counter++; + } + } + + if (str) { + listElement.value += str + '\n'; + listElement.scrollTop = listElement.scrollHeight; //scroll list to the last line + } + + map.redraw(); +}; + +UIControlMeasure.prototype.hitFace = function(origin, dir, face) { + var EPSILON = 0.0000001; + var v1 = face[0]; + var v2 = face[1]; + var v3 = face[2]; + + var h = [0,0,0], q = [0,0,0], s; + var a,f,u,v; + var edge1 = [v2[0] - v1[0], v2[1] - v1[1], v2[2] - v1[2]]; + var edge2 = [v3[0] - v1[0], v3[1] - v1[1], v3[2] - v1[2]]; + + vec3.cross(dir, edge2, h); + a = vec3.dot(edge1, h); + + if (a > -EPSILON && a < EPSILON) { + return [false]; + } + + f = 1/a; + s = [origin[0] - v1[0], origin[1] - v1[1], origin[2] - v1[2]]; + u = f * (vec3.dot(s, h)); + + if (u < 0.0 || u > 1.0) { + return [false]; + } + + q = vec3.cross(s, edge1); + v = f * vec3.dot(dir, q); + if (v < 0.0 || u + v > 1.0) { + return [false]; + } + + // At this stage we can compute t to find out where the intersection point is on the line. + var t = f * vec3.dot(edge2, q); + //if (t > EPSILON) { // ray intersection + return [true, t]; //[origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t ]]; + //} else { // This means that there is a line intersection but not a ray intersection. + // return [false]; + //} +}; + + +UIControlMeasure.prototype.hitFaces = function(coords, dir, faces) { + var hit = false, t = Number.POSITIVE_INFINITY; + + for (var i = 0, li = faces.length; i < li; i++) { + var res = this.hitFace(coords, dir, faces[i]); + + if (res[0]) { + hit = true; + + if (res[1] < t) { + t = res[1]; + } + } + } + + //console.log(hit ? ("" + t.toFixed(2)) : ("N")); + return [hit, t]; +}; + + UIControlMeasure.prototype.onClear = function() { this.counter = 1; this.lastCoords = null; + this.navCoords = null; var listElement = this.list.getElement(); listElement.value = ''; listElement.scrollTop = 0; + + this.compute.setStyle('display', 'none'); + + var map = this.browser.getMap(); + if (map) { + map.redraw(); + } }; + UIControlMeasure.prototype.update = function() { //var button = this.control.getElement('vts-measure-button'); @@ -221,14 +736,142 @@ UIControlMeasure.prototype.update = function() { }; -UIControlMeasure.prototype.updateLink = function() { - /* - var linkValue = this.browser.getLinkWithCurrentPos(); - if (this.list.getElement().value != linkValue) { - this.list.getElement().value = linkValue; - }*/ +UIControlMeasure.prototype.onMapUpdate = function() { + var map = this.browser.getMap(); + if (!map) { + return; + } + + var renderer = this.browser.getRenderer(); + + if (!this.circleImage) { + this.circleImage = utils.loadImage( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QAAAAAAAD5Q7t/AAAACW9GRnMAAAAgAAAA4ACD+EAUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA/UlEQVRYw+2VPwqDMBTG3dz1Am56EnH2XLroETxGuwc3Z7cOdhY8QJpfSUBspUvStJAPPggvD973/uQligICAgL+DKViqygUV02hbaXLwJlio7gpyhNu2idzEXwwgfI8H+u6vnZdN/V9P3EuimLcCRlsiyArGcfxjWDLsmzyAGzc4aNFNDZ7/iw7AeQH4LNrh5WZYLgkJTaZCyHuVVVdkiSZ0zSdOWMzlaBFWkRrQ4A4Zk/A4wBie1MFYUMAz0wybCYAmR8FUAlzj6+2r18TgM2VAO8tOB1Cyk7mrofQ+zP0voheVjHtIBjDxjrmvCu7k1Xs/TP6ie84ICDAGR5uCYdPo0MWiAAAAABJRU5ErkJggg==', + //"http://maps.google.com/mapfiles/kml/shapes/placemarkcircle.png", + (function(){ + this.circleTexture = renderer.createTexture({ 'source': this.circleImage }); + }).bind(this) + ); + } + + if (!this.circleTexture) { + return; + } + + var i, li, coords, points; + + switch(this.tool) { + case 0: //point + + if (this.navCoords) { + coords = map.convertCoordsFromNavToCanvas(this.navCoords, "fix"); + + renderer.drawImage({ + rect : [coords[0]-12, coords[1]-12, 24, 24], + texture : this.circleTexture, + color : [255,0,0,255], //white point is multiplied by red color so resulting point will be red + depth : coords[2], + depthTest : false, + blend : true //point texture has alpha channel so blend is needed + }); + } + + break; + + case 1: //line + case 2: //track + case 3: //area + case 4: //volume + + if (this.navCoords) { + points = []; + + for (i = 0, li = this.navCoords.length; i < li; i++) { + points.push(map.convertCoordsFromNavToCanvas(this.navCoords[i], "fix")); + } + + if (li > 1) { + + var points2 = [], points3 = [], tmp; + + for (i = 0, li = this.navCoords.length - 1; i < li; i++) { + tmp = map.getGeodesicLinePoints(this.navCoords[i], this.navCoords[i+1]); + points2 = points2.concat(tmp); + } + + for (i = 0, li = points2.length; i < li; i++) { + points3.push(map.convertCoordsFromNavToCanvas(points2[i], "fix")); + } + + if (this.tool == 3 || this.tool == 4) { + points3.push(map.convertCoordsFromNavToCanvas(points2[0], "fix")); + } + + renderer.drawLineString({ + points : points3, + size : 5.0, + color : [0,0,0,255], + depthTest : false, + //depthTest : true, + //depthOffset : [-0.01,0,0], + blend : false + }); + + renderer.drawLineString({ + points : points3, + size : 2.0, + color : [255,0,0,255], + depthTest : false, + //depthTest : true, + //depthOffset : [-0.01,0,0], + blend : false + }); + } + + for (i = 0, li = points.length; i < li; i++) { + coords = points[i]; + + renderer.drawImage({ + rect : [coords[0]-12, coords[1]-12, 24, 24], + texture : this.circleTexture, + color : [255,0,0,255], //white point is multiplied by red color so resulting point will be red + depth : coords[2], + depthTest : false, + blend : true //point texture has alpha channel so blend is needed + }); + } + } + + + } + + + if ((this.tool == 2 || this.tool == 3 || this.tool == 4) && points) { + if (points.length < 2 || this.renderCounter != this.counter) { + this.compute.setStyle('display', 'none'); + return; + } + + coords = points[points.length - 1]; + coords[0] -= this.divRect.left; + coords[1] -= this.divRect.top; + + this.compute.setStyle('display', 'block'); + this.compute.setStyle('left', (coords[0]+20)+'px'); + this.compute.setStyle('top', (coords[1]+10)+'px'); + } + }; +UIControlMeasure.prototype.getTextNumber = function(value) { + var m2ft = 3.28084, km2mi = 0.621371; + + if (value >= 100000) { + return (this.metric) ? (value*0.001).toFixed(2) + 'km' : (value*0.001*km2mi).toFixed(2) + 'mi'; + } else { + return (this.metric) ? (value).toFixed(2) + 'm' : (value*m2ft).toFixed(2) + 'ft'; + } +}; -export default UIControlMeasure; +export {UIControlMeasure , UIControlMeasureIcon, UIControlMeasureIcon2}; diff --git a/src/browser/ui/ui.js b/src/browser/ui/ui.js index ada14aa7..d45f4469 100755 --- a/src/browser/ui/ui.js +++ b/src/browser/ui/ui.js @@ -19,11 +19,12 @@ import UIControlSpace_ from './control/space'; import UIControlSearch_ from './control/search'; import UIControlLink_ from './control/link'; import UIControlGithub_ from './control/github'; -import UIControlMeasure_ from './control/measure'; import UIControlLayers_ from './control/layers'; import UIControlFallback_ from './control/fallback'; import UIControlPopup_ from './control/popup'; import UIControlLoading_ from './control/loading'; +import { UIControlMeasure as UIControlMeasure_ } from './control/measure'; +import UIControlMeasureLite_ from './control/measure-lite'; //get rid of compiler mess var UIControlCompass = UIControlCompass_; @@ -35,6 +36,7 @@ var UIControlSearch = UIControlSearch_; var UIControlLink = UIControlLink_; var UIControlGithub = UIControlGithub_; var UIControlMeasure = UIControlMeasure_; +var UIControlMeasureLite = UIControlMeasureLite_; var UIControlLayers = UIControlLayers_; var UIControlFallback = UIControlFallback_; var UIControlPopup = UIControlPopup_; @@ -81,6 +83,7 @@ UI.prototype.init = function() { this.link = new UIControlLink(this, (!loading && this.config.controlLink), loading); this.github = new UIControlGithub(this, (!loading && this.config.controlGithub), loading); this.measure = new UIControlMeasure(this, (!loading && this.config.controlMeasure), loading); + this.measure2 = new UIControlMeasureLite(this, (!loading && this.config.controlMeasureLite), loading); //this.navigator = new UIControlNavigation(this, this.config.controlNavigator); this.layers = new UIControlLayers(this, (!loading && this.config.controlLayers), loading); this.fallback = new UIControlFallback(this); diff --git a/src/core/core.js b/src/core/core.js index 51ab191c..d7762e06 100755 --- a/src/core/core.js +++ b/src/core/core.js @@ -339,7 +339,7 @@ Core.prototype.setOption = function(/*key, value*/) { }; -Core.prototype.on = function(name, listener) { +Core.prototype.on = function(name, listener, wait, once) { if (this.killed) { // || this.renderer == null) { return; } @@ -349,17 +349,32 @@ Core.prototype.on = function(name, listener) { } this.listenerCounter++; - this.listeners.push({ name : name, listener : listener, id : this.listenerCounter }); + this.listeners.push({ name : name, listener : listener, id : this.listenerCounter, once: once, wait: wait ? wait : 0 }); return (function(id){ this.removeListener(id); }).bind(this, this.listenerCounter); }; +Core.prototype.once = function(name, listener, wait) { + this.on(name, listener, wait, true); +}; + + // private Core.prototype.callListener = function(name, event, log) { for (var i = 0; i < this.listeners.length; i++) { if (this.listeners[i].name == name) { - this.listeners[i].listener(event); + var listener = this.listeners[i]; + + if (listener.wait > 0) { + listener.wait--; + } else { + listener.listener(event); + if (listener.once) { + this.listeners.splice(i, 1); + i--; + } + } } } @@ -508,7 +523,7 @@ string getCoreVersion() */ function getCoreVersion(full) { - return (full ? 'Core: ' : '') + '2.15.16'; + return (full ? 'Core: ' : '') + '2.16.0'; } diff --git a/src/core/interface.js b/src/core/interface.js index 42ae10ed..eb8b8b8a 100755 --- a/src/core/interface.js +++ b/src/core/interface.js @@ -81,7 +81,12 @@ CoreInterface.prototype.getProj4 = function() { CoreInterface.prototype.on = function(eventName, call) { if (!this.core) { return null; } - this.core.on(eventName, call); + return this.core.on(eventName, call); +}; + +CoreInterface.prototype.once = function(eventName, call, wait) { + if (!this.core) { return null; } + return this.core.once(eventName, call, wait); }; CoreInterface.prototype.callListener = function(name, event) { diff --git a/src/core/map/convert.js b/src/core/map/convert.js index 2e490433..59c2fa3d 100755 --- a/src/core/map/convert.js +++ b/src/core/map/convert.js @@ -34,8 +34,7 @@ MapConvert.prototype.movePositionCoordsTo = function(position, azimuth, distance position.setCoords2([coords[0] + (forward[0]*distance), coords[1] + (forward[1]*distance)]); } else { - var geod = new GeographicLib.Geodesic.Geodesic(navigationSrsInfo['a'], - (navigationSrsInfo['a'] / navigationSrsInfo['b']) - 1.0); + var geod = this.measure.getGeodesic(); var r = geod.Direct(coords[1], coords[0], azimuth, distance); position.setCoords2([r.lon2, r.lat2]); @@ -296,5 +295,41 @@ MapConvert.prototype.convertCoordsFromPhysToNav = function(coords, mode, lod) { return coords; }; +MapConvert.prototype.getGeodesicLinePoints = function(coords, coords2, height, density) { + var geod, r, length, azimuth, minStep, d; + var navigationSrsInfo = this.measure.navigationSrsInfo; + var dx = coords2[0] - coords[0]; + var dy = coords2[1] - coords[1]; + var dz = coords2[2] - coords[2]; + + if (this.isProjected) { + length = Math.sqrt(dx*dx + dy*dy + dz*dz); + minStep = 1000000; //just big number + } else { + geod = this.measure.getGeodesic(); + r = geod.Inverse(coords[1], coords[0], coords2[1], coords2[0]); + length = r.s12; + azimuth = r.azi1; + minStep = 10 * ((navigationSrsInfo['a'] * 2 * Math.PI) / 4007.5); //aprox 100km for earth + } + + var points = [coords]; + var distance = minStep; + + for (;distance < length; distance += minStep) { + d = distance / length; + + if (this.isProjected) { + points.push([ coords[0] + dx * d, coords[1] + dy * d, coords[2] + dz * d ]); + } else { + r = geod.Direct(coords[1], coords[0], azimuth, distance); + points.push([r.lon2, r.lat2, coords[2] + dz * d]); + } + } + + points.push(coords2); + + return points; +}; export default MapConvert; diff --git a/src/core/map/draw-tiles.js b/src/core/map/draw-tiles.js index 2b6b13e5..50d556ba 100755 --- a/src/core/map/draw-tiles.js +++ b/src/core/map/draw-tiles.js @@ -736,6 +736,10 @@ MapDrawTiles.prototype.updateTileSurfaceBounds = function(tile, submesh, surface // tile = tile; //} + if (this.config.mapNoTextures) { + return; + } + //search map view if (surface.boundLayerSequence.length > 0) { if (fullUpdate) { diff --git a/src/core/map/draw.js b/src/core/map/draw.js index 47fa8afe..2f4fca26 100755 --- a/src/core/map/draw.js +++ b/src/core/map/draw.js @@ -357,7 +357,7 @@ MapDraw.prototype.drawMap = function(skipFreeLayers) { } if (this.tree.surfaceSequence.length > 0) { - this.tree.draw(camInfo); + this.tree.draw(); } if (replay.storeTiles) { //used only in inspectors @@ -404,7 +404,7 @@ MapDraw.prototype.drawMap = function(skipFreeLayers) { if (layer.type == 'geodata') { this.drawMonoliticGeodata(layer); } else { - layer.tree.draw(camInfo); + layer.tree.draw(); } this.zbufferOffset = null; diff --git a/src/core/map/geodata-builder.js b/src/core/map/geodata-builder.js index a8684581..cf39b579 100644 --- a/src/core/map/geodata-builder.js +++ b/src/core/map/geodata-builder.js @@ -1043,7 +1043,7 @@ MapGeodataBuilder.prototype.addPolygon3 = function(shape, holes, middle, heightM ' p6:' + (Array.isArray(sbuffer2[l+5][1]) ? 'a' : '') + sbuffer2[l+5][1]);*/ } else if (l2 == l) { - console.log('l2'); + //console.log('l2'); //add new vertices to the buffer vbuffer[mm] = p5[0]; @@ -1463,7 +1463,8 @@ MapGeodataBuilder.prototype.extractGeometry = function(id) { var group = this.groups[i]; var groupPoints = group.points; - var groupLines = group.lines, j, lj; + var groupLines = group.lines; + var groupPolygons = group.polygons, j, lj; //get group bbox for (j = 0, lj = groupPoints.length; j < lj; j++) { @@ -1477,6 +1478,12 @@ MapGeodataBuilder.prototype.extractGeometry = function(id) { feature = groupLines[j]; } } + + for (j = 0, lj = groupPolygons.length; j < lj; j++) { + if (groupPolygons[j].id == id) { + feature = groupPolygons[j]; + } + } } if (feature) { @@ -1529,6 +1536,13 @@ MapGeodataBuilder.prototype.extractGeometry = function(id) { } return new MapGeodataGeometry(this.map, {'type': 'line-geometry', 'id':feature.id, 'geometryBuffer': vertexBuffer, 'indicesBuffer': indexBuffer }); + } else if (feature.vertices) { + + /*feature.vertices = featureVertices; + feature.surface = featureSurface; + feature.borders = featureBorders;*/ + + return new MapGeodataGeometry(this.map, {'type': 'polygon-geometry', 'id':feature.id, 'geometryBuffer': feature.vertices, 'surface': feature.surface }); } return; diff --git a/src/core/map/geodata-geometry.js b/src/core/map/geodata-geometry.js index 1ba2adc2..df3ceb3b 100644 --- a/src/core/map/geodata-geometry.js +++ b/src/core/map/geodata-geometry.js @@ -25,6 +25,9 @@ var MapGeodataGeometry = function(map, data) { break; case 'polygon-geometry': this.type = 3; + this.vertexBuffer = this.data.geometryBuffer; + this.surface = this.data.surface; + this.borders = this.data.borders; break; } }; @@ -42,12 +45,17 @@ MapGeodataGeometry.prototype.getElement = function(index) { switch(this.type) { case 1: return [v[i], v[i+1], v[i+2]]; //point case 2: return [[v[i], v[i+1], v[i+2]], [v[i+3], v[i+4], v[i+5]]]; //line + case 3: + var s = this.surface; + var i1 = s[i], i2 = s[i+1], i3 = s[i+2]; + return [[v[i1][0], v[i1][1], v[i1][2]], [v[i2][0], v[i2][1], v[i2][2]], [v[i3][0], v[i3][1], v[i3][2]]]; //polygon } }; MapGeodataGeometry.prototype.getElements = function(pathIndex) { switch(this.type) { - case 1: return this.vertexBuffer.length / 3; //point + case 1: //point + case 3: return this.surface.length / 3; //polygon case 2: //line pathIndex = pathIndex || 0; @@ -272,6 +280,46 @@ MapGeodataGeometry.prototype.getPathsCount = function() { }; +MapGeodataGeometry.prototype.getSurfaceArea = function() { + if (this.type != 3) { + return 0; + } + + if (!this.surfaceArea) { + var v = this.vertexBuffer, s = this.surface; + var p1, p2, p3, l1, l2, l3, dx, dy, dz, perimeter, area; + + this.surfaceArea = 0; + for (var i = 0, li = s.length; i < li; i+= 3) { + p1 = v[s[i]]; + p2 = v[s[i+1]]; + p3 = v[s[i+2]]; + + dx = p2[0] - p1[0]; + dy = p2[1] - p1[1]; + dz = p2[2] - p1[2]; + l1 = Math.sqrt(dx*dx + dy*dy + dz*dz); + + dx = p2[0] - p3[0]; + dy = p2[1] - p3[1]; + dz = p2[2] - p3[2]; + l2 = Math.sqrt(dx*dx + dy*dy + dz*dz); + + dx = p3[0] - p1[0]; + dy = p3[1] - p1[1]; + dz = p3[2] - p1[2]; + l3 = Math.sqrt(dx*dx + dy*dy + dz*dz); + + //Heron's formula + perimeter = (l1 + l2 + l3)/2; + area = Math.sqrt(perimeter*((perimeter-l1)*(perimeter-l2)*(perimeter-l3))); + + this.surfaceArea += area; + } + } + + return this.surfaceArea; +}; export default MapGeodataGeometry; diff --git a/src/core/map/geodata-import/geojson.js b/src/core/map/geodata-import/geojson.js index ed1d8ad4..fb0e6b30 100644 --- a/src/core/map/geodata-import/geojson.js +++ b/src/core/map/geodata-import/geojson.js @@ -21,7 +21,7 @@ MapGeodataImportGeoJSON.prototype.processGeometry = function(geometry, feature) break; case 'MultiPoint': - this.builder.addPointArray(cords, this.heightMode, feature['properties'], feature['properties'] ? feature['properties']['id'] : null, this.srs); + this.builder.addPointArray(coords, this.heightMode, feature['properties'], feature['properties'] ? feature['properties']['id'] : null, this.srs); break; case 'LineString': diff --git a/src/core/map/interface.js b/src/core/map/interface.js index 616dea9b..f2edd985 100755 --- a/src/core/map/interface.js +++ b/src/core/map/interface.js @@ -245,11 +245,38 @@ MapInterface.prototype.movePositionCoordsTo = function(position, azimuth, distan }; +MapInterface.prototype.getGeodesicLinePoints = function(coords, coords2, height, density) { + return this.map.convert.getGeodesicLinePoints(coords, coords2, height, density); +}; + + MapInterface.prototype.getSurfaceHeight = function(coords, precision) { return this.map.measure.getSurfaceHeight(coords, this.map.measure.getOptimalHeightLodBySampleSize(coords, precision)); }; +MapInterface.prototype.getSurfaceAreaGeometry = function(coords, radius, mode, limit, callback, loadTextures) { + var res = this.map.measure.getSurfaceAreaGeometry(coords, radius, mode, limit, true, loadTextures); + //console.log('getSurfaceAreaGeometry'); + + if (!res[0]) { + return this.map.core.once('map-update', this.getSurfaceAreaGeometry.bind(this, coords, radius, mode, limit, callback, loadTextures), 1); + } else { + var buffer = res[1], ret = [], map = this.map; + + if (map.tree) { + map.storedTilesRes = []; + map.tree.storeGeometry(buffer, buffer.length); + ret = map.storedTilesRes; + map.storedTilesRes = []; + } + + callback(ret); + return (function(){}); + } +}; + + MapInterface.prototype.getDistance = function(coords, coords2, includingHeights, usePublic) { return this.map.measure.getDistance(coords, coords2, includingHeights, usePublic); }; @@ -260,8 +287,8 @@ MapInterface.prototype.getAzimuthCorrection = function(coords, coords2) { }; -MapInterface.prototype.getNED = function(coords) { - return this.map.measure.getNewNED(coords, true); +MapInterface.prototype.getNED = function(coords, onlyMatrix) { + return this.map.measure.getNewNED(coords, (onlyMatrix === false) ? false : true); }; @@ -395,6 +422,11 @@ MapInterface.prototype.renderToImage = function() { }; +MapInterface.prototype.getCurrentGeometry = function() { + return this.map.getCurrentGeometry(); +}; + + MapInterface.prototype.getStats = function(switches) { if (switches) { return { diff --git a/src/core/map/map.js b/src/core/map/map.js index 5ca1615c..5eebb256 100755 --- a/src/core/map/map.js +++ b/src/core/map/map.js @@ -1122,6 +1122,16 @@ Map.prototype.hitTestGeoLayers = function(screenX, screenY, mode) { } }; +Map.prototype.getCurrentGeometry = function() { + if (this.draw.tree.surfaceSequence.length > 0) { + this.draw.tree.draw(true); + var res = this.storedTilesRes; + this.storedTilesRes = []; + return res; + } + + return res; +}; Map.prototype.applyCredits = function(tile) { var value, value2; diff --git a/src/core/map/measure.js b/src/core/map/measure.js index 81227a98..64c9b6b9 100755 --- a/src/core/map/measure.js +++ b/src/core/map/measure.js @@ -25,8 +25,45 @@ var MapMeasure = function(map) { this.maxDivisionNodeDepth = res[1]; }; +MapMeasure.prototype.getSurfaceAreaGeometry = function(coords, radius, mode, limit, loadMeshes, loadTextures) { + var tree = this.map.tree; + + if (tree.surfaceSequence.length == 0) { + reurn [true, []]; + } + + var center = this.convert.convertCoords(coords, 'navigation', 'physical'); + var coneVec = [0,0,0]; + + vec3.normalize(center, coneVec); + + var distance = vec3.length(center); + var coneAngle = Math.atan(Math.tan(radius / distance)); + + tree.params = { + coneVec : coneVec, + coneAngle : coneAngle, + mode : mode, + limit : limit, + loaded : true, + areaTiles : [], + loadMeshes: (loadMeshes === true), + loadTextures: (loadTextures === true) + }; + + //priority = 0, noReadInly = false + tree.traceAreaTiles(tree.surfaceTree, 0, false); + + return [tree.params.loaded, tree.params.areaTiles]; +}; MapMeasure.prototype.getSurfaceHeight = function(coords, lod, storeStats, node, nodeCoords, coordsArray, useNodeOnly) { + var tree = this.map.tree; + + if (tree.surfaceSequence.length == 0) { + return [0, true, true, null, null, null]; + } + if (!node) { var result = this.getSpatialDivisionNode(coords); node = result[0]; @@ -41,8 +78,6 @@ MapMeasure.prototype.getSurfaceHeight = function(coords, lod, storeStats, node, return this.getSurfaceHeightNodeOnly(null, lod + 8, storeStats, lod, null, node, nodeCoords, coordsArray); } - var tree = this.map.tree; - if (node != null && lod !== null) { var root = tree.findSurfaceTile(node.id); @@ -136,6 +171,12 @@ MapMeasure.prototype.getSurfaceHeight = function(coords, lod, storeStats, node, MapMeasure.prototype.getSurfaceHeightNodeOnly = function(coords, lod, storeStats, statsLod, deltaSample, node, nodeCoords, coordsArray) { var arrayRes, height, stats = this.map.stats; + + var tree = this.map.tree; + + if (tree.surfaceSequence.length == 0) { + return [0, true, true, null, null, null]; + } if (!deltaSample) { if (!node) { @@ -203,8 +244,6 @@ MapMeasure.prototype.getSurfaceHeightNodeOnly = function(coords, lod, storeStats //blend values } - var tree = this.map.tree; - if (node != null && lod !== null) { var root = tree.findSurfaceTile(node.id); diff --git a/src/core/map/surface-tile.js b/src/core/map/surface-tile.js index 089501af..755f9969 100755 --- a/src/core/map/surface-tile.js +++ b/src/core/map/surface-tile.js @@ -309,7 +309,7 @@ MapSurfaceTile.prototype.isMetanodeReady = function(tree, priority, preventLoad) var ret = this.checkMetanode(tree, priority); if (!ret && !(this.metanode != null && this.lastMetanode)) { //metanode is not ready yet - return; + return false; } } @@ -632,6 +632,17 @@ MapSurfaceTile.prototype.bboxVisible = function(id, bbox, cameraPos, node) { } }; +MapSurfaceTile.prototype.insideCone = function(coneVec, angle, node) { + + if (this.map.isGeocent) { // && node.diskPos && node.diskNormal) { + var a = Math.acos(vec3.dot(coneVec, node.diskNormal)); + + return (a < angle + node.diskAngle2A); + } + + return false; +}; + MapSurfaceTile.prototype.getPixelSize = function(bbox, screenPixelSize, cameraPos, worldPos, returnDistance) { var min = bbox.min; diff --git a/src/core/map/surface-tree.js b/src/core/map/surface-tree.js index ca483543..ab113b4c 100755 --- a/src/core/map/surface-tree.js +++ b/src/core/map/surface-tree.js @@ -125,7 +125,7 @@ MapSurfaceTree.prototype.findNavTile = function(id) { }; -MapSurfaceTree.prototype.draw = function() { +MapSurfaceTree.prototype.draw = function(storeTilesOnly) { this.cameraPos = [0,0,0]; this.worldPos = [0,0,0]; @@ -150,8 +150,8 @@ MapSurfaceTree.prototype.draw = function() { this.drawSurface([0,0,0]); if (periodicity.type == 'X') { - this.drawSurface([periodicity.period,0,0]); - this.drawSurface([-periodicity.period,0,0]); + this.drawSurface([periodicity.period,0,0], storeTilesOnly); + this.drawSurface([-periodicity.period,0,0], storeTilesOnly); } } else { @@ -164,9 +164,9 @@ MapSurfaceTree.prototype.draw = function() { } switch(mode) { - case 'topdown': this.drawSurface([0,0,0]); break; - case 'fit': this.drawSurfaceFit([0,0,0]); break; - case 'fitonly': this.drawSurfaceFitOnly([0,0,0]); break; + case 'topdown': this.drawSurface([0,0,0], storeTilesOnly); break; + case 'fit': this.drawSurfaceFit([0,0,0], storeTilesOnly); break; + case 'fitonly': this.drawSurfaceFitOnly([0,0,0], storeTilesOnly); break; } } }; @@ -219,7 +219,7 @@ MapSurfaceTree.prototype.logTileInfo = function(tile, node, cameraPos) { //loadmode = topdown -MapSurfaceTree.prototype.drawSurface = function() { +MapSurfaceTree.prototype.drawSurface = function(shift, storeTilesOnly) { this.counter++; var tile = this.surfaceTree; @@ -403,6 +403,11 @@ MapSurfaceTree.prototype.drawSurface = function() { processBufferIndex = newProcessBufferIndex; } while(processBufferIndex > 0); + + if (storeTilesOnly) { + this.storeDrawBufferGeometry(drawBufferIndex); + return; + } if (best2 > draw.bestMeshTexelSize) { draw.bestMeshTexelSize = best2; @@ -449,7 +454,7 @@ MapSurfaceTree.prototype.drawSurface = function() { //loadmode = fitonly -MapSurfaceTree.prototype.drawSurfaceFitOnly = function() { +MapSurfaceTree.prototype.drawSurfaceFitOnly = function(shift, storeTilesOnly) { this.counter++; // this.surfaceTracer.trace(this.surfaceTree);//this.rootId); @@ -622,6 +627,11 @@ MapSurfaceTree.prototype.drawSurfaceFitOnly = function() { } while(processBufferIndex > 0); + if (storeTilesOnly) { + this.storeDrawBufferGeometry(drawBufferIndex); + return; + } + var stats = map.stats; stats.usedNodes = usedNodes; @@ -658,7 +668,7 @@ MapSurfaceTree.prototype.drawSurfaceFitOnly = function() { //loadmode = fit -MapSurfaceTree.prototype.drawSurfaceFit = function() { +MapSurfaceTree.prototype.drawSurfaceFit = function(shift, storeTilesOnly) { this.counter++; var tile = this.surfaceTree; @@ -971,6 +981,11 @@ MapSurfaceTree.prototype.drawSurfaceFit = function() { } while(processBufferIndex > 0); + if (storeTilesOnly) { + this.storeDrawBufferGeometry(drawBufferIndex); + return; + } + var stats = map.stats; stats.usedNodes = usedNodes; @@ -1011,6 +1026,55 @@ MapSurfaceTree.prototype.drawSurfaceFit = function() { }; +MapSurfaceTree.prototype.storeDrawBufferGeometry = function(drawBufferIndex) { + var map = this.map; + var drawBuffer = map.draw.drawBuffer; + + this.storeGeometry(drawBuffer, drawBufferIndex); +}; + + +MapSurfaceTree.prototype.storeGeometry = function(array, length) { + var map = this.map; + var drawBuffer = array; + map.storedTilesRes = new Array(length); + + for (var i = length - 1; i >= 0; i--) { + var tile = drawBuffer[i]; + + if (tile.metanode && tile.surface && tile.metanode.hasGeometry() && + tile.surfaceMesh && tile.surfaceMesh.isReady(true, 0, true)) { + + var mesh = tile.surfaceMesh; + var submeshes = []; + + for (var j = 0, lj = mesh.submeshes.length; j < lj; j++) { + var submesh = mesh.submeshes[j], + vertices = submesh.vertices.slice(), + min = submesh.bbox.min, + max = submesh.bbox.max, + delta = [max[0] - min[0], max[1] - min[1], max[2] - min[2]]; + + for (var k = 0, lk = vertices.length; k < lk; k+=3) { + vertices[k] = vertices[k]*delta[0] + min[0]; + vertices[k+1] = vertices[k+1]*delta[1] + min[1]; + vertices[k+2] = vertices[k+2]*delta[2] + min[2]; + } + + submeshes.push({ + "bbox": [min.slice(), max.slice()], + "vertices" : vertices }); + } + + map.storedTilesRes[i] = { + "id": tile.id.slice(), + "type": "mesh", + "submeshes": submeshes + }; + } + } +}; + MapSurfaceTree.prototype.traceHeight = function(tile, params, nodeOnly) { if (!tile) { return; @@ -1296,4 +1360,66 @@ MapSurfaceTree.prototype.getRenderedNodeById = function(id, drawCounter) { }; +MapSurfaceTree.prototype.chekTileMesh = function(tile) { + if (this.params.loadMeshes || this.params.loadTextures) { + + var tmp = this.config.mapNoTextures; + this.config.mapNoTextures = !this.params.loadTextures; + + //are resources ready? priority=0, preventRender=true, preventLoad=false, doNotCheckGpu=true + if (!this.map.draw.drawTiles.drawSurfaceTile(tile, tile.metanode, this.map.renderer.cameraPosition, tile.texelSize, 0, true, false, true)) { + this.params.loaded = false; + } + + this.config.mapNoTextures = tmp; + } +}; + + +MapSurfaceTree.prototype.traceAreaTiles = function(tile, priority, nodeReadyOnly) { + if (tile == null) { + return; + } + + if (!tile.isMetanodeReady(this, 0) || nodeReadyOnly) { + this.params.loaded = false; + //console.log('(L)' + JSON.stringify(tile.id)); + tile.isMetanodeReady(this, 0); + return; + } + + tile.metanode.metatile.used(); + + if (tile.lastSurface && tile.lastSurface == tile.surface) { + tile.lastSurface = null; + tile.restoreLastState(); + //return; + } + + if (!tile.insideCone(this.params.coneVec, this.params.coneAngle, tile.metanode)) { + return; + } + + var fit = (this.params.mode == 'lod') ? (tile.id[0] >= this.params.limit) : (tile.metanode.pixelSize <= this.params.limit); + + if (fit) { + //console.log('(A)' + JSON.stringify(tile.id)); + this.chekTileMesh(tile); + this.params.areaTiles.push(tile); + return; + } + + if (!tile.metanode.hasChildren()) { + //console.log('(A)' + JSON.stringify(tile.id)); + this.chekTileMesh(tile); + this.params.areaTiles.push(tile); + } else { + for (var i = 0; i < 4; i++) { + this.traceAreaTiles(tile.children[i], priority, nodeReadyOnly); + } + } +}; + + + export default MapSurfaceTree; diff --git a/src/core/renderer/draw.js b/src/core/renderer/draw.js index e222c019..a13ffa3e 100755 --- a/src/core/renderer/draw.js +++ b/src/core/renderer/draw.js @@ -273,6 +273,7 @@ RendererDraw.prototype.drawLineString = function(points, screenSpace, size, colo gl.depthMask(false); } + gl.disable(gl.STENCIL_TEST); gl.disable(gl.CULL_FACE); } diff --git a/src/core/renderer/gmap.js b/src/core/renderer/gmap.js index 6dbcb2dc..23dda738 100644 --- a/src/core/renderer/gmap.js +++ b/src/core/renderer/gmap.js @@ -6,7 +6,7 @@ function processGMap(gpu, gl, renderer, screenPixelSize) { var ppi = 96 * (window.devicePixelRatio || 1); var screenLX = renderer.curSize[0]; var screenLY = renderer.curSize[1]; - var featureCount = (screenLX/ppi)*(screenLY/ppi)*featuresPerSquareInch; + var featureCount = Math.ceil((screenLX/ppi)*(screenLY/ppi)*featuresPerSquareInch); var i, li, top = renderer.config.mapFeaturesSortByTop; //get top features @@ -61,7 +61,7 @@ function processGMap(gpu, gl, renderer, screenPixelSize) { pp = feature[5]; - if (pp[0] < 0 || pp[0] >= screenLX || pp[1] < 0 || pp[1] >= screenLY) { + if (pp[0] < 30 || pp[0] >= (screenLX-30) || pp[1] < 30 || pp[1] >= (screenLY-30)) { featureCache[i] = null; continue; } @@ -137,6 +137,7 @@ function processGMap(gpu, gl, renderer, screenPixelSize) { c *= my; usedFeatures += a + b + c + d; + featureCount -= a + b + c + d; tileSize *= 2; } while(usedFeatures < featureCount2); diff --git a/src/core/renderer/interface.js b/src/core/renderer/interface.js index 0af46484..1af5f3c0 100755 --- a/src/core/renderer/interface.js +++ b/src/core/renderer/interface.js @@ -2,12 +2,14 @@ import GpuTexture_ from './gpu/texture'; import GpuMesh_ from './gpu/mesh'; import GpuProgram_ from './gpu/program'; +import {Octree as Octree_, OctreeRaycaster as OctreeRaycaster_} from './octree.js'; //get rid of compiler mess var GpuTexture = GpuTexture_; var GpuMesh = GpuMesh_; var GpuProgram = GpuProgram_; - +var Octree = Octree_; +var OctreeRaycaster = OctreeRaycaster_; var RendererInterface = function(renderer) { this.renderer = renderer; @@ -407,6 +409,19 @@ RendererInterface.prototype.drawDebugText = function(options) { }; +RendererInterface.prototype.buildOctreeFromGeometry = function(geometry) { + var octree = new Octree(); + octree.buildFromGeometry(geometry); + return octree; +}; + + +RendererInterface.prototype.raycastOctreeGeometry = function(octree, rayPos, rayDir) { + var raycaster = new OctreeRaycaster(), intersects = []; + raycaster.intersectOctree(rayPos, rayDir, octree, intersects); + return raycaster.intersectOctants(rayPos, rayDir, intersects); +}; + RendererInterface.prototype.saveScreenshot = function(output, filename, filetype) { return this.renderer.saveScreenshot(output, filename, filetype); }; diff --git a/src/core/renderer/octree.js b/src/core/renderer/octree.js new file mode 100644 index 00000000..7670282d --- /dev/null +++ b/src/core/renderer/octree.js @@ -0,0 +1,567 @@ + +import {vec3 as vec3_} from '../utils/matrix'; + +//get rid of compiler mess +var vec3 = vec3_; + + +var Octree = function() { + this.root = null; + this.maxItemsPerNode = 20; + this.maxDepth = 20; + + this.depthCount = []; + for (var i = 0; i < 1000; i++) { + this.depthCount[i] = 0; + } + + /** + * A binary pattern that describes the standard octant layout: + * + * ```text + * 3____7 + * 2/___6/| + * | 1__|_5 + * 0/___4/ + * ``` + * + * This common layout is crucial for positional assumptions. + * + */ + + this.pattern = [ + new Uint8Array([0, 0, 0]), + new Uint8Array([0, 0, 1]), + new Uint8Array([0, 1, 0]), + new Uint8Array([0, 1, 1]), + + new Uint8Array([1, 0, 0]), + new Uint8Array([1, 0, 1]), + new Uint8Array([1, 1, 0]), + new Uint8Array([1, 1, 1]) + ]; +}; + + +Octree.prototype.clear = function() { +}; + + +Octree.prototype.buildFromGeometry = function(data) { + if (!data) { + return; + } + + var i, li, j, lj, k, lk, v, item, submeshes, submesh, bbox, index, + minX, minY, minZ, maxX, maxY, maxZ, geometry, vertices; + + minX = minY = minZ = Number.POSITIVE_INFINITY; + maxX = maxY = maxZ = Number.NEGATIVE_INFINITY; + + //get gemetery bbox + for (i = 0, li = data.length; i < li; i++) { + geometry = data[i]; + if (geometry["type"] == "mesh") { + submeshes = geometry["submeshes"]; + + for (j = 0, lj = submeshes.length; j < lj; j++) { + submesh = submeshes[j]; + bbox = submesh["bbox"]; + + if (bbox[0][0] < minX) minX = bbox[0][0]; + if (bbox[0][1] < minY) minY = bbox[0][1]; + if (bbox[0][2] < minZ) minZ = bbox[0][2]; + + if (bbox[1][0] > maxX) maxX = bbox[1][0]; + if (bbox[1][1] > maxY) maxY = bbox[1][1]; + if (bbox[1][2] > maxZ) maxZ = bbox[1][2]; + } + } + } + + this.root = new OctreeNode([minX, minY, minZ], [maxX, maxY, maxZ]); + + //get gemetery bbox + for (i = 0, li = data.length; i < li; i++) { + geometry = data[i]; + if (geometry["type"] == "mesh") { + submeshes = geometry["submeshes"]; + + for (j = 0, lj = submeshes.length; j < lj; j++) { + submesh = submeshes[j]; + + vertices = submesh["vertices"]; + + for (k = 0, lk = vertices.length; k < lk; k += 9) { + + minX = minY = minZ = Number.POSITIVE_INFINITY; + maxX = maxY = maxZ = Number.NEGATIVE_INFINITY; + + for (v = 0; v < 3; v ++) { + index = k + v * 3; + + if (vertices[index] < minX) minX = vertices[index]; + if (vertices[index+1] < minY) minY = vertices[index+1]; + if (vertices[index+2] < minZ) minZ = vertices[index+2]; + + if (vertices[index] > maxX) maxX = vertices[index]; + if (vertices[index+1] > maxY) maxY = vertices[index+1]; + if (vertices[index+2] > maxZ) maxZ = vertices[index+2]; + } + + //this.root.add([minX, minY, minZ], [maxX, maxY, maxZ], [vertices, k]) + this.root.add([minX, minY, minZ, maxX, maxY, maxZ, vertices, k], this) + } + + } + } + } + + +}; + + +var OctreeNode = function(min, max) { + this.min = min; + this.max = max; + this.children = null; + this.items = null; +}; + +OctreeNode.prototype.add = function(item, octree, depth) { + if (this.children) { + if (!depth) { + depth = 0; + } + + for (var i = 0; i < 8; i++) { + var child = this.children[i], + min = child.min, + max = child.max; + + if (item[0] < max[0] && item[3] > min[0] && + item[1] < max[1] && item[4] > min[1] && + item[2] < max[2] && item[5] > min[2]) { + + //collision detected, add item + child.add(item, octree, depth + 1); + } + } + + return; + } + + if (!this.items) { + this.items = []; + } + + this.items.push(item); + + if (depth < octree.maxDepth && this.items.length >= octree.maxItemsPerNode) { + this.split(octree, depth + 1); + } +}; + +OctreeNode.prototype.split = function(octree, depth) { + var min = this.min, + max = this.max, + mid = [(max[0] + min[0]) * 0.5, (max[1] + min[1]) * 0.5, (max[2] + min[2]) * 0.5], + i, li, j; + + this.children = [ + null, null, + null, null, + null, null, + null, null + ]; + + this.depthCount[depth]++; + + for (i = 0; i < 8; i++) { + var combination = octree.pattern[i]; + + this.children[i] = new OctreeNode( + [ + (combination[0] === 0) ? min[0] : mid[0], + (combination[1] === 0) ? min[1] : mid[1], + (combination[2] === 0) ? min[2] : mid[2] + ], + + [ + (combination[0] === 0) ? mid[0] : max[0], + (combination[1] === 0) ? mid[1] : max[1], + (combination[2] === 0) ? mid[2] : max[2] + ] + ); + } + + var items = this.items; + + //distribute items + if (items) { + for (i = 0, li = items.length; i < li; i++) { + var item = items[i]; + + for (j = 0; j < 8; j++) { + var child = this.children[j]; + min = child.min; + max = child.max; + + if (item[0] < max[0] && item[3] > min[0] && + item[1] < max[1] && item[4] > min[1] && + item[2] < max[2] && item[5] > min[2]) { + + //collision detected, add item + child.add(item, octree); + } + } + } + } + + this.items = null; +}; + + +var OctreeRaycaster = function() { + + // A lookup-table containing octant ids. Used to determine the exit plane from an octant. + this.octantTable = [ + new Uint8Array([4, 2, 1]), + new Uint8Array([5, 3, 8]), + new Uint8Array([6, 8, 3]), + new Uint8Array([7, 8, 8]), + new Uint8Array([8, 6, 5]), + new Uint8Array([8, 7, 8]), + new Uint8Array([8, 8, 7]), + new Uint8Array([8, 8, 8]) + ]; + + // A byte that stores raycasting flags. + this.flags = 0; +}; + + +/** + * Finds the entry plane of the first octant that a ray travels through. + * + * Determining the first octant requires knowing which of the t0s is the + * largest. The tms of the other axes must also be compared against that + * largest t0. + * + * tx0, ty0,tz0 - Ray projection parameter. + * txm, tym, tzm - Ray projection parameter mean. + * returns - index of the first octant that the ray travels through. + */ + +OctreeRaycaster.prototype.findEntryOctant = function(tx0, ty0, tz0, txm, tym, tzm) { + var entry = 0; + + // Find the entry plane. + if(tx0 > ty0 && tx0 > tz0) { + + // YZ-plane. + if (tym < tx0) { + entry |= 2; + } + + if (tzm < tx0) { + entry |= 1; + } + + } else if (ty0 > tz0) { + + // XZ-plane. + if (txm < ty0) { + entry |= 4; + } + + if (tzm < ty0) { + entry |= 1; + } + + } else { + + // XY-plane. + if (txm < tz0) { + entry |= 4; + } + + if (tym < tz0) { + entry |= 2; + } + } + + return entry; +} + +/** + * Finds the next octant that intersects with the ray based on the exit plane of + * the current one. + * + * urrentOctant - The index of the current octant. + * tx1, ty1, tz1 - Ray projection parameter. + * returns - index of the next octant that the ray travels through. + */ + +OctreeRaycaster.prototype.findNextOctant = function(currentOctant, tx1, ty1, tz1) { + var min; + var exit = 0; + + // Find the exit plane. + if (tx1 < ty1) { + min = tx1; + exit = 0; // YZ-plane. + } else { + min = ty1; + exit = 1; // XZ-plane. + } + + if (tz1 < min) { + exit = 2; // XY-plane. + } + + return this.octantTable[currentOctant][exit]; +} + +/** + * Finds all octants that intersect with the given ray. + * + * octant - The current octant. + * tx0 - Ray projection parameter. Initial tx0 = (minX - rayOriginX) / rayDirectionX. + * ty0 - Ray projection parameter. Initial ty0 = (minY - rayOriginY) / rayDirectionY. + * tz0 - Ray projection parameter. Initial tz0 = (minZ - rayOriginZ) / rayDirectionZ. + * tx1 - Ray projection parameter. Initial tx1 = (maxX - rayOriginX) / rayDirectionX. + * ty1 - Ray projection parameter. Initial ty1 = (maxY - rayOriginY) / rayDirectionY. + * tz1 - Ray projection parameter. Initial tz1 = (maxZ - rayOriginZ) / rayDirectionZ. + * intersects - An array to be filled with the intersecting octants. + * returns + */ + +OctreeRaycaster.prototype.raycastOctant = function(octant, tx0, ty0, tz0, tx1, ty1, tz1, intersects) { + var children = octant.children; + var currentOctant; + var txm, tym, tzm; + + if (tx1 >= 0.0 && ty1 >= 0.0 && tz1 >= 0.0) { + + if (!children) { + + // Leaf. + if (octant.items) { + intersects.push(octant); + } + + } else { + + // Compute means. + txm = 0.5 * (tx0 + tx1); + tym = 0.5 * (ty0 + ty1); + tzm = 0.5 * (tz0 + tz1); + + currentOctant = this.findEntryOctant(tx0, ty0, tz0, txm, tym, tzm); + + do { + + /* The possibilities for the next node are passed in the same respective + * order as the t-values. Hence, if the first value is found to be the + * greatest, the fourth one will be returned. If the second value is the + * greatest, the fifth one will be returned, etc. + */ + + switch(currentOctant) { + + case 0: + this.raycastOctant(children[this.flags], tx0, ty0, tz0, txm, tym, tzm, intersects); + currentOctant = this.findNextOctant(currentOctant, txm, tym, tzm); + break; + + case 1: + this.raycastOctant(children[this.flags ^ 1], tx0, ty0, tzm, txm, tym, tz1, intersects); + currentOctant = this.findNextOctant(currentOctant, txm, tym, tz1); + break; + + case 2: + this.raycastOctant(children[this.flags ^ 2], tx0, tym, tz0, txm, ty1, tzm, intersects); + currentOctant = this.findNextOctant(currentOctant, txm, ty1, tzm); + break; + + case 3: + this.raycastOctant(children[this.flags ^ 3], tx0, tym, tzm, txm, ty1, tz1, intersects); + currentOctant = this.findNextOctant(currentOctant, txm, ty1, tz1); + break; + + case 4: + this.raycastOctant(children[this.flags ^ 4], txm, ty0, tz0, tx1, tym, tzm, intersects); + currentOctant = this.findNextOctant(currentOctant, tx1, tym, tzm); + break; + + case 5: + this.raycastOctant(children[this.flags ^ 5], txm, ty0, tzm, tx1, tym, tz1, intersects); + currentOctant = this.findNextOctant(currentOctant, tx1, tym, tz1); + break; + + case 6: + this.raycastOctant(children[this.flags ^ 6], txm, tym, tz0, tx1, ty1, tzm, intersects); + currentOctant = this.findNextOctant(currentOctant, tx1, ty1, tzm); + break; + + case 7: + this.raycastOctant(children[this.flags ^ 7], txm, tym, tzm, tx1, ty1, tz1, intersects); + // Far top right octant. No other octants can be reached from here. + currentOctant = 8; + break; + + } + + } while(currentOctant < 8); + + } + + } + +} + +OctreeRaycaster.prototype.hitFace = function(origin, dir, index, vertices) { + var EPSILON = 0.0000001, + v1 = [vertices[index], vertices[index+1], vertices[index+2]], + v2 = [vertices[index+3], vertices[index+4], vertices[index+5]], + v3 = [vertices[index+6], vertices[index+7], vertices[index+8]]; + + var h = [0,0,0], q = [0,0,0], s, a, f, u, v, + edge1 = [v2[0] - v1[0], v2[1] - v1[1], v2[2] - v1[2]], + edge2 = [v3[0] - v1[0], v3[1] - v1[1], v3[2] - v1[2]]; + + vec3.cross(dir, edge2, h); + a = vec3.dot(edge1, h); + + if (a > -EPSILON && a < EPSILON) { + return [false]; + } + + f = 1/a; + s = [origin[0] - v1[0], origin[1] - v1[1], origin[2] - v1[2]]; + u = f * (vec3.dot(s, h)); + + if (u < 0.0 || u > 1.0) { + return [false]; + } + + q = vec3.cross(s, edge1); + v = f * vec3.dot(dir, q); + if (v < 0.0 || u + v > 1.0) { + return [false]; + } + + // At this stage we can compute t to find out where the intersection point is on the line. + var t = f * vec3.dot(edge2, q); + //if (t > EPSILON) { // ray intersection + return [true, t]; //[origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t ]]; + //} else { // This means that there is a line intersection but not a ray intersection. + // return [false]; + //} +}; + + +/** + * Finds the octants that intersect with the given ray. The intersecting + * octants are sorted by distance, closest first. + * + * octree - An octree. + * intersects - A list to be filled with intersecting octants. + */ + +// https://github.com/vanruesc/sparse-octree/blob/master/src/core/OctreeRaycaster.js + +OctreeRaycaster.prototype.intersectOctree = function(rayPos, rayDir, octree, intersects) { + // Translate the octree extents to the scene origin. + var min = [0,0,0]; + var max = [octree.root.max[0] - octree.root.min[0], + octree.root.max[1] - octree.root.min[1], + octree.root.max[2] - octree.root.min[2]] + + var dimensions = [max[0], max[1], max[2]]; + var halfDimensions = [dimensions[0]*0.5, dimensions[1]*0.5, dimensions[2]*0.5]; + + var origin = [rayPos[0],rayPos[1],rayPos[2]]; + var direction = [rayDir[0], rayDir[1], rayDir[2]]; + + var invDirX, invDirY, invDirZ; + var tx0, tx1, ty0, ty1, tz0, tz1; + + var center = [(octree.root.max[0] + octree.root.min[0]) * 0.5, + (octree.root.max[1] + octree.root.min[1]) * 0.5, + (octree.root.max[2] + octree.root.min[2]) * 0.5] + + // Translate the ray to the center of the octree. + //origin.sub(octree.getCenter(v[2])).add(halfDimensions); + origin[0] = (origin[0] - center[0]) + halfDimensions[0]; + origin[1] = (origin[1] - center[1]) + halfDimensions[1]; + origin[2] = (origin[2] - center[2]) + halfDimensions[2]; + + // Reset all flags. + this.flags = 0; + + // Handle rays with negative directions. + if (direction[0] < 0.0) { + origin[0] = dimensions[0] - origin[0]; + direction[0] = -direction[0]; + this.flags |= 4; + } + + if (direction[1] < 0.0) { + origin[1] = dimensions[1] - origin[1]; + direction[1] = -direction[1]; + this.flags |= 2; + } + + if (direction[2] < 0.0) { + origin[2] = dimensions[2] - origin[2]; + direction[2] = -direction[2]; + this.flags |= 1; + } + + // Improve IEEE double stability. + invDirX = 1.0 / direction[0]; + invDirY = 1.0 / direction[1]; + invDirZ = 1.0 / direction[2]; + + // Project the ray to the root's boundaries. + tx0 = (min[0] - origin[0]) * invDirX; + tx1 = (max[0] - origin[0]) * invDirX; + ty0 = (min[1] - origin[1]) * invDirY; + ty1 = (max[1] - origin[1]) * invDirY; + tz0 = (min[2] - origin[2]) * invDirZ; + tz1 = (max[2] - origin[2]) * invDirZ; + + // Check if the ray hits the octree. + if (Math.max(Math.max(tx0, ty0), tz0) < Math.min(Math.min(tx1, ty1), tz1)) { + // Find the intersecting octants. + this.raycastOctant(octree.root, tx0, ty0, tz0, tx1, ty1, tz1, intersects); + } +}; + +OctreeRaycaster.prototype.intersectOctants = function(rayPos, rayDir, octants) { + var hits = []; + var t = Number.POSITIVE_INFINITY; + + for (var i = 0, li = octants.length; i < li; i++) { + var items = octants[i].items; + + for (var j = 0, lj = items.length; j < lj; j++) { + var item = items[j]; + var res = this.hitFace(rayPos, rayDir, item[7], item[6]); + + if (res[0] && res[1] < t) { + t = res[1]; + } + } + } + + if (t !== Number.POSITIVE_INFINITY) { + hits = [t]; + } + + return hits; +}; + +export {Octree, OctreeRaycaster};