//Declaring global variables representing DOM elements which will be used throughout the application var mapDom = document.getElementById('qd30map-cesium'); var placeholderDom = document.getElementById('loading-placeholder'); var navContainer = document.getElementById('nav-ui-wrap'); var navParent = document.getElementById('nav-arrows'); var navReset = document.getElementById('nav-reset'); var mapSearchBtn = document.getElementById('map-search'); var navElements = navParent.getElementsByTagName('button'); var searchForm = document.getElementById('map-ui-form'); var appContainer = document.getElementById('map-search'); //These will represent the coordinates of the App and Form, which will be used in centering functions (this is //especially helpful for mobile use var appX = null; var appY = null; var formX= null; var formY = null; navElements[0].tabIndex = 3; navElements[1].tabIndex = 4; navElements[2].tabIndex = 5; navElements[3].tabIndex = 6; navReset.tabIndex = 7; mapSearchBtn.tabIndex = 8; //Iterate over the navigation elements, add listener and add the appropriate camera function for (var i=0; i < navElements.length; i++) { navElements[i].addEventListener('mousedown', function(param) { switch(param.target.id) { case 'nav-up': camera.moveUp(100); break; case 'nav-down': camera.moveDown(100); break; case 'nav-left': camera.moveLeft(100); break; case 'nav-right': camera.moveRight(100); break; } }); navElements[i].addEventListener('keydown', function(param) { switch(param.target.id) { case 'nav-up': if (param.keyCode == 13) { camera.moveUp(100); } break; case 'nav-down': if (param.keyCode == 13) { camera.moveDown(100); } break; case 'nav-left': if (param.keyCode == 13) { camera.moveLeft(100); } break; case 'nav-right': if (param.keyCode == 13) { camera.moveRight(100); } break; } }); //Reset the view navReset.onclick = function(param) { gotoQD30(); }; navReset.onkeyup = function(param) { if (param.keyCode == 13) { gotoQD30(); } } } //Center the form -> especially for mobile users, as the map will take up the entire screen mapSearchBtn.onclick = function() { centerForm(); }; mapSearchBtn.onkeydown = function(param) { if (param.keyCode == 13) { centerForm(); } }; var zoomParent = document.getElementById('zoom'); var zoomElements = zoomParent.getElementsByTagName('button'); zoomElements[0].tabIndex = 1; zoomElements[1].tabIndex = 2; for (var a=0; a < zoomElements.length; a++) { zoomElements[a].addEventListener('mousedown', function(param) { var y = camera.position.y; switch(param.target.id) { case 'zoom-in': camera.moveForward(250); break; case 'zoom-out': camera.moveBackward(250); break; } }); zoomElements[a].addEventListener('keydown', function(param) { var y = camera.position.y; switch(param.target.id) { case 'zoom-in': if (param.keyCode == 13) { camera.moveForward(100); } break; case 'zoom-out': if (param.keyCode == 13) { camera.moveBackward(100); } break; } }); } //To check version // console.log(Cesium.VERSION); Cesium.BingMapsApi.defaultKey = 'mqrOOSbN2SMpDuWhHx0W~HVi_IW2A0UwRAV1xLIZuTQ~AnSezO-xLZ4_s1rf8ydB6Wf0aRdKtw_znPffJQ9qKbntyOPWFvitPVAvxT0v6dib'; //Instantiate the viewer. This is the primary object from which most functionality is derived var viewer = new Cesium.Viewer('qd30map-cesium', { animation : false, homeButton : false, vrButton : false, infoBox : true, geocoder : false, sceneModePicker: false, selectionIndicator: true, timeline : false, navigationHelpButton : false, navigationInstructionsInitiallyVisible: false, scene3DOnly : false, shadows : true, terrainShadows : true, baseLayerPicker : false }); var camera = viewer.camera; var scene = viewer.scene; var ellipsoid = scene.globe.ellipsoid; var canvas = viewer.canvas; var layers = viewer.scene.imageryLayers; var globe = viewer.scene.globe; //Reset the globe's imagery layers and begin with a light grey base globe.imageryLayers.removeAll(); globe.baseColor = Cesium.Color.fromCssColorString('#f3f3f3'); //Add a map tiling service over top of the base layer, to show the city street details var tonerLayer = layers.addImageryProvider(Cesium.createOpenStreetMapImageryProvider({ url : 'http://tile.stamen.com/toner/' })); tonerLayer.alpha = 0.2; // var terrainProvider = new Cesium.CesiumTerrainProvider({ // url : '//assets.agi.com/stk-terrain/world', // requestVertexNormals: true // }); //Remove tab index from the canvas, as this won't be necessary for keyboard only users canvas.setAttribute('tabindex', -1); var reset = document.getElementById('nav-reset'); reset.addEventListener('click', function() { gotoQD30(); }); //Load the datasource as a promise which triggers the "then" function upon loading var dataSource = Cesium.GeoJsonDataSource.load('/sites/quartierdix30/files/geojson/quartier_en.json').then(function(data) { viewer.dataSources.add(data); gotoQD30(); //entities.values provides the entities as individual features, as found in the geoJson file. Iterate over these and //endow them with the appropriate properties for display var entities = data.entities.values; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; //Buildings should have a black outline by default entity.polygon.outline = true; entity.polygon.outlineWidth = 1.2; entity.polygon.outlineColor = Cesium.Color.BLACK; //use extrudedHeight to stretch buildings to their appropriate height, as per their level property (this is //adjustable for each node) if (entity.properties.hasOwnProperty('level')) { entity.polygon.extrudedHeight = 0.5 * (entity.properties.level * 10); } //The value of colour for each feature is dependent on which sector the entity has been assigned to. //Boutique Node is paired with a Zone Node, and Zone Nodes are organized by Sector, which is how they derive their colour. //The value found in the json is added dynamically everytime the map data is rebuilt if (entity.properties.hasOwnProperty("colour")) { if (entity.properties.hasOwnProperty("event")) { //If the boutique has a special event, we use the StripeMaterialProperty to make it stand out entity.polygon.material = new Cesium.StripeMaterialProperty({ evenColor : Cesium.Color.fromCssColorString(entity.properties.colour), oddColor : Cesium.Color.BLACK, repeat : 10 }); } else { entity.polygon.material = Cesium.Color.fromCssColorString(entity.properties.colour); } } //The description property provides all of the markup for the infobox which pops up when a boutique is selected if (entity.properties.hasOwnProperty("description")) { entity.description = '
' + entity.properties.description + '
'; } } }); //This function takes us to the default view of the map function gotoQD30() { var pos = Cesium.Ellipsoid.WGS84.cartesianToCartographic(new Cesium.Cartesian3( 1278149.1996018, -4297423.8092246, 4522591.15943)); pos.latitude = Cesium.Math.toDegrees(pos.latitude); pos.longitude = Cesium.Math.toDegrees(pos.longitude); viewer.camera.flyTo({ destination : Cesium.Cartesian3.fromDegrees(pos.longitude, pos.latitude, pos.height), orientation : { heading : 5.51747, pitch : -1.419427, roll : 6.2675 }, duration: 0.5 }); } //Not currently used function clearLayers() { globe.imageryLayers.removeAll(); globe.baseColor = new Cesium.Color(0, 0, 0.5, 1); } //Not currently used function customLayer() { globe.imageryLayers.removeAll(); globe.baseColor = Cesium.Color.fromCssColorString('#f3f3f3'); var tonerLayer = layers.addImageryProvider(Cesium.createOpenStreetMapImageryProvider({ url : 'http://tile.stamen.com/toner/' })); tonerLayer.alpha = 0.2; } //Not currently used function highlight(chosenColour) { for (var j = 0; j < viewer.dataSources.get(0).entities.values.length; j++) { var entity = viewer.dataSources.get(0).entities.values[j]; if (entity.properties.hasOwnProperty('colour')) { if (entity.properties.colour.toLowerCase() == chosenColour.toLowerCase()) { entity.polygon.material = Cesium.Color.BLACK; entity.polygon.outlineWidth = 2.0; } } } } //Resets all boutiques to their default initial colour as per their colour and event properties function colourReset() { // for (z = viewer.entities.values.length -1; z >= 0; z--) { // if (viewer.entities.values[z].name.substring(0, 16) == 'selectionOutline') { // viewer.entities.remove(viewer.entities.values[z]); // } // } for (var j = 0; j < viewer.dataSources.get(0).entities.values.length; j++) { var entity = viewer.dataSources.get(0).entities.values[j]; if (entity.properties.hasOwnProperty('colour')) { if (entity.properties.hasOwnProperty("event")) { entity.polygon.material = new Cesium.StripeMaterialProperty({ evenColor : Cesium.Color.fromCssColorString(entity.properties.colour), oddColor : Cesium.Color.BLACK, repeat : 10 }); } else { entity.polygon.material = Cesium.Color.fromCssColorString(entity.properties.colour); entity.polygon.outlineWidth = 1.2; entity.polygon.outlineColor = Cesium.Color.BLACK; } } } } //Centers the view over the map canvas function centerApp() { window.scrollTo(appX, appY); } //Centers the view over the form function centerForm() { window.scrollTo(formX, formY); } //Helper function to determine position of an element function getPosition(el) { var xPos = 0; var yPos = 0; while (el) { if (el.tagName == "BODY") { // deal with browser quirks with body/window/document and page scroll var xScroll = el.scrollLeft || document.documentElement.scrollLeft; var yScroll = el.scrollTop || document.documentElement.scrollTop; xPos += (el.offsetLeft - xScroll + el.clientLeft); yPos += (el.offsetTop - yScroll + el.clientTop); } else { // for all other non-BODY elements xPos += (el.offsetLeft - el.scrollLeft + el.clientLeft); yPos += (el.offsetTop - el.scrollTop + el.clientTop); } el = el.offsetParent; } return { x: xPos, y: yPos }; } //Not currently used function getOffset( el ) { var _x = 0; var _y = 0; while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) { _x += el.offsetLeft - el.scrollLeft; _y += el.offsetTop - el.scrollTop; el = el.offsetParent; } return { top: _y, left: _x }; } //Determines whether or not an element is visible function domIsVisible(element) { var rect = element.getBoundingClientRect(); var html = document.documentElement; return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || html.clientHeight) && rect.right <= (window.innerWidth || html.clientWidth) ); } window.onload = function() { mapDom.style.display = 'inline-block'; searchForm.style.display = 'inline-block'; navContainer.style.display = 'block'; placeholderDom.style.display = 'none'; //Set the form and canvas X and Y values after loading the window var formPosition = getPosition(searchForm); formX = formPosition.x; formY = formPosition.y; var appPosition = getPosition(appContainer); appX = appPosition.x; appY = appPosition.y ; centerApp(); // var formAnchorTags = Array.from(searchForm.querySelectorAll("a")); // formAnchorTags.forEach(function(anchor) { // if (!anchor.children[0].classList.contains('hidden-nid')) { // console.dir(anchor.parentElement); // console.dir(anchor.parentNode); // } // if (anchor.parentElement.nodeName == 'SUMMARY') { // anchor.parentElement.tabIndex = -1; // } // // }); //Reload the stylesheet for the infobox, as it is an iframe and its styling needs to be enforced after the window has loaded var frame = viewer.infoBox.frame; var cssLink = frame.contentDocument.createElement('link'); cssLink.href = Cesium.buildModuleUrl('../../../../css/qd30map.css'); cssLink.rel = 'stylesheet'; cssLink.type = 'text/css'; frame.contentDocument.head.appendChild(cssLink); //Remove tabindex from elements within the header var header = document.getElementById('header'); var headChilds = header.getElementsByTagName('*'); for (var c = 0; c < headChilds.length; c++) { headChilds[c].tabIndex = -1; } var qdTitle = document.getElementById('block-quartierdix30-page-title'); var titleChilds = qdTitle.getElementsByTagName('*'); for (var d = 0; d < titleChilds.length; d++) { titleChilds[d].tabIndex = -1; } //Set a variable to represent the primary datasource, which contains all of the geojson features var data = viewer.dataSources.get(0); var subBtn = document.getElementById('edit-mapsubmit'); //Set a listener for the form's submit button, so submissions can be overridden subBtn.onclick = function(v) { v.preventDefault(); handleSubmit(); }; //Set the same type of listener for the autocomplete field, whose submit logic must also be overridden var autoComplete = document.getElementById('ui-id-1'); autoComplete.onclick = function() { handleSubmit(); }; autoComplete.onkeydown = function(param) { if (param.keyCode == 13) { handleSubmit(); } }; //Override selections based on clicking objects within the WebGL Canvas clickHandler = new Cesium.ScreenSpaceEventHandler(scene.canvas); clickHandler.setInputAction(function(movement) { var pickedObject = scene.pick(movement.position); if (Cesium.defined(pickedObject)) { for (var i = 0; i < data.entities.values.length; i++) { var entity = data.entities.values[i]; if (entity.properties.nid != undefined) { if (entity.properties.nid == pickedObject.id.properties.nid) { exploreStore(entity, data); } } } } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); //Iterate over all the anchor elements in the form and setup appropriate click/keyboard listeners to select stores var anchorElements = searchForm.querySelectorAll('.form-item'); for (var i = 0; i < anchorElements.length; i++) { anchorElements[i].tabIndex = 0; } for (var k=0; k < anchorElements.length; k++) { anchorElements[k].addEventListener('click', function(param) { for (var g=0; g < anchorElements.length; g++) { //Adding the selected-store class to the elements will cause them to appear highlighted within the form //We must first remove this from the previous selected element anchorElements[g].classList.remove('selected-store'); } var choice = param.target.innerHTML.trim(); for (var i = 0; i < data.entities.values.length; i++) { var entity = data.entities.values[i]; if (entity.properties.nid != undefined) { var formNid = choice.substring(choice.indexOf('d">') + 3, choice.indexOf(' 1) { selectStore(entity, data); } else { gotoQD30(); } if (!domIsVisible(appContainer)) { centerApp(); } this.classList.add('selected-store'); } } } }); //This logic is the same above, but for selection via keyboard anchorElements[k].addEventListener('keyup', function(param) { for (var g=0; g < anchorElements.length; g++) { anchorElements[g].classList.remove('selected-store'); } if (param.keyCode == 13) { var choice = param.target.innerHTML.trim(); for (var i = 0; i < data.entities.values.length; i++) { var entity = data.entities.values[i]; if (entity.properties.nid != undefined) { var formNid = choice.substring(choice.indexOf('d">') + 3, choice.indexOf(' 1) { selectStore(entity, data); } else { gotoQD30(); } if (!domIsVisible(appContainer)) { centerApp(); } this.classList.add('selected-store'); } } } } }); } //This listener is used to highlight multiple boutiques by store type var types = Array.from(document.querySelectorAll('summary > a.details-title')); types.forEach(function(type) { type.addEventListener('click', function(param) { var text = param.srcElement.lastChild.textContent.toLowerCase(); highlightByType(text, data); }); }); //this function is used to override form submission function handleSubmit() { var search = document.getElementById('edit-search'); var formNid = search.value.substring(search.value.indexOf('(') + 1, search.value.indexOf(')')); var length = data.entities.values.length; for (var i = 0; i < length; i++) { var entity = data.entities.values[i]; if (entity.properties.nid != undefined) { console.log(entity.properties.nid); console.log(formNid); if (formNid == entity.properties.nid) { if (entity.polygon.hierarchy._value.positions.length > 1) { selectStore(entity, data); } else { gotoQD30(); } } } } } //The primary function for selecting a boutique: finds the average point between the feature's coordinates, flies the camera //at an appropriate angle, colours the building and sets the viewer.selectedEntity property to it, triggering the display //of the infobox popup function selectStore(entity, data) { var pCoords = entity.polygon.hierarchy.getValue(viewer.clock.currentTime); var avgPoint = Math.round(pCoords.positions.length / 2); var topPoint = 0; for (var xy = 0; xy < pCoords.positions.length - 1; xy++) { if (pCoords.positions[xy+1].x > pCoords.positions[xy].x) { topPoint = xy; } } var pos = Cesium.Ellipsoid.WGS84.cartesianToCartographic(pCoords.positions[avgPoint]); pos.latitude = Cesium.Math.toDegrees(pos.latitude); pos.longitude = Cesium.Math.toDegrees(pos.longitude); viewer.camera.flyTo({ destination : Cesium.Cartesian3.fromDegrees(pos.longitude, pos.latitude, 500), orientation : { heading : 5.51747, pitch : -1.419427, roll : 6.2675 }, duration: 1 }); colourReset(); //Previously, we wrapped highlighted buildings with a glowing polyline to improve contrast // viewer.entities.add({ // name : 'selectionOutline', // polyline : { // positions : pCoords.positions, // width : 4, // material : new Cesium.PolylineGlowMaterialProperty({ // glowPower : 0.5, // color : Cesium.Color.fromCssColorString('#008B8B'), // extrudedHeight : entity.polygon.extrudedHeight + 20 // }) // } // }); entity.polygon.material = Cesium.Color.fromCssColorString('#000000'); entity.polygon.material.outlineColor = Cesium.Color.fromCssColorString('#008B8B'); entity.polygon.material.outlineWidth = 5; viewer.selectedEntity = entity; } //this function is similar to above, but used for clicking on the canvas. This preserves the zoom height which the user //is previously using, such as to avoid the confusion of changing height each time a building is clicked on function exploreStore(entity, data) { var pCoords = entity.polygon.hierarchy.getValue(viewer.clock.currentTime); var cartographic = new Cesium.Cartographic(); var camHeight = ellipsoid.cartesianToCartographic(camera.position, cartographic).height; // var pos = Cesium.Ellipsoid.WGS84.cartesianToCartographic(pCoords.positions[0]); // pos.latitude = Cesium.Math.toDegrees(pos.latitude); // pos.longitude = Cesium.Math.toDegrees(pos.longitude); var avgPoint = Math.round(pCoords.positions.length / 2); var pos = Cesium.Ellipsoid.WGS84.cartesianToCartographic(pCoords.positions[avgPoint]); pos.latitude = Cesium.Math.toDegrees(pos.latitude); pos.longitude = Cesium.Math.toDegrees(pos.longitude); // var topPoint = 0; // // for (var xy = 0; xy < pCoords.positions.length - 1; xy++) { // if (pCoords.positions[xy+1].x > pCoords.positions[xy].x) { // topPoint = xy; // } // } viewer.camera.flyTo({ destination : Cesium.Cartesian3.fromDegrees(pos.longitude, pos.latitude, camHeight), orientation : { heading : camera.heading }, duration: 1 }); colourReset(); // pCoords.positions.forEach(function(position) { // position.x += 0.33; // position.y += 0.33; // }); // var selectionOutline = viewer.entities.add({ // name : 'selectionOutline', // polyline : { // positions : pCoords.positions, // width : 4, // material : new Cesium.PolylineGlowMaterialProperty({ // glowPower : 0.5, // color : Cesium.Color.fromCssColorString('#008B8B'), // extrudedHeight : entity.polygon.extrudedHeight + 20 // }) // } // }); entity.polygon.material = Cesium.Color.fromCssColorString('#000000'); entity.polygon.material.outlineColor = Cesium.Color.fromCssColorString('#008B8B'); entity.polygon.material.outlineWidth = 5; viewer.selectedEntity = entity; } //This is the function used to highlight multiple boutiques based on their store type (restaurant, entertainment, "accepts gift cards", etc) function highlightByType(type, data) { colourReset(); // for (z = 0; z < viewer.entities.values.length; z++) { // if (viewer.entities.values[z].name.substring(0, 16) == 'selectionOutline') { // viewer.entities.remove(viewer.entities.values[z]); // } // } for (var j = 0; j < data.entities.values.length; j++) { var entity = data.entities.values[j]; if (entity.properties.hasOwnProperty('storetypes')) { if (entity.properties.storetypes.toLowerCase().match(type)) { var pCoords = entity.polygon.hierarchy.getValue(viewer.clock.currentTime); matched = true; entity.polygon.material = Cesium.Color.fromCssColorString('#000000'); entity.polygon.material.outlineColor = Cesium.Color.fromCssColorString('#008B8B'); entity.polygon.material.outlineWidth = 5; // viewer.entities.add({ // name : 'selectionOutline', // polyline : { // positions : pCoords.positions, // width : 4, // material : new Cesium.PolylineGlowMaterialProperty({ // glowPower : 0.5, // color : Cesium.Color.fromCssColorString('#008B8B'), // extrudedHeight : entity.polygon.extrudedHeight + 20 // }) // } // }); } } } } //Logic to close all non-target details elements //This was important to keep the use of the form from becoming confusing, as there are many collapsible details elements var details = Array.from(document.querySelectorAll("details")); details.forEach(function(detail) { if (detail.constructor.name === 'HTMLDetailsElement') { detail.addEventListener('click', function() { for (f = 0; f < details.length; f++) { details[f].classList.remove('selected-detail'); } this.classList.add('selected-detail'); subDet = this.querySelectorAll('details'); for (g = 0; g < subDet.length; g++) {subDet[g].classList.add('selected-detail');} closeOthers(this); }); } }); //helper function used in the above listeners function closeOthers(elem) { for (var i = 0; i < details.length; i++) { if (!details[i].contains(elem)) { var selectedFound = false; var elems = details[i].querySelectorAll('*'); for (z = 0; z < elems.length; z++) { if (elems[z].classList.contains('selected-detail') || elems[z].classList.contains('selected-store')) { selectedFound = true; } } if (selectedFound == false) { details[i].removeAttribute('open'); } } else { details[i].setAttribute('open', 'open'); } } } //This helper function is no longer used, but was helpful during development to find the visible coordinates on the canvas function getExtentView() { var cl2 = new Cesium.Cartesian2(0, 0); var leftTop = viewer.scene.camera.pickEllipsoid(cl2, ellipsoid); cr2 = new Cesium.Cartesian2(viewer.scene.canvas.width, viewer.scene.canvas.height); var rightDown = viewer.scene.camera.pickEllipsoid(cr2, ellipsoid); if (leftTop != null && rightDown != null) { leftTop = ellipsoid.cartesianToCartographic(leftTop); rightDown = ellipsoid.cartesianToCartographic(rightDown); return new Cesium.Rectangle(leftTop.longitude, rightDown.latitude, rightDown.longitude, leftTop.latitude); } else { console.log("Sky is visible"); return null; } } }; //It is important to update the values of the form and app positions upon window resize window.onresize = function() { var formPosition = getPosition(searchForm); formX = formPosition.x; formY = formPosition.y; var appPosition = getPosition(appContainer); appX = appPosition.x; appY = appPosition.y; }; //This jQuery code was being used for the map legend (function($, Drupal) { Drupal.behaviors.qd30map = { attach: function(context, settings) { $('.closemenu').on('mouseenter', function() { $(this).find('span').show(); }).on('mouseleave', function() { $(this).find('span').hide(); }).on('click tap', function() { $(this).parent().parent().children('div').fadeOut(150); $(this).parent().parent().delay(50).animate({width: 'toggle'}, 300); if ($(this).parent().hasClass('closeinfo')) { infoOpen = false; $('.selected').removeClass('selected'); } }); $('#open-legend').on('click tap', function() { $('.map-legend-info').animate({width:'toggle'}, 300); $('.map-legend-info > div').delay(150).fadeIn(150); }); }}})(jQuery, Drupal); //Helper function not currently being used function fade(element) { var op = 1; // initial opacity var timer = setInterval(function() { if (op <= 0.1) { clearInterval(timer); element.style.display = 'none'; } element.style.opacity = op; element.style.filter = 'alpha(opacity=' + op * 100 + ")"; op -= op * 0.1; }, 150); }