/*
* # 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 = "
"; // 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);
});
}
};