/* * # motMap * MOT reusable mashup functions */ mM = { version: "0.4.2", // Set the version manually /* * ## Layer definition functions * Called after the layer array from the config file has been read * * All layers are stored in the global object *mM*. The name is derived * from the *objName* attribute set in the config file. * So if the objName is called *myLayer*... You can access the layer * object anytime from *mM.myLayer*. */ /* * ### addTMS * Create an openlayers layer object for a TMS (Tile Map Service) layer. * This can only be used if the data source follows a standard * [OGC](http://www.opengeospatial.org/standards/wmts) tiling scheme. * @param l {object} Layer object from the config file */ addTMS: function (l) { this[l.objName] = new OpenLayers.Layer.TMS(l.objName, l.url, { // Parameters "domains":l.domains, "type":String(l.imageType), 'getURL':mM.getTMS, 'isBaseLayer': l.base, 'visibility': false } ); mM.map.addLayer(this[l.objName]); // Add to the map }, /* * ### addESRIRest * Create an openlayers layer object for a ESRI Rest tile service * @param l {object} Layer object from the config file */ addEsriRest: function (l) { this[l.objName] = new OpenLayers.Layer.ArcGIS93Rest( l.objName, l.url, null, { isBaseLayer: l.base } ); mM.map.addLayer(this[l.objName]); // Add to the map }, /* * ### addGoogle * Create an openlayers layer object for the Google basemap. * @param l {object} Layer object from the config file */ addGoogle: function (l) { var view; switch (l.layerView) { case "streets": view = google.maps.MapTypeId.ROADMAP; break; case "terrain": view = google.maps.MapTypeId.TERRAIN; break; case "satellite": view = google.maps.MapTypeId.SATELLITE; break; case "hybrid": view = google.maps.MapTypeId.HYBRID; break; default: view = google.maps.MapTypeId.ROADMAP; break; } // Define the layer object this[l.objName] = new OpenLayers.Layer.Google( l.objName, {type: view} ); mM.map.addLayer(this[l.objName]); // Add to the map // $(this[l.objName].div).css({top:"30px"}); $(this[l.objName].div).load(function() { $(this).css({top:"30px"}); }); }, /* * ### addBing * Create an openlayers layer object for a TMS (Tile Map Service). * Specific to Microsoft's Bing layers... Which don't follow a * standard tile format. * @param l {object} Layer object from the config file */ addBing: function (l) { var url = l.url; if (typeof(l.apiKey) !== 'undefined') { // If a Bing API key exists, combine the url and key if (l.apiKey !== null && l.apiKey !== 'null' && l.apiKey !== '') { url += 'key='+l.apiKey+'&'; } } this[l.objName] = new OpenLayers.Layer.TMS( // Create the openlayers layer object l.layerView, url, { 'type':'jpg', 'getURL':mM.getBing, 'isBaseLayer': l.base, 'visibility': false } ); mM.map.addLayer(this[l.objName]); // Add to the map }, /* * ### AddWFSCluster * Create an Openlayers Cluster object. It leverages the * AnimatedCluster plugin which is separate form the Openlayers * core. * @param l {object} Layer object from the config file */ addWFSCluster: function (l) { mM[l.objName] = new OpenLayers.Layer.Vector(l.objName, { strategies: [ new OpenLayers.Strategy.Fixed(), new OpenLayers.Strategy.AnimatedCluster({ distance: 45, animationMethod: OpenLayers.Easing.Expo.easeOut, animationDuration: 10 }) ], protocol: new OpenLayers.Protocol.HTTP({ url: l.url, params: { outputFormat: "JSON", typeName: l.layerSchema+":"+l.layerWFS, service: "WFS", version: l.version, request: "GetFeature", srsName: l.projection, maxfeatures: 10000 }, format: new OpenLayers.Format.GeoJSON() }), styleMap: l.styleMap }); mM.map.addLayer(mM[l.objName]); // Add to the map if (l.on) { mM.layerOn(l.objName); } else { mM.layerOff(l.objName); } }, /* * ### addWMS * Create a layer object from a WMS data source. * DataBC layers don't have a schema... So deal with that here. * @param l {object} Layer object from the config file */ addWMS: function (l) { var layerName = (l.layerSchema) ? l.layerSchema+":"+l.layerWMS : l.layerWMS; mM[l.objName] = new OpenLayers.Layer.WMS(l.objName, // Create the openlayers layer object l.url, { layers: layerName, transparent: true, styles: (l.styles) ? l.styles : "", tiled: (l.ignoreGWC) ? false : "" },{ isBaseLayer: l.base, singleTile: (l.singleTile) ? l.singleTile : false, transitionEffect: (l.transitionEffect) ? l.transitionEffect : null } ); // Set transparency if (!OpenLayers.Util.alphaHack()) { mM[l.objName].setOpacity(l.opacity); } // Add the layer to the map mM.map.addLayer(mM[l.objName]); if (l.on) { mM.layerOn(l.objName); } else { mM.layerOff(l.objName); } }, /* * ### addAttribution * Add the attribution for the specified base layer. * There needs to be attribution information entered into the mapConfig * file in order for this to work. It should look something like this: * > attributeLogo: "http://dev.virtualearth.net/Branding/logo_powered_by.png", * > attributeLogoLink: "http://dev.virtualearth.net/Branding/logo_powered_by.png", * > attributeText: "Basemap courtesy of " * Only those parameters that are specified will show on the map. * * @param l {object} Layer object from the config file */ addAttribution: function (l) { if ($("#map-main #attribution").length) // If there's a previous attribution $("#map-main #attribution").remove(); // Remove it. var str = "
"; // Initialize string if (l.attributeText) str += ""+l.attributeText+""; // Add text if existing if (l.attributeLogoLink) str += ""; if (l.attributeLogo) str += ""; if (l.attributeLogoLink) str += ""; // Close link // This for the printing template only. It's hidden on screen. str += ""; // The print logo str += "
"; // Close the string $(str).appendTo("#map-main"); // Add attribution to the map window }, /* * ### contextSize * Method for calculating size of the context pane which * may or may not have rendered. */ contextSize: function () { var height = $(window).outerHeight(true) - $("#context hr").offset().top - $("#context hr").outerHeight(true); // This 13 number is a total hack var width = $("#context").outerWidth(true); return {h:height,w:width}; }, /* * ### layerOn * Turn layer on. * Takes the layer/object name as an argument. * The same value that is stored in the config file as "objName". * @param n {string} The layer object name. This gets specified * in the config file with the **objName** variable. */ layerOn: function (n) { $("#"+n+"-lyr a").addClass("active"); // Check the checkbox $("#"+n+"-lyr"). // Old fontawesome find(".lyr-ico .icon-check-empty"). removeClass("icon-check-empty"). addClass("icon-check"); $("#"+n+"-lyr"). // New fontawesmoe find(".lyr-ico .fa-square-o"). removeClass("fa-square-o"). addClass("fa-check-square-o"); if (mM[n]) {mM[n].setVisibility(true);} // Turn on WMS layer $.each(mM.config.layers,function(i,d) { // Make sure the layer in on in the config if (d.objName === n) mM.config.layers[i].on = true; }); }, /* * ### layerOff * Turn layer off. * @param n {string} The layer object name. This gets specified * in the config file with the **objName** variable. */ layerOff: function (n) { $("#"+n+"-lyr a").removeClass("active"); $("#"+n+"-lyr"). // Old fontawesome find(".lyr-ico .icon-check"). removeClass("icon-check"). addClass("icon-check-empty"); $("#"+n+"-lyr"). // New fontawesmoe find(".lyr-ico .fa-check-square-o"). removeClass("fa-check-square-o"). addClass("fa-square-o"); if (mM[n]) {mM[n].setVisibility(false);} // Turn on WMS layer // Make sure the layer in off in the config $.each(mM.config.layers,function(i,d) { if (d.objName === n) mM.config.layers[i].on = false; }); }, /* * ### layerActivity * Take the current zoom level and determine based on the layer configuration * what layers should be active... And ones that shouldn't be. * @param z {integer} Zoom level. Could be 0-22. */ layerActivity: function (z) { $.each( // Cycle through all the layers mM.config.layers, function (i,l) { if (l.on && //Must be on l.visible && // and visible !l.base && // and not a base layer (z > l.maxZoom || z < l.minZoom)) { // and outside our zooming tolerance. mM[l.objName].setVisibility(false); // Shouldn't be visibe } else if (l.on && // Otherwise must be on l.visible && // and visible !l.base && // and not a base layer z >= l.minZoom && z <= l.maxZoom) { // and within our zooming tolerance. mM[l.objName].setVisibility(true); // Should be visible } }); }, toGeocoder: function (string) { $.ajax({ url: mM.env.dataBCGeoCoderUrl, dataType: "jsonp", // Different origin so use jsonp. data: { // Some settings. May change later maxResults: 25, outputSRS: 4326, fullAddress: string }, jsonp: "callback", success: function (data) { if (data.features[0]) { return data.features[0]; } else { // Return the first entry return null; } } }); }, /* * ##Tile algorithm functions * Each tile datasource has a specific layout of image tiles. * Each function below returns the url for the image specific to the bounds sent * by the OpenLayers TMS function. * * ### getTMS * General TMS function. * This should hopefully take care of most standard layouts * TODO: Add OGC tiling scheme options which reverses the Y axis. * @param bounds {object} The openlayers bounds object. * @return url {string} URL path to the image tile */ getTMS: function (bounds) { var res = this.map.getResolution(); var x = Math.round ((bounds.left - this.maxExtent.left) / (res * this.tileSize.w)); var y = Math.round ((this.maxExtent.top - bounds.top) / (res * this.tileSize.h)); var z = this.map.getZoom(); var url = this.url; if (this.domains && this.domains.length > 0) { // If subdomains were specified choose one at random var n = this.domains[Math.random() * 4 | 0]; // random tile domain. url = url.replace("{n}",n); } /* * Save this little item for later * Here is the OGC Y standard * var y = Math.round((bounds.bottom - this.tileOrigin.lat) / (res * this.tileSize.h)); */ // Substitute values into the url url = url.replace("{x}",x); url = url.replace("{y}",y); url = url.replace("{z}",z); return url; }, /* * ### getBing * The Bing (Microsoft) basemap has a custom tiling scheme * @param bounds {object} The openlayers bounds object. * @return url {string} URL path to the image tile */ getBing: function (bounds) { var res = this.map.getResolution(); var x = Math.round ((bounds.left - this.maxExtent.left) / (res * this.tileSize.w)); var y = Math.round ((this.maxExtent.top - bounds.top) / (res * this.tileSize.h)); var z = this.map.getZoom(); var n = ["1", "2", "3", "4"][Math.random() * 4 | 0]; // random tile domain. var tileName = this.name; for (var iT = 1; iT <= z; iT++) { tileName += (((y >> z - iT) & 1) << 1) | ((x >> z - iT) & 1); } var url = this.url; url = url.replace("{n}",n); url = url.replace("{tileName}",tileName); return url; }, /* * ## Generic helper functions * * ### geocoder * * Grab all *geocoder* classed inputs and connect them * to the GeoBC geocoder. * Use the Bootstrap jQuery helper 'typeahead' * * **IMPORTANT** * Geocoder forms must be within their own parent container. * If there is more then one geocoder element in the same div the data * binding will get confused and bad things will happen. * * @param selector {text} The sizzle (jQuery) type selector for * finding the input element. */ geocoder: function (selector) { $(selector).typeahead({ items: mM.config.geocoder.maxItems, // The max number of visible options comes from the config file. matcher: function (items) { // Remove the exact match setting which is default return items; }, updater: function (item) { // When an entry is selected zoomToGeoloc(); // Zoom action return item; // return value so form is populated }, source: function (string,response) { // data source is the function that calls GeoBC Geocoder $.ajax({ url: mM.env.dataBCGeoCoderUrl, dataType: "jsonp", // Different origin so use jsonp. data: { // Some settings. May change later maxResults: mM.config.geocoder.maxItems, // Max results from service. outputSRS: 4326, addressString: string }, jsonp: "callback", success: function (data) { if(!data.features){return;} // exit if no features present var options = $.map(data.features,function(i) { // Map the place names out of the reponse hash. var place = i.properties.fullAddress; // Don't want the BC default. place = (place.match(/^BC$/)) ? "Keep trying" : place; return place; }); response(options); // Send name array to typeahead. /* * As far as I can tell this function is synchronous. So... * It allows us to bind the coordinate values to the elements * that are placed in the drop down list. * * It's important to note that items returned from the geocoder * aren't necessarily visible in the dropdown. This is because the * jQuery Typeahead plugin further filters what is visible based * on what the user is typing. */ // First cycle through all elements in the dropdown $(selector).parent().find("li").each(function () { var that = this; // Save this in that var name = $(this).attr("data-value"); // grab the name $.each(data.features, function (i,d) { // Now cycle through the features return by the geocoder if (d.properties.fullAddress.match(name)) { // If we have a match... $(that). // Bind it's geometry to the element. data({"geometry":data.features[i].geometry}); } }); }); } }); } }); /* * #### zoomToGeolocation * Look for the active record in the geolocation dropdown * Grab the bound coordinates and zoom to location. */ function zoomToGeoloc () { // Extract geometry from the selected/active typeahead element. var geom = $(selector).parent().find("li[class=active]").data("geometry"); /* * Currently the Geocoder only gives out single point features... * So we don't need to worry about querying the feature type. For now. */ var lon = geom.coordinates[0]; // Grab Longitude var lat = geom.coordinates[1]; // Grab Latitude // Set the center point in Lat and Long var centerLatLon = new OpenLayers.LonLat(lon, lat); // Convert to map coordinates var centerMerc = centerLatLon.transform(new OpenLayers.Projection("EPSG:4326"),mM.map.getProjectionObject()); // Move and zoom to new center point mM.map.setCenter(centerMerc, mM.config.geocoder.zoom); // Destroy the the previous geocoder marker layer (if it exists) mM.destroyGeoCoderMarkers(); // Create the geocoder marker layer and add it to the map var geocoderMarkerLayer = new OpenLayers.Layer.Markers("GeoCoderMarkerLayer"); mM.map.addLayer(geocoderMarkerLayer); // Add a marker using the resulting geocoder coordinates geocoderMarkerLayer.addMarker(new OpenLayers.Marker(centerMerc, mM.config.geoCoderMarkerIcon)); } }, /* * ### geocder2 * Next generation geocoder that handles multiple sources of data. * Taking the original geocoder with the DataBC geocoder logic. Then * adding the option of loading any number of WMS layers. Layers must * have a geocoder entry in the config file. * @param selector {string} jQuery selector string for the text input form. */ geocoder2: function (selector,callback) { var prefs = { // General typeahead settings hint: true, highlight: true }; // This is the dataBC config and should always be present var gcGeoBC = new Bloodhound ({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, limit: mM.config.geocoder.maxItems, remote: { url: mM.env.dataBCGeoCoderUrl+ "&maxResults="+mM.config.geocoder.maxItems+ "&outputSRS=4326"+ "&addressString=%QUERY", ajax: { dataType: "jsonp", // Different origin so use jsonp. jsonp: "callback" }, filter: function (d) { var options = $.map(d.features,function(i) { var place = i.properties; place.geometry = i.geometry; place.lon = i.geometry.coordinates[0].toFixed(4); // Add longitude place.lat = i.geometry.coordinates[1].toFixed(4); // Add Latitude return place; }); return options; } } }); gcGeoBC.initialize(); // Setup the Bloodhound preferences for the geoBC source. var sourceGeoBC = { name: "places", minLength: 4, source: gcGeoBC.ttAdapter(), templates: { header: mM.config.geocoder.template.header, suggestion: Handlebars.compile(mM.config.geocoder.template.body) } }; var sources = [sourceGeoBC]; // Add to the source array /* * This is where I'll cycle through the legend... Seeing what layers: * 1. Have a geocoder configuration AND * 2. Are turned on OR * 3. Are "alwaysOn" * This is for the typeahead data source array. */ $("#layer-drawer div.layer").each(function(i,e) { var layer = $(e).data(); // Grab the bound data. if (!("geocoder" in layer)) return true; // Skip if no geocoder config. var active = $(e).find("a.lyr-ico").hasClass("active"); // active layers var alwaysSearch = (layer.geocoder.alwaysSearch) ? true : false; // if (active || alwaysSearch) { // If active or the always Search flag is set sources.push(configSource(layer)); // Add to data source array // } return true; }); // Now we can connect everything together $(selector). typeahead("destroy"). typeahead(prefs,sources). on("typeahead:selected",zoomToGeoloc); /* * #### configSource * Take the layer object and create a typeAhead configered object * It needs to conform to the Geoserver rest spec * @param layer {object} Layer object from the config file * @return {object} Typeahead data source object */ function configSource (layer) { var gcAnother = new Bloodhound ({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, limit: mM.config.geocoder.maxItems, remote: { url: layer.geocoder.url, ajax: { dataType: "json", contentType: "text/plain", type: "POST", data: layer.geocoder.xml, beforeSend: function (jqXhr,settings) { var query = $("#start-here input:not([disabled])").val(); settings.data = settings.data.replace(/%QUERY%/g,"%"+query+"%"); } }, filter: function (d) { // This assumes we received a valid geometry var options = $.map(d.features,function(i) { var place = i.properties; var bbox = new OpenLayers.Bounds(place.bbox[0],place.bbox[1],place.bbox[2],place.bbox[3]); var centroid = bbox.getCenterLonLat(); place.placeHolder = i.properties[layer.geocoder.column]; // Do we really need this? place.geometry = i.geometry; place.lon = centroid.lon.toFixed(mM.config.geocoder.precision); // Add longitude place.lat = centroid.lat.toFixed(mM.config.geocoder.precision); // Add Latitude place.layer = layer; // Add Latitude return place; }); return options; } } }); gcAnother.initialize(); // Initialize return { // Return the layer config name: layer.objName, source: gcAnother.ttAdapter(), templates: { header: layer.geocoder.header, suggestion: Handlebars.compile(layer.geocoder.template) } }; } /* * #### zoomToGeolocation * Look for the active record in the geolocation dropdown. * If we're lucky enough to have a bounding box... Zoom to that * extent. Otherwise, fall back to lat and lon which is standard * for the Provincial geocoder. */ function zoomToGeoloc (e,d) { if ("bbox" in d) { // If there's a bounding box mM.map.zoomToExtent( new OpenLayers.Bounds(d.bbox). transform("EPSG:4326", mM.map.getProjectionObject())); $(selector).typeahead("val",d.placeHolder); // Populate placeholder return; // May as well get out. } /* * Otherwise we are left with just a point */ // Extract geometry from the selected/active typeahead element. var geom = d.geometry; /* * Currently the Geocoder only gives out single point features... * So we don't need to worry about querying the feature type. For now. */ var lon = geom.coordinates[0]; // Grab Longitude var lat = geom.coordinates[1]; // Grab Latitude // Set the center point in Lat and Long var centerLatLon = new OpenLayers.LonLat(lon, lat); // Convert to map coordinates var centerMerc = centerLatLon.transform( new OpenLayers.Projection("EPSG:4326"), mM.map.getProjectionObject()); // Move and zoom to new center point mM.map.setCenter(centerMerc, mM.config.geocoder.zoom); // Destroy the the previous geocoder marker layer (if it exists) mM.destroyGeoCoderMarkers(); // Load the selected entry into the form if ("fullAddress" in d) { // If the standard DataBC geocoder $(selector).typeahead("val",d.fullAddress); // } else { // $(selector).typeahead("val",d.placeHolder); } // Create the geocoder marker layer and add it to the map var geocoderMarkerLayer = new OpenLayers.Layer.Markers("GeoCoderMarkerLayer"); mM.map.addLayer(geocoderMarkerLayer); // Add a marker using the resulting geocoder coordinates geocoderMarkerLayer.addMarker(new OpenLayers.Marker(centerMerc, mM.config.geoCoderMarkerIcon)); } }, /* * ### getIntersections * Use the [BC Geocoder](http://www.data.gov.bc.ca/dbc/geographic/locate/geocoding.page) * to reverse geocode intersections to major highways. * @param loc {object} Openlayers LatLon object. * @param name {string} The name in the header of the context window. * @param limit {integer} Max number of features to return. * @param callback {object} Async.js callback function. * @return {object} Object containing the name and a feature array. */ getIntersections: function (loc,name,limit,callback) { limit = (limit) ? limit : 10; // Default limit of 10. var bbox = [ // Calculate the bbox to query loc.lon - 1000, loc.lat - 1000, loc.lon + 1000, loc.lat + 1000 ].join(","); var url = "https://apps.gov.bc.ca/pub/geocoder/intersections/within.geojsonp?"+ "bbox="+bbox+"&"+ "outputSRS=3005&"+ "minDegree=2&"+ "maxDegree=100"; $.ajax({ // Send request type: "GET", url: url, timeout: 10000, dataType: "jsonp", jsonp: "callback"}). done(function (data) { var sortedFeatures = data.features.sort(function (a,b) { // Sort by proximity var pointA = new OpenLayers.Geometry.Point(a.geometry.coordinates[0],a.geometry.coordinates[1]); var pointB = new OpenLayers.Geometry.Point(b.geometry.coordinates[0],b.geometry.coordinates[1]); var geom = new OpenLayers.Geometry.Point(loc.lon,loc.lat); // Click geometry var distA = pointA.distanceTo(geom); var distB = pointB.distanceTo(geom); return distA - distB; }).filter(function (data) { // Only want highways. return data.properties.intersectionName.match(/hwy/i); }); // Now that we are sorted by proximity... Apply the limit var limitedFeatures = sortedFeatures.slice(0,limit); // Return the final array like this var bundle = {name: name,features: limitedFeatures}; callback(null,bundle); }). error(function(jqXHR, textStatus, error) { callback(textStatus+": "+error);// Return error }); }, /* * ### getGazetted * Get geographic features local to a point. * Based the on the [BC Gov Geographic Names service](http://apps.gov.bc.ca/pub/api/bcgnws/rest_search.html) * This method is meant to be called within an asynchronous manager * like [async.js](https://github.com/caolan/async). * The following feature class types are available and should be specified * through the *name* parameter: * - Populated * - Administrative Areas * - Water Features * - Terrain Features * - Vegetative Features * - Constructed Features * - Unclassed Features * An exact match is required. * @param loc {object} Openlayers LatLon object * @param name {string} The feature class name identified by the API. * This also shows up in the header of the results. * @param limit {integer} Max number of features to return * @param distance {integer} Number of kilometers to search * @param callback {object} Async.js callback function * @return {object} Object containing the name and a feature array. */ getGazetted: function (loc,name,limit,distance,callback) { var point = new OpenLayers.LonLat(loc.lon, loc.lat). // point must be in geo transform(new OpenLayers.Projection("EPSG:3005"),new OpenLayers.Projection("EPSG:4326")); limit = (limit) ? limit : 10; // Default limit of 10. distance = (distance) ? distance : 5; // Default window of 5km callback = (callback) ? callback : function () {}; // If there's no callback var fc; switch (name) { case "Populated": fc = 1; break; case "Administrative Areas": fc = 2; break; case "Water Features": fc = 3; break; case "Terrain Features": fc = 4; break; case "Vegetative Features": fc = 5; break; case "Constructed Features": fc = 6; break; case "Unclassed Features": fc = 7; break; default: fc = 1; break; } var url = "https://apps.gov.bc.ca/pub/bcgnws/names/near?"+ "featurePoint="+point.lon+","+point.lat+"&"+ // Point coordinate in geog "distance="+distance+"&"+ // Radius to search "itemsPerPage="+100+"&"+ // The max number of records to return "featureClass="+fc+"&"+ // The feature class "outputFormat=jsonx&"+ // Unorthodoxed... But jsonx is what the API wants "outputSRS=3005"; $.ajax({ // Send request type: "GET", url: url, timeout: 10000, dataType: "jsonp", jsonp: "callback"}). done(function (data) { var sortedFeatures = data.features.sort(function (a,b) { var pointA = new OpenLayers.Geometry.Point(a.geometry.coordinates[0],a.geometry.coordinates[1]); var pointB = new OpenLayers.Geometry.Point(b.geometry.coordinates[0],b.geometry.coordinates[1]); var geom = new OpenLayers.Geometry.Point(loc.lon,loc.lat); // Click geometry var distA = pointA.distanceTo(geom); var distB = pointB.distanceTo(geom); return distA - distB; }); // Now that we are sorted by proximity... Apply the limit var limitedFeatures = sortedFeatures.slice(0,limit); // Return the final array like this var bundle = {name: name,features: limitedFeatures}; callback(null,bundle); }). error(function(jqXHR, textStatus, error) { callback(textStatus+": "+error);// Return error }); }, /* * ### destroyGeoCoderMarkers * Destroys the geocoder marker layer. */ destroyGeoCoderMarkers: function () { $.each(mM.map.layers, function (index,layer) { if (layer.name == "GeoCoderMarkerLayer") { layer.destroy(); } }); }, /* * ### destroySurveyLocationMarkers * Destroys the survey location marker layer. */ destroySurveyLocationMarkers: function () { $.each(mM.map.layers, function (index,layer) { if (layer.name == "SurveyLocationMarkerLayer") { layer.destroy(); } }); }, /* * ### identBox * Calculate a reasonable bounding box for identification purposes * So it should depend on the current resolution. * @param e {object} the jquery mouse event * @param win {object} The dom object in which the mouse was clicked * @return {string} A GML extent string */ identBox: function (e,win) { var res = this.map.getResolution(), // Grab the current resolution position = $(win).offset(), // position of map on the page x = e.pageX - position.left, // x relative map y = e.pageY - position.top, // y relative map coord = this.map.getLonLatFromPixel({x:x,y:y}), // Use openlayers to get coordinate albersLatLon= coord.transform(mM.map.getProjectionObject(),new OpenLayers.Projection("EPSG:3005")), // Calculate the bounding box lon1 = albersLatLon.lon - res, lon2 = albersLatLon.lon + res, lat1 = albersLatLon.lat - res, lat2 = albersLatLon.lat + res; // Return GML string return ""+lon1+" "+lat1+""+lon2+" "+lat2+""; }, /* * ### identPoint * Calculate a mouse click position in web mercator * * Accept the current click event and window * The returned point is in web mercator. * @param e {object} jQuery mouse click event * @param position {object} A position object which represents * represents the position of the clicked div on the screen. * looks like this {top: 60, left: 409.796875} * Typical of the jQuery.offset() method. * @return {object} Openlayers point object */ identPoint: function (e,position) { var x = e.pageX - position.left, // x relative map y = e.pageY - position.top; // y relative map return this.map.getLonLatFromPixel({x:x,y:y}); // return point }, /* * ### identBoxJSON * Calculate a bounding box from a single point. * * @param e {object} jQuery mouse click event * @param position {object} A position object which represents * the position of the clicked div on the screen. * looks like this {top: 60, left: 409.796875} * Typical of the jQuery.offset() method. * @param identWindow {integer} The factor to scale size of the returned box. * @return {object} a lat and long string like this: * lon1+","+lat1+","+lon2+","+lat2; */ identBoxJSON: function (e,position,identWindow) { var factor = (identWindow) ? identWindow : 3; var res = (this.map.getResolution()*factor), // Grab the current resolution - multiply by 3 so click precision does not have to be exact. x = e.pageX - position.left, // x relative map y = e.pageY - position.top, // y relative map coord = this.map.getLonLatFromPixel({x:x,y:y}), // Use openlayers to get coordinate albersLatLon = coord.transform(mM.map.getProjectionObject(),new OpenLayers.Projection("EPSG:3005")), lon1 = albersLatLon.lon - res, // Calculate the bounding box lon2 = albersLatLon.lon + res, lat1 = albersLatLon.lat - res, lat2 = albersLatLon.lat + res; // Return JSON string return lon1+","+lat1+","+lon2+","+lat2; }, /* * ### ClosestFeature * Calculate the closest feature from a single point. * This is kinda a crux to get around a bug in Geoserver * That returns numerous features even on just one was * requested. * It also returns stuff that is nowhere near the click sometimes * So check if the closest feature is reasonable by measuring the * distance to resolution ratio. * * @param identPoint {object} Single point feature that can * be collected from a jQuery click event. It should be formated * like this **[lat,lon]** * @param features {object} A feature collection object which * is what gets returned from Geoserver after an identification. * This object can contain points, lines or polygons. * @return {object} A single feature object calculated to be the * closest. */ closestFeature: function (identPoint,features) { var closest, distance = 1000000000; // Misc large number $.each(features, function (i,d) { // Cycle through features var center = d.geometry.getBounds().getCenterLonLat(); // Get the center of the geometry var dist = Math.max(center.lon, identPoint.lon) - // Get distance to the click point Math.min(center.lon, identPoint.lon) + Math.max(center.lat, identPoint.lat) - Math.min(center.lat, identPoint.lat); if (dist < distance) { // If we're closer... Update the values distance = dist; closest = i; } }); var scale = mM.map.getScale(); var frac = distance / mM.map.getResolution(); return (frac < 5 && scale < 30000) ? features[closest] : null; // Only return if reasonable close }, /* * ### xmlToSting * @deprecated * Geoserver has this bug that is returning an XML string with * some null schema names. Some browsers can handle it... but other * can't. So we try and fix it with this. * This was useful in HED. * @param xml {object} The xml object that was returned from Geoserver. */ xmlToString: function (xmlObject) { var xmlData = $(xmlObject).children()[0]; // Use jQuery to grab the correct object var xmlString; if (window.ActiveXObject){ //IE ****Untested**** xmlString = xmlData.xml; } else{ // code for Mozilla, Chrome, Safari, etc. xmlString = (new XMLSerializer()).serializeToString(xmlData); } return xmlString; }, /* * ### sortFeatures * Sorts the passed features array in ascending or descending * order based on the specified attribute name. * @param features {array} An array of feature objects containing an attributes property * @param attribute_name {string} The name of the attribute to be sorted on * @param descending {boolean} Sorting direction * @param cast_to_date {boolean} Optional - Used to trigger the casting of a string date to real date * @return {array} A sorted array of feature objects */ sortFeatures: function (features, attribute_name, descending, cast_to_date) { features.sort(function(a, b) { var a_attrib = a.attributes[attribute_name]; var b_attrib = b.attributes[attribute_name]; if (cast_to_date) { a_attrib = Date.parse(a_attrib); b_attrib = Date.parse(b_attrib); } if (a_attrib < b_attrib) { if (descending) { return 1; } else { return -1; } } else if (a_attrib > b_attrib) { if (descending) { return -1; } else { return 1; } } else { return 0; } }); return features; }, /* * ### addCommas * A little helper function for adding commas to a number * every three digits. * @param str {number} An unformated number * @return {string} A comma formated string */ addCommas: function (str) { var arr,num,dec; str += ''; arr = str.split('.'); num = arr[0] + ''; dec = arr.length>1?'.'+arr[1]:''; return num.replace(/(\d)(?=(\d{3})+$)/g,"$1,") + dec; }, /* * ### shimForEach * The *forEach* array method is missing in some older browsers like IE8. * Typically jQuery is utilized to fill this gap. However some third party * libraries, like [JSTS](https://github.com/bjornharrtell/jsts), make the * assumption it is in the prototype. So offer it as a shim here. * * If this function is called a *forEach* method gets inserted in the * Array prototype **only** if it doesn't already exist. * * Code adapted from [ie.shims.js](https://gist.github.com/dhm116/1790197). */ shimForEach: function () { if (!('forEach' in Array.prototype)) { Array.prototype.forEach= function(action, that /*opt*/) { for (var i= 0, n= this.length; i>> 0; if (typeof fun !== 'function') { throw new TypeError(); } var res = []; var thisArg = arguments.length >= 2 ? arguments[1] : undefined; for (var i = 0; i < len; i++) { if (i in t) { var val = t[i]; if (fun.call(thisArg, val, i, t)) { res.push(val); } } } return res; }; } }, /* * ### getUrlVars * Handy dandy function for grabbing the url parameters * @return {object} Key value pair object for parameters */ getUrlVars: function () { var vars = [], hash; var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); for(var i = 0; i < hashes.length; i++) { hash = hashes[i].split('='); vars.push(hash[0]); vars[hash[0]] = hash[1]; } return vars; }, /* * ### calcGroupHeights * Cycle through all the layer groups and open them. * This forces the height calculations which is important * for accurate transitions when opening the groups. */ calcGroupHeights: function () { // Hide layer stuff until it's ready. $("#desktop-lyr-btn a").css({color: "#dcdcdc"}); $("#layer-drawer").css({visibility: "hidden"}); var count = $(".layer-box").length; $(".layer-box").each(function (i) { var e = this; $(e).collapse("show"); // Expand the group setTimeout(function () { // Wait half a sec then close $(e).collapse("hide"); // Hide again if (count === i + 1) { // If it's the last one. $("#desktop-lyr-btn a").css({color: ""}); // show layer button normally $("#layer-drawer").css({visibility: ""}); // Show the layer drawer } },500); }); } };