diff --git a/assets/app/MaxMap.choropleth.js b/assets/app/MaxMap.choropleth.js new file mode 100644 index 0000000..5d1dba4 --- /dev/null +++ b/assets/app/MaxMap.choropleth.js @@ -0,0 +1,236 @@ +var MaxMapChoroplethHelper = (function() { + var queryParams, providers, base_layers, map, data_obj, map_params; + + var initSharedVars = function() { //convenience function + queryParams = MaxMap.shared.queryParams; + providers = MaxMap.providers; + base_layers = MaxMap.shared.base_layers; + map = MaxMap.shared.map; + data_obj = MaxMap.shared.data_obj; + map_params = MaxMap.shared.map_params; + } + + /******************************** + * Choropleth display helper functions + */ + var getStyledChoroplethLabel = function(dataset, where) { + var styledLabel = $(""); + styledLabel.css("font-weight", "bold"); + styledLabel.text(dataset.label); + if (dataset && dataset.hasOwnProperty("colors")) { + var gradientBox = getChoroplethGradientBox(40, "px", 14, "px", dataset.colors, where); + return gradientBox + styledLabel.prop("outerHTML"); + } else { + //console.log("Couldn't create gradient box for dataset "+dataset.slug+" -- no colors provided"); + return styledLabel.prop("outerHTML"); + } + } + + function getChoroplethGradientBox(width, width_measure, height, height_measure, colors, where, orientation) { + var colorBoxDiv = (where === undefined) ? "
" : "
"; + var aDiv; + var gradientBox = $(colorBoxDiv); + gradientBox.width(width.toString()+width_measure) + .height(height.toString()+height_measure) + .css("border-width","0px") + .css("padding","0") + .css("line-height", "0") + .css("vertical-align","middle"); + colors.forEach(function (col) { + aDiv = $("
") + .css("border-width", "0px") + .css("margin", "0") + .css("padding", "0") + .css("vertical-align","middle"); + if (orientation && orientation === "vertical") { + aDiv.width("100%") + .height((100 / colors.length).toString()+"%"); + } else { + aDiv.height("100%") + .width((100 / colors.length).toString()+"%") + .css("display", "inline-block"); + } + if (queryParams.report) { + aDiv.css("box-shadow","inset 0 0 0 1000px "+col); + } else { + aDiv.css("background-color", col); + } + if (orientation && orientation === "vertical") { + gradientBox.append(aDiv); + } else { + gradientBox.prepend(aDiv); + } + }); + return gradientBox.prop("outerHTML"); + } + var getBaselineChoropleths = function() { + return map.baselineChoropleths; + } + + var getChoropleths = function() { + return Object.keys(data_obj).map(function(dataset) { + if (data_obj.hasOwnProperty(dataset) + && data_obj[dataset].type === "choropleth") { + return data_obj[dataset].layer_data; + } + }); + } + var getChoroplethVariableLabel = function(dataset, props) { + var varlabels = dataset.variable_label; + if (varlabels instanceof Array) { + return varlabels.map(function (k) { + return props[k]; + }).join(", "); + } + return props[varlabels]; + } + + var getChoroplethVariable = function(dataset, props) { + return props[dataset.variable]; + } + + var setChoroplethDisplayFunctions = function(dataset, cd) { + cd.reset = function() { + var hoverMessage = this.dataset.hover_instructions || map_params.default_hover_instructions; + this.element.html(""+hoverMessage+""); + }; + if (dataset.hasOwnProperty("colors")) { + var cols = dataset.colors; + for (var i = 0; i < cols.length; i++) { + var col = cols[i]; + cd[col] = '
'+ + getChoroplethGradientBox(8, "px", 100, "%", cols, "info-gradient", "vertical")+ + '
'; + } + } + cd.update = function (e) { + if (e && e.hasOwnProperty("target") && e.target.hasOwnProperty("feature") && e.target.feature.hasOwnProperty("properties")) { + var props = e.target.feature.properties; + if (this.dataset.category === "baseline") { + this.element.html("
" + + this[MaxMap.providers.display.getColor(this.dataset, Math.round(this.variable(props)))] + + '
' + this.variable_label(props) + + "

" + + Math.round(this.variable(props)) + "% " + + this.dataset.label+"

"); + } + if (this.dataset.category === "summary") { + this.element.html("
" + + this[MaxMap.providers.display.getColor(this.dataset, Math.round(this.variable(props)))] + + '
' + this.variable_label(props) + + "

" + + Math.round(this.variable(props)) + " initiatives

"); + } + } else { + this.reset() + } + }; + return cd; + } + + var createChoroplethDisplay = function(dataset) { + var cd = { + "dataset": dataset, + variable: function (p) { + return getChoroplethVariable(dataset, p); + }, + variable_label: function (p) { + return getChoroplethVariableLabel(dataset, p); + }, + "element": $('
').addClass('choropleth-display'), + currentShape: null, + outerHTML: function() { return this.element.prop('outerHTML'); } + }; + cd = setChoroplethDisplayFunctions(dataset, cd); + return cd; + } + + var styleChoroplethRegion = function(dataset, region) { + var layerProps = region.feature.properties; + var variable = parseInt(getChoroplethVariable(dataset, layerProps), 10); + var theColor = MaxMap.providers.display.getColor(dataset, variable); + region.setStyle({ "color": theColor }); + } + + var addChoroplethRegionEventHandlers = function(region) { + region.on("mouseover", function(e) { + var targets = {}; + MaxMap.providers.layers.getSummaryOverlays().map( function(summary) { + var poly = MaxMap.providers.polygon.getLocationsForPointInDataset(e.latlng, data_obj[summary]); + if (poly.length) { targets[summary] = poly[0]; } + }); + getBaselineChoropleths().map( function(choro) { + var poly = MaxMap.providers.polygon.getLocationsForPointInDataset(e.latlng, data_obj[choro]); + if (poly.length) { targets[choro] = poly[0]; } + }); + for (var overlay in targets) { + if (targets.hasOwnProperty(overlay)) { + if (targets[overlay]) { + e.target = targets[overlay]; + data_obj[overlay].choroplethLegend.update(e); + } else { + data_obj[overlay].choroplethLegend.update(); + } + } + } + }); + region.on("mouseout", function(e) { + MaxMap.providers.layers.getSummaryOverlays().forEach(function(overlay) { + data_obj[overlay].choroplethLegend.update(); + }); + getBaselineChoropleths().forEach(function(overlay) { + data_obj[overlay].choroplethLegend.update(); + }); + }); + region.on("click", MaxMap.providers.display.displayPopup); + } + + var choroplethUpdateCallback = function (e) { + var legendString = "Legend: "+this.dataset.label+""; + var colors = this.dataset.colors; + var thresholds = this.dataset.thresholds; + legendString += '
'; + for (var i = 0; i < thresholds.length; i++) { + if (i == 0) { + legendString += '
' + +'
" + +"> "+(thresholds[i]+1)+"
"; + } else { + legendString += '
' + +'
" + +(thresholds[i]+1)+" - "+(thresholds[i-1])+"
"; + } + } + legendString += '
' + +'
" + +"< "+(thresholds[thresholds.length-1])+"
"; + legendString += "
"; + this.display.update(e); + legendString += this.display.outerHTML(); + if (this.dataset.hasOwnProperty("legend_credits") && this.dataset.legend_credits) { + var creditString = this.dataset.legend_credits; + legendString += '
'+creditString + +' (more)
'; + } + $(this._div).html(legendString); + }; + + + return { + initSharedVars: initSharedVars, + getStyledChoroplethLabel: getStyledChoroplethLabel, + getChoropleths: getChoropleths, + getBaselineChoropleths: getBaselineChoropleths, + styleChoroplethRegion: styleChoroplethRegion, + addChoroplethRegionEventHandlers: addChoroplethRegionEventHandlers, + getChoroplethVariableLabel: getChoroplethVariableLabel, + getChoroplethVariable: getChoroplethVariable, + createChoroplethDisplay: createChoroplethDisplay, + choroplethUpdateCallback: choroplethUpdateCallback, + }; +})(); diff --git a/assets/app/MaxMap.config.js b/assets/app/MaxMap.config.js new file mode 100644 index 0000000..ca338c5 --- /dev/null +++ b/assets/app/MaxMap.config.js @@ -0,0 +1,35 @@ +var MaxMap = (function() { + var providers = {}; + var shared= { + //map, data_obj, map_params, numDatasets, layerOrdering, choropleths, queryParams, base_layers + overlayCount: 0, + }; + + var getProviders = function() { + return providers; + } + + var init = function () { + this.providers = { + choropleth: MaxMapChoroplethHelper, + data: MaxMapDataProvider, + display: MaxMapDisplayHelper, + driver: MaxMapDriver, + layers: MaxMapLayerHelper, + map: MaxMapLeaflet, + polygon: MaxMapPolygonHelper, + query: MaxMapQueryParser, + marker: MaxMapMarkers + }; + this.providers.driver.init(); + } + + return { + providers: providers, + shared: shared, + init: init, + } + +})(); + +MaxMap.init(); diff --git a/assets/app/MaxMap.data.js b/assets/app/MaxMap.data.js new file mode 100644 index 0000000..ae006e8 --- /dev/null +++ b/assets/app/MaxMap.data.js @@ -0,0 +1,325 @@ +var MaxMapDataProvider = (function() { + var queryParams, providers, base_layers, map, data_obj, map_params, layerControl, overlayCount, numDatasets; + var initSharedVars = function() { //convenience function + queryParams = MaxMap.shared.queryParams; + providers = MaxMap.providers; + base_layers = MaxMap.shared.base_layers; + map = MaxMap.shared.map; + data_obj = MaxMap.shared.data_obj; + map_params = MaxMap.shared.map_params; + layerControl = MaxMap.shared.layerControl; + overlayCount = MaxMap.shared.overlayCount; + numDatasets = MaxMap.shared.numDatasets; + } + /******************************** + * Data loading functions + */ + + var getMapData = function() { + return $.getJSON('data/datasets.json').promise(); + } + + var appendOverlaysControlDiv = function() { + providers.layers.reorderLayers(); + choropleths = providers.choropleth.getChoropleths(); + if (!queryParams.report) { + // Add check all and uncheck all buttons to overlays selection + var overlaysDiv = $("div.leaflet-control-layers-overlays"); + var baseLayersDiv = $("div.leaflet-control-layers-base"); + var buttonsDiv = $("
").addClass("bulk-select-overlays"); + var selectAllButton = ""; + var unselectAllButton = ""; + buttonsDiv.html(selectAllButton+unselectAllButton); + // Add titles to Layers control + var baseLayersTitle = $("
") + .html("

Base Map Layers

"); + baseLayersDiv.before(baseLayersTitle); + var overlayLayersTitle = $("
") + .html("

Overlay Layers

") + .append(buttonsDiv); + overlaysDiv.before(overlayLayersTitle); + var titleSpan = "

" + + "What's on this map?

" + + "

" + + "More about these initiatives and data sets" + + "

"; + $("form.leaflet-control-layers-list").prepend($(titleSpan)); + } + } + + var processMapData = function(data_format) { + providers.display.setupMapControls(map_params); + MaxMap.shared.layerOrdering = []; + for (k in data_obj) { + if (data_obj.hasOwnProperty(k)) { + data_obj[k].slug = k; + } + } + for (k in data_obj) { + if (data_obj.hasOwnProperty(k) && data_obj[k].hasOwnProperty("layerOrder")) { + MaxMap.shared.layerOrdering[parseInt(data_obj[k]["layerOrder"],10)-1] = k; + } + } + var i; + // Load each program and add it as an overlay layer to control + for (i = 0; i < numDatasets; i++) { + k = MaxMap.shared.layerOrdering[i]; + if (data_obj.hasOwnProperty(k)) { + populate_layer_control(data_obj[k], data_format); + overlayCount++; + } + } + + if (overlayCount === numDatasets) { + appendOverlaysControlDiv(); + } + if (queryParams.report) { + // Put location into title of report + var t = map_params.hasOwnProperty("titleElement") ? + $(map_params.titleElement) : $("#content h1"); + providers.display.addLocationToReportTitle(t); + } + map.invalidateSize(false); + for (i = 0; i < numDatasets; i++) { + k = MaxMap.shared.layerOrdering[i]; + if (data_obj.hasOwnProperty(k)) { + if (isRequestedDataset(data_obj[k])) { + addLayerToMap(data_obj[k]); + } + } + } + if (!queryParams.report) { + // Add popup actions to layers control layer titles + providers.layers.addPopupActionsToLayersControlLayerTitles(data_obj, map_params); + } + map.invalidateSize(false); + } + + var loadLayerData = function(dataset, add) { + add = (typeof add === "undefined") ? false : add; + if (dataset.hasOwnProperty("data_loaded") && !dataset.data_loaded) { + switch (dataset.data_format) { + case "topojson": + load_topojson_location_data(dataset, add); + break; + case "geojson": + load_geojson_location_data(dataset, add); + break; + default: + break; + } + } + } + + var addLayerToMap = function(dataset) { + if (dataset.data_loaded) { + dataset.layer_data.addTo(map); + } else { + loadLayerData(dataset, true); + } + } + + function populate_layer_control(dataset, data_format) { + var layerGroup = MaxMap.providers.map.getLayerGroup(); + dataset.data_format = data_format; + dataset.data_loaded = false; + providers.display.createColorBoxCSS(dataset); + if (dataset.type === "regions" || dataset.type === "points") { + dataset.layer_data = layerGroup; + if (!queryParams.report) { + layerControl.addOverlay(dataset.layer_data, + providers.display.getStyledInitiativeLabel(dataset, "legend"), + providers.display.getLayerCategoryLabel(dataset.category)); + } + } else if (dataset.type === "choropleth") { + if (dataset.category === "summary") { + map.summaryOverlays.push(dataset.slug); + } + if (dataset.category === "baseline") { + map.baselineChoropleths.push(dataset.slug); + } + providers.map.createChoroplethTools(dataset); + dataset.layer_data = layerGroup; + if (!queryParams.report) { + layerControl.addOverlay(dataset.layer_data, + providers.choropleth.getStyledChoroplethLabel(dataset, "legend"), + providers.display.getLayerCategoryLabel(dataset.category)); + } + } + } + + function create_topojson_layer(dataset) { + var newLayer = MaxMap.providers.map.getNewTopoJSONLayer(dataset); + newLayer.on("mouseover", function(e) { + var targets = {}; + MaxMap.providers.layers.getSummaryOverlays().map( function(summary) { + var poly = MaxMap.providers.polygon.getLocationsForPointInDataset(e.latlng, data_obj[summary]); + if (poly.length) { targets[summary] = poly[0]; } + }); + MaxMap.providers.choropleth.getBaselineChoropleths().map( function(choro) { + var poly = MaxMap.providers.polygon.getLocationsForPointInDataset(e.latlng, data_obj[choro]); + if (poly.length) { targets[choro] = poly[0]; } + }); + for (var overlay in targets) { + if (targets.hasOwnProperty(overlay)) { + if (targets[overlay]) { + e.target = targets[overlay]; + data_obj[overlay].choroplethLegend.update(e); + } else { + data_obj[overlay].choroplethLegend.update(); + } + } + } + }); + newLayer.on("mouseout", function(e) { + MaxMap.providers.layers.getSummaryOverlays().forEach(function(overlay) { + data_obj[overlay].choroplethLegend.update(); + }); + MaxMap.providers.choropleth.getBaselineChoropleths().forEach(function(overlay) { + data_obj[overlay].choroplethLegend.update(); + }); + }); + newLayer.on("click", providers.display.displayPopup); + return newLayer; + } + + function load_topojson_location_data (dataset, add) { + if (dataset.hasOwnProperty("data_loaded") && !dataset.data_loaded) { + map.spin(true); + var layer; + $.getJSON(dataset.topojson, function(data) { + var newLayer = create_topojson_layer(dataset); + newLayer.addData(data); + newLayer.setStyle(dataset.style); + if (dataset.type === "choropleth") { + for (layer in newLayer._layers) { + if (newLayer._layers.hasOwnProperty(layer)) { + var theLayer = newLayer._layers[layer]; + MaxMap.providers.choropleth.styleChoroplethRegion(dataset, theLayer); + MaxMap.providers.choropleth.addChoroplethRegionEventHandlers(theLayer); + } + } + } + dataset.layer_data.addLayer(newLayer); + dataset.data_loaded = true; + if (add) { dataset.layer_data.addTo(map); } + if (queryParams.report) { + // Populate initiatives report + var container = $("div#initiatives"); + var reportString = populateInitiativesReport(); + container.html(reportString); + } + map.spin(false); + }, function(e) { map.spin(false); console.log(e); }); + } + } + + /******************************** + * Data loading functions: IE8 Support + */ + function load_geojson_location_data (dataset, add) { + if (dataset.hasOwnProperty("data_loaded") && !dataset.data_loaded) { + map.spin(true); + $.getJSON(dataset.geojson, function(data) { + var newLayer; + data.features.forEach(function(feature) { + newLayer = MaxMap.providers.map.newGeoJSONLayer(feature); + newLayer.setStyle(data_obj[dataset]["style"]); + if (dataset.type === "choropleth") { + MaxMap.providers.choropleth.styleChoroplethRegion(dataset, theLayer); + MaxMap.providers.choropleth.addChoroplethRegionEventHandlers(theLayer); + } + dataset.layer_data.addLayer(newLayer); + }); + dataset.data_loaded = true; + if (add) { dataset.layer_data.addTo(map); } + if (queryParams.report) { + // Populate initiatives report + var container = $("div#initiatives"); + var reportString = populateInitiativesReport(); + container.html(reportString); + } + map.spin(false); + }, function(e) { map.spin(false); console.log(e); }); + } + } + + /******************************** + * Dataset information helpers + */ + function isRequestedDataset(dataset) { + if (typeof queryParams.datasets == 'undefined') { + if (dataset && dataset.hasOwnProperty("displayed")) { + return dataset.displayed; + } else { + return true; + } + } + if (typeof queryParams.datasets == "string") { + return dataset.slug === queryParams.datasets; + } + return ($.inArray(dataset.slug, queryParams.datasets) !== -1); + } + + + + /******************************** + * Geocoding helper functions + */ + var fixBadAddressData = function(address) { + var city = "", county = "", state = ""; + if (address.hasOwnProperty("city")) { city = address.city.trim(); } + if (address.hasOwnProperty("county")) { county = address.county.trim(); } + if (address.hasOwnProperty("state")) { state = address.state.trim(); } + // Fix bad city and state data coming back from geocoding server + if (state && (state === "penna")) { state = "Pennsylvania"; } + if (city && (city === "NYC") && state && (state === "New York")) { city = "New York"; } + if (city && (city === "LA") && state && (state === "California")) { city = "Los Angeles"; } + if (city && (city === "SF") && state && (state === "California")) { city = "San Francisco"; } + if (city && (city === "ABQ") && state && (state === "New Mexico")) { city = "Albuquerque"; } + if (city && (city === "PGH") && state && (state === "Pennsylvania")) { city = "Pittsburgh"; } + // Display "City of ..." where appropriate + if (city && ((city.slice(-4).toLowerCase() !== "city") + || (city.slice(-8).toLowerCase() !== "township"))) { city = "City of " + city; } + if (city && (city.slice(-10).toLowerCase() === " (city of)")) { + city = city.slice(0, city.length - 10); } + return {city: city, county: county, state: state}; + } + + var getReverseGeolocationPromise = function(latlng) { + var serviceRequestUrl = map_params.reverse_geocode_service_url+"&lat="+latlng.lat+ + "&lon="+latlng.lng+"&zoom=12&addressdetails=1"; + return $.getJSON(serviceRequestUrl); + } + + + var getAboutDataPath =function (map_params) { + if (queryParams && queryParams.hasOwnProperty("about_data_url")) { + return queryParams.about_data_url; + } + if (map_params.hasOwnProperty("about_data_url") && map_params.about_data_url) { + return map_params.about_data_url; + } + if (queryParams.hasOwnProperty("hostname") && queryParams.hasOwnProperty("rootpath") && queryParams.hasOwnProperty("subpath")) { + var hn = queryParams.hostname; + var rp = queryParams.rootpath; + var sp = queryParams.subpath; + return "//"+hn+rp+'datasets'+(sp.slice(-5) === '.html' ? '.html' : ''); + } + return "datasets.html"; + } + + return { + getAboutDataPath: getAboutDataPath, + getMapData: getMapData, + processMapData: processMapData, + initSharedVars: initSharedVars, + loadLayerData: loadLayerData, + addLayerToMap: addLayerToMap, + getReverseGeolocationPromise: getReverseGeolocationPromise, + fixBadAddressData: fixBadAddressData, + } + +})(); diff --git a/assets/app/MaxMap.display.js b/assets/app/MaxMap.display.js new file mode 100644 index 0000000..df5ebc6 --- /dev/null +++ b/assets/app/MaxMap.display.js @@ -0,0 +1,481 @@ +var MaxMapDisplayHelper = (function() { + var queryParams, providers, base_layers, map, data_obj, map_params, numDatasets; + + var initSharedVars = function() { //convenience function + queryParams = MaxMap.shared.queryParams; + providers = MaxMap.providers; + base_layers = MaxMap.shared.base_layers; + map = MaxMap.shared.map; + data_obj = MaxMap.shared.data_obj; + map_params = MaxMap.shared.map_params; + numDatasets = MaxMap.shared.numDatasets; + } + + function hexToRgb(hex) { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, function(m, r, g, b) { + return r + r + g + g + b + b; + }); + + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; + } + + var getColor = function(dataset, d) { + if (dataset.hasOwnProperty("colors") && dataset.hasOwnProperty("thresholds")) { + var col = dataset.colors; + var thr = dataset.thresholds; + var cmap = {}; + for (var i = 0; i < col.length; i++) { + if (i < thr.length) { + cmap[thr[i].toString()] = col[i]; + } else { + cmap["default"] = col[i]; + } + } + var sorted_thr = thr.sort( function(a,b) { return b-a; } ); + for (var t = 0; t < sorted_thr.length; t++) { + if (d > sorted_thr[t]) { + var k = sorted_thr[t].toString(); + if (cmap.hasOwnProperty(k)) { return cmap[k]; } + } + } + return cmap["default"]; + } + return null; + } + + /******************************** + * Regions/points display helper functions + */ + var createColorBoxCSS = function(dataset) { + if (dataset.hasOwnProperty("style") && dataset.style.hasOwnProperty("color")) { + var rgb_color = hexToRgb(dataset.style.color); + var cssString = ".colorbox-" + dataset.slug; + var fillOpacity = dataset.style.hasOwnProperty("fillOpacity") + ? dataset.style.fillOpacity : 0.6; + var opacity = dataset.style.hasOwnProperty("opacity") + ? dataset.style.opacity : 0.6; + if (queryParams.report) { + cssString += " { box-shadow: inset 0 0 0 1000px rgba(" + + rgb_color.r + "," + rgb_color.g + "," + rgb_color.b + "," + + fillOpacity + "); "; + } else { + cssString += " { background-color: rgba(" + + rgb_color.r + "," + rgb_color.g + "," + rgb_color.b + "," + + fillOpacity + "); "; + } + cssString += "border-color: rgba(" + + rgb_color.r + "," + rgb_color.g + "," + rgb_color.b + "," + + opacity + "); }"; + $("style#colorboxes").append(cssString); + } + } + + function getColorBoxDiv(dataset, where) { + return "
"; + } + + /******************************** + * Initiative display functions + */ + var getStyledInitiativeLabel = function(dataset, where, linked) { + linked = typeof linked !== 'undefined' ? linked : false; // default to no link + var colorBoxDiv = getColorBoxDiv(dataset, where); + var styledLabel = $(""); + styledLabel.css("font-weight", "bold"); + if (linked && dataset.hasOwnProperty("initiativeURL") + && dataset.initiativeURL.length > 0) { + var linkString = '' + + dataset.label + ''; + styledLabel.html(linkString); + } else { + styledLabel.text(dataset.label); + } + return colorBoxDiv + styledLabel.prop("outerHTML"); + } + + function getInitiativeSegment(dataset, polygons, where) { + var popupString = "
" + (where == "report" ? "

" : "") + + getStyledInitiativeLabel(dataset, where, true) + (where == "report" ? + '

' : ""); + for (var poly in polygons) { + if (polygons.hasOwnProperty(poly)) { + var disp = polygons[poly].feature.properties.LocationDisplay; + var city = polygons[poly].feature.properties.hasOwnProperty("city") ? + polygons[poly].feature.properties.city : ""; + var county = polygons[poly].feature.properties.hasOwnProperty("county") ? + polygons[poly].feature.properties.county : ""; + var state = polygons[poly].feature.properties.hasOwnProperty("state") ? + polygons[poly].feature.properties.state : ""; + var cityCountyState = (city.length > 0 ? city + ", " : "") + + (county.length > 0 ? county + ", " : "") + state; + var cityState = (city.length > 0 ? city + ", " : "") + state; + var countyState = (county.length > 0 ? county + ", " : "") + state; + if (disp == cityCountyState || disp == cityState || disp == countyState) { + popupString += "

" + disp + "

"; + } else { + popupString += "

" + disp + + (cityCountyState.length > 0 ? " (" + cityCountyState + ")" : "") + + "

"; + } + } + } + popupString += (where == "report" ? '
' : "") + "
"; + return popupString; + } + + function getInitiativePopupSegment(dataset, polygons) { + return getInitiativeSegment(dataset, polygons, "popup") + } + + function getInitiativeReportSegment(dataset, polygons) { + return getInitiativeSegment(dataset, polygons, "report") + } + + /******************************** + * Summary display functions + */ + function getSummarySegment(dataset, polygons, numPolygons, where) { + var polysAndCounts = MaxMap.providers.polygon.sortPolygonsByState(polygons); + var polys = polysAndCounts.sortedPolygons; + //console.log(polysAndCounts.countsByState); + var popupString = "
" + + (where == "report" ? "

" : "") + + MaxMap.providers.choropleth.getStyledChoroplethLabel(dataset, where) + + (where == "report" ? '

' : ""); + for (var poly in polys) { + if (polys.hasOwnProperty(poly)) { + popupString += "

" + (where !== "report" ? numPolygons : + polysAndCounts.countsByState[getState(polys[poly])]) + " of " + + Math.round(MaxMap.providers.choropleth.getChoroplethVariable(dataset, polygons[poly].feature.properties)) + + " programs in " + + MaxMap.providers.choropleth.getChoroplethVariableLabel(dataset, polygons[poly].feature.properties) + "

"; + } + } + popupString += (where == "report" ? '
' : ""); + popupString += "
"; + return popupString; + } + + function getSummaryPopupSegment(dataset, polygons, numPolygons) { + return getSummarySegment(dataset, polygons, numPolygons, "popup"); + } + + function getSummaryReportSegment(dataset, polygons, numPolygons) { + return getSummarySegment(dataset, polygons, numPolygons, "report"); + } + + /******************************** + * Baseline display functions + */ + function getBaselineSegment(dataset, polygons, where) { + var popupString = "
"; + var poly; + var polysAndCounts = MaxMap.providers.polygon.sortPolygonsByState(polygons); + var polys = polysAndCounts.sortedPolygons; + if (dataset.type == "regions" || dataset.type === "points") { + popupString += (where == "report" ? "

" : "") + + getStyledInitiativeLabel(dataset, where, true) + + (where == "report" ? '

' : ""); + for (poly in polys) { + if (polys.hasOwnProperty(poly)) { + popupString += "

"; + var disp = polys[poly].feature.properties.LocationDisplay; + var city = polys[poly].feature.properties.hasOwnProperty("city") ? + polys[poly].feature.properties.city : ""; + var county = polys[poly].feature.properties.hasOwnProperty("county") ? + polys[poly].feature.properties.county : ""; + var state = polys[poly].feature.properties.hasOwnProperty("state") ? + polys[poly].feature.properties.state : ""; + var cityCountyState = (city.length > 0 ? city + ", " : "") + + (county.length > 0 ? county + ", " : "") + state; + var cityState = (city.length > 0 ? city + ", " : "") + state; + var countyState = (county.length > 0 ? county + ", " : "") + state; + if (disp == cityCountyState || disp == cityState || disp == countyState) { + popupString += disp + "

"; + } else { + popupString += disp + + (cityCountyState.length > 0 ? " (" + cityCountyState + ")" : "") + + "

"; + } + } + } + popupString += (where == "report" ? '
' : ""); + } else if (dataset.type = "choropleth") { + popupString += (where == "report" ? "

" : "") + + MaxMap.providers.choropleth.getStyledChoroplethLabel(dataset, where) + + (where == "report" ? '

' : ""); + for (poly in polys) { + if (polys.hasOwnProperty(poly)) { + popupString += "

" + + Math.round(MaxMap.providers.choropleth.getChoroplethVariable(dataset, polys[poly].feature.properties)) + + "% " + + MaxMap.providers.choropleth.getChoroplethVariableLabel(dataset, polys[poly].feature.properties) + + "

"; + } + } + popupString += (where == "report" ? '
' : ""); + } + popupString += "
"; + return popupString; + } + + function getBaselinePopupSegment(dataset, polygons) { + return getBaselineSegment(dataset, polygons, "popup"); + } + + function getBaselineReportSegment(dataset, polygons) { + return getBaselineSegment(dataset, polygons, "report"); + } + + /******************************** + * Report view display functions + */ + var addLocationToReportTitle = function(titleElement) { + var t; + if(titleElement instanceof $) { t = titleElement; } else { t = $(titleElement); } + MaxMap.providers.data.getReverseGeolocationPromise(map.getCenter()).done(function (data) { + var titleString = t.html() + " for " + getPopupLocationString(data) + " and Surrounds"; + t.html(titleString); + }).error(function (err) { + console.log("Reverse geolocation failed. Error:"); + console.log(err); + }); + } + + var populateInitiativesReport = function() { + var datasetsList = queryParams.datasets; + var locations = providers.polygon.getLocationsInBoundsForDatasets(datasetsList); + // var numLocations = countLocationInitiatives(locations); + var datasetKey = ""; + var reportString = ""; + for (var k = 0; k < datasetsList.length; k++) { + datasetKey = datasetsList[k]; + if (data_obj.hasOwnProperty(datasetKey) && locations[datasetKey].length) { + switch (data_obj[datasetKey].category) { + case "summary": + /* + reportString += getSummaryReportSegment(data_obj[datasetKey], + locations[datasetKey], numLocations); + */ + break; + case "baseline": + reportString += getBaselineReportSegment(data_obj[datasetKey], + locations[datasetKey]); + break; + case "initiative": + reportString += getInitiativeReportSegment(data_obj[datasetKey], + locations[datasetKey]); + break; + default: + break; + } + } + } + return reportString; + } + + /******************************** + * Popup display functions + */ + function getPopupSegmentsForLocations(locations) { + var popupString = ""; + var numPolys = providers.polygon.countLocationInitiatives(locations); + for (var i = 0; i < numDatasets; i++) { + var dataset = MaxMap.shared.layerOrdering[i]; + if (locations.hasOwnProperty(dataset) && locations[dataset].length) { + switch (data_obj[dataset].category) { + case "initiative": + popupString += getInitiativePopupSegment(data_obj[dataset], locations[dataset]); + break; + case "summary": + popupString += getSummaryPopupSegment(data_obj[dataset], locations[dataset], numPolys); + break; + case "baseline": + popupString += getBaselinePopupSegment(data_obj[dataset], locations[dataset]); + break; + default: + break; + } + } + } + return popupString; + } + + function getPopupLocationString(data) { + var location_string = ""; + var address, city, county, state; + if (data.hasOwnProperty("address")) { + address = MaxMap.providers.data.fixBadAddressData(data.address); + city = address.city; + county = address.county; + state = address.state; + if (city && state) { + location_string = city + ", " + state; + } else if (county && state) { + location_string = county + ", " + state; + } else if (state) { + location_string = state + " (" + data["lat"] + + ", " + data["lon"] + ")"; + } else { + location_string = "(" + data["lat"] + ", " + data["lon"] + ")"; + } + } else { + location_string = "(" + data["lat"] + ", " + data["lon"] + ")"; + } + return location_string; + } + + var displayPopup = function(e) { + if (!queryParams.report) { + MaxMap.providers.data.getReverseGeolocationPromise(e.latlng).done(function (data) { + $("#popup_location_heading").text(getPopupLocationString(data)); + }).error(function (err) { + console.log("Reverse geolocation failed. Error:"); + console.log(err); + }); + var popup = L.popup().setLatLng(e.latlng); + var locations = providers.polygon.getLocationsForPoint(e.latlng); + var popupString = getPopupSegmentsForLocations(locations); + if (!popupString) { + popupString = "No layers found."; + } + popupString = "

" + popupString; + popup.setContent(popupString).openOn(map); + } + } + /******************************** + * Report view display functions + */ + var setupMapControls = function (p) { + // Set attribution data for base layers + for (var bl in base_layers) { if (base_layers.hasOwnProperty(bl)) { + base_layers[bl].options.attribution += p.attribution_tail; + } } + + // Add a title to the map + if (p.hasOwnProperty("map_title") && p.map_title + && !queryParams.report) { + map.titleControl = L.control({ + position: 'topcenter' + }); + map.titleControl.onAdd = function (map) { + this._div = L.DomUtil.create('div', 'map-title-div'); + var theHeader = '

'+p.map_title+'

'; + $(this._div).html(theHeader); + return this._div; + }; + map.titleControl.addTo(map); + } + + // Create a location search control and add to top right of map (non-report) + if (!queryParams.report) { + new L.Control.GeoSearch({ + provider: new L.GeoSearch.Provider.OpenStreetMap(), + position: 'topcenter', + showMarker: false, + retainZoomLevel: false + }).addTo(map); + } + + // Add the print report button + if (!queryParams.report) { + window.spawnPrintView = function () { + var bounds = map.getBounds(); + var SWlat = bounds.getSouthWest().lat; + var SWlon = bounds.getSouthWest().lng; + var NElat = bounds.getNorthEast().lat; + var NElon = bounds.getNorthEast().lng; + var activeBaseLayer; + for (var bl in base_layers) { + if (base_layers.hasOwnProperty(bl) && map.hasLayer(base_layers[bl]) + && !base_layers[bl].overlay) { + activeBaseLayer = bl; + } + } + var activeOverlays = []; + for (var dataset in data_obj) { + if (data_obj.hasOwnProperty(dataset) + && data_obj[dataset].hasOwnProperty("layer_data") + && map.hasLayer(data_obj[dataset]["layer_data"])) { + activeOverlays.push(dataset); + } + } + var queryString = "report&SWlat=" + SWlat + "&SWlon=" + SWlon + "&NElat=" + + NElat + "&NElon=" + NElon + "&base=" + activeBaseLayer + "&datasets=" + + activeOverlays.join(","); + var base_url = providers.query.getPrintViewPath(p); + var url = encodeURI(base_url + "?" + queryString); + window.open(url, "_blank"); + }; + + map.printButton = L.control({"position": "topright"}); + map.printButton.onAdd = function (map) { + this._div = L.DomUtil.create('div', 'printbutton-div'); + var pb = ''; + $(this._div).html(pb); + return this._div; + }; + map.printButton.zoomTrigger = 9; + map.printButton.onMap = false; + map.printButton.zoomHandler = function(e) { + if (map.getZoom() >= map.printButton.zoomTrigger && !map.printButton.onMap) { + map.printButton.addTo(map); + map.printButton.onMap = true; + } else if (map.getZoom() < map.printButton.zoomTrigger && map.printButton.onMap) { + map.printButton.removeFrom(map); + map.printButton.onMap = false; + } + }; + map.on('zoomend', map.printButton.zoomHandler); + map.printButton.zoomHandler({}); + } + + // Add disclaimer control + var disclaimer = '

' + p.disclaimer_text + + ' Last updated ' + p.last_updated + '.

'; + var disclaimerControl = L.control({position: "bottomright"}); + disclaimerControl.onAdd = function (map) { + this._div = L.DomUtil.create('div', 'leaflet-control-disclaimer'); + $(this._div).html(disclaimer); + return this._div; + }; + disclaimerControl.addTo(map); + + // Add back attribution control + L.control.attribution({position: "bottomright"}).addTo(map); + + // Add base layer to map + queryParams.base = typeof queryParams.base === "undefined" ? + p.default_base_layer : queryParams.base; + base_layers[queryParams.base].addTo(map); + } + + var getLayerCategoryLabel = function(category) { + switch (category) { + case "summary": return "Summaries"; + case "initiative": return "Place-based Initiatives"; + case "baseline": return "Baseline Information"; + default: return "Other Data Layers"; + } + } + + return { + initSharedVars: initSharedVars, + displayPopup:displayPopup, + getLayerCategoryLabel: getLayerCategoryLabel, + createColorBoxCSS: createColorBoxCSS, + getStyledInitiativeLabel: getStyledInitiativeLabel, + setupMapControls: setupMapControls, + getColor: getColor, + populateInitiativesReport: populateInitiativesReport, + addLocationToReportTitle: addLocationToReportTitle + }; +})(); diff --git a/assets/app/MaxMap.driver.js b/assets/app/MaxMap.driver.js new file mode 100644 index 0000000..685fbd5 --- /dev/null +++ b/assets/app/MaxMap.driver.js @@ -0,0 +1,51 @@ +var MaxMapDriver = (function() { + + + var datasetCount = function(data_obj) { + var n = 0; + for (var k in data_obj) { + if (data_obj.hasOwnProperty(k)) n++; + } + return n; + } + + var setData = function(obj) { + var map_params = {}; + for (var k in obj) { + if (obj.hasOwnProperty(k) && k !== 'datasets') { + map_params[k] = obj[k]; + } + } + MaxMap.shared.data_obj = obj.datasets; + MaxMap.shared.numDatasets = datasetCount(obj.datasets); + MaxMap.shared.map_params = map_params; + + } + + var initChildrenSharedVars = function() { + MaxMap.providers.display.initSharedVars(); + MaxMap.providers.layers.initSharedVars(); + MaxMap.providers.map.initSharedVars(); + MaxMap.providers.data.initSharedVars(); + MaxMap.providers.choropleth.initSharedVars(); + MaxMap.providers.polygon.initSharedVars(); + } + + var init = function() { + MaxMap.providers.data.getMapData().done(function(obj) { + setData(obj); + MaxMap.providers.query.init(); //setting our queryParams + MaxMap.shared.map = MaxMap.providers.map.createMap(); //create map + MaxMap.shared.layerControl = MaxMap.providers.map.createLayerControls(); //create map + + initChildrenSharedVars(); //once map is initialized, we can init for all children - all references defined by now + + MaxMap.providers.map.configMap(); + + MaxMap.providers.data.processMapData('topojson'); + }); + } + return { + init: init, + }; +})(); diff --git a/assets/app/MaxMap.layers.js b/assets/app/MaxMap.layers.js new file mode 100644 index 0000000..0f45977 --- /dev/null +++ b/assets/app/MaxMap.layers.js @@ -0,0 +1,142 @@ +var MaxMapLayerHelper = (function() { + var queryParams, providers, base_layers, map, data_obj, map_params, numDatasets; + + var initSharedVars = function() { //convenience function + queryParams = MaxMap.shared.queryParams; + providers = MaxMap.providers; + base_layers = MaxMap.shared.base_layers; + map = MaxMap.shared.map; + data_obj = MaxMap.shared.data_obj; + map_params = MaxMap.shared.map_params; + numDatasets = MaxMap.shared.numDatasets; + } + + +/******************************** + * Layer control tooltip function + */ +var addPopupActionsToLayersControlLayerTitles = function (data_obj, map_params) { + var layerSlugs = {}; + for (var slug in data_obj) { + if (data_obj.hasOwnProperty(slug)) { + layerSlugs[data_obj[slug].label] = slug; + } + } + //console.log(layerSlugs); + $(".leaflet-control-layers-group>label").each(function(n, el) { + var layerTitle = $(el).find("span>span")[0].innerHTML; + var slug = layerSlugs[layerTitle]; + //console.log(layerTitle+" --> "+slug); + var labelElementId = slug+"-layer-control-label"; + //console.log($(el).attr("id")); + if (typeof $(el).attr("id") === 'undefined' || $(el).attr("id") !== labelElementId) { + //console.log("No label id: creating tooltip div"); + $(el).attr("id", labelElementId); + $(el).addClass("leaflet-control-layers-selector-label"); + //console.log(el); + $(el).append(createDescriptionTooltip(data_obj[slug], map_params)); + //console.log(el); + $(el).on("mouseover", function (e) { + var selector = 'div#' + slug + '-description-tooltip.layer-description-tooltip'; + //var layerControlLabelSelector = 'label#' + slug + "-layer-control-label"; + //console.log("Showing: " + selector); + $(selector).show(1, function() { + if (!$(selector).data('height-adjusted')) { + adjustLayerTooltipDisplay($(selector)); + } + }); + //console.log("Label: " + $(layerControlLabelSelector)[0].outerHTML + "\n" + // + " | CSS top: " + $(layerControlLabelSelector).css("top") + // + " | $().offset().top: " + $(layerControlLabelSelector).offset().top + // + " | $().position().top: " + $(layerControlLabelSelector).position().top); + }).on("mouseout", function (e) { + //console.log("Hiding: " + 'div#' + slug + '-description-tooltip.layer-description-tooltip'); + $('div#' + slug + '-description-tooltip.layer-description-tooltip').hide(); + }); + $('div#' + slug + '-description-tooltip.layer-description-tooltip').hide(); + } + //console.log("Final label element:"); + //console.log(el); + }); +} + +function createDescriptionTooltip(dataset, p) { + var el = $('#' + dataset.slug + "-layer-control-label"); + var tooltip = $("
"); + tooltip.attr("id", dataset.slug + "-description-tooltip"); + tooltip.addClass("layer-description-tooltip"); + //console.log(tooltip); + var tooltipContents = '

' + dataset.label + '

' + dataset.description + + '

'; + //console.log(tooltipContents); + tooltip.html(tooltipContents); + //console.log(tooltip); + return tooltip; +} + +function adjustLayerTooltipDisplay(el) { + if (tooltipIsNearTheBottomEdge(el)) { + el.css('top', (-el.height() - parseInt(el.css('top'))) + 'px'); + el.data('height-adjusted', true); + } +} + +function tooltipIsNearTheBottomEdge(el) { + var w_height = $(window).height(); + var el_offset = el.offset(); + if (el_offset.top + el.height() > w_height) { + return true; + } + return false; +} + +/******************************** + * Layer management functions + */ +var addAllLayers = function() { + for (var i = 0; i < numDatasets; i++) { + providers.data.addLayerToMap(data_obj[MaxMap.shared.layerOrdering[i]]); + } +} + +var removeAllLayers = function() { + for (var k in data_obj) { + if (data_obj.hasOwnProperty(k)) { + map.removeLayer(data_obj[k].layer_data); + } + } +} + +var reorderLayers = function() { + for (var i = 0; i < numDatasets; i++) { + var layer = data_obj[MaxMap.shared.layerOrdering[i]].layer_data; + if (map.hasLayer(layer)) { + layer.bringToFront(); + } + } +} + +var setLayerControlHeight = function (e) { + var controlHeight = map.getSize().y-50; + var cssString = ".leaflet-control-layers-expanded { max-height: " + + controlHeight.toString() + "px; }"; + $("style#layercontrol").text(cssString); +} + +var getSummaryOverlays = function() { + return map.summaryOverlays; +} + + return { + setLayerControlHeight: setLayerControlHeight, + addPopupActionsToLayersControlLayerTitles:addPopupActionsToLayersControlLayerTitles, + initSharedVars: initSharedVars, + reorderLayers: reorderLayers, + addAllLayers: addAllLayers, + removeAllLayers: removeAllLayers, + getSummaryOverlays: getSummaryOverlays, + }; + +})(); diff --git a/assets/app/MaxMap.leaflet.js b/assets/app/MaxMap.leaflet.js new file mode 100644 index 0000000..604e300 --- /dev/null +++ b/assets/app/MaxMap.leaflet.js @@ -0,0 +1,220 @@ +var MaxMapLeaflet = (function() { + var queryParams, providers, base_layers, map, data_obj, map_params, numDatasets; + + var initSharedVars = function() { //convenience function + queryParams = MaxMap.shared.queryParams; + providers = MaxMap.providers; + base_layers = MaxMap.shared.base_layers; + map = MaxMap.shared.map; + data_obj = MaxMap.shared.data_obj; + map_params = MaxMap.shared.map_params; + numDatasets = MaxMap.shared.numDatasets; + } + + var createMap = function() { + return L.map('map', { + click: MaxMap.providers.display.displayPopup, + scrollWheelZoom: false, + zoomControl: !MaxMap.shared.queryParams.report, + defaultExtentControl: !MaxMap.shared.queryParams.report, + attributionControl: false, + }); // add attribution control after adding disclaimer control below + } + + var addMoveZoomControls = function() { + new L.Control.ZoomBox().addTo(map); + + // Move zoom controls into a single div container + var zoomControl = $("div.leaflet-control-zoom.leaflet-bar.leaflet-control"); + var defaultExtentControl = $("div.leaflet-control-defaultextent.leaflet-bar.leaflet-control"); + var zoomBoxControl = $("div.leaflet-zoom-box-control.leaflet-bar.leaflet-control"); + var zoomControlContainer = $("
").prop("id", "zoomcontrols"); + zoomControlContainer + .append(defaultExtentControl) + .append(zoomControl) + .append(zoomBoxControl); + $("div.leaflet-top.leaflet-left").prepend(zoomControlContainer); + + new L.Control.Pan({ + position: 'topleft' + }).addTo(map); + + } + + var createLayerControls = function() { + queryParams = MaxMap.shared.queryParams; + providers = MaxMap.providers; + map = MaxMap.shared.map; + base_layers = MaxMap.shared.base_layers; + if (!queryParams.report) { + // Create layers control and add base map to control + var overlay_groups = {}; + overlay_groups[providers.display.getLayerCategoryLabel("summary")] = {}; + overlay_groups[providers.display.getLayerCategoryLabel("initiative")] = {}; + overlay_groups[providers.display.getLayerCategoryLabel("baseline")] = {}; + var layerControl = L.control.groupedLayers( + base_layers, overlay_groups, { exclusiveGroups: [] }); + + + layerControl.addTo(map); + // For accessibility + $("a.leaflet-control-layers-toggle") + .prop("title","What's on this map? (Data layers legend and layer information)") + .append("What's on this map?"); + $(".leaflet-control-layers-toggle").on("mouseover", providers.layers.setLayerControlHeight) + .on("focus", providers.layers.setLayerControlHeight) + .on("touchstart", providers.layers.setLayerControlHeight); + return layerControl; + } + } + + var addMapEventHandlers = function() { + map.on("overlayadd", function(e) { + for (var i = 0; i < numDatasets; i++) { + var dataset = MaxMap.shared.layerOrdering[i]; + if (data_obj.hasOwnProperty(dataset) && e.layer === data_obj[dataset].layer_data) { + if (!data_obj[dataset].data_loaded) { providers.data.loadLayerData(data_obj[dataset]); } + if (!queryParams.report && data_obj[dataset].type === "choropleth") { + data_obj[dataset].choroplethLegend.update(e); + data_obj[dataset].choroplethLegend.addTo(map); + } + } + } + providers.layers.reorderLayers(); + }); + + map.on("overlayremove", function(e) { + for (var dataset in data_obj) { + if (data_obj.hasOwnProperty(dataset) && e.layer === data_obj[dataset].layer_data) { + if (!queryParams.report && data_obj[dataset].type === "choropleth") { + map.removeControl(data_obj[dataset].choroplethLegend); + } + } + } + }); + } + + var addTopCenterLocationForMapControls = function() { + var $controlContainer = map._controlContainer; + var nodes = $controlContainer.childNodes; + var topCenter = false; + + for (var i = 0, len = nodes.length; i < len; i++) { + var klass = nodes[i].className; + if (/leaflet-top/.test(klass) && /leaflet-center/.test(klass)) { + topCenter = true; + break; + } + } + + if (!topCenter) { + var tc = document.createElement('div'); + tc.className += 'leaflet-top leaflet-center'; + $controlContainer.appendChild(tc); + map._controlCorners.topcenter = tc; + } + + if (!queryParams.report) { + // Add popup actions to layers control layer titles + providers.layers.addPopupActionsToLayersControlLayerTitles(data_obj, map_params); + } + + $("a.leaflet-control-layers-toggle").on("mouseover", function(e) { + if (!queryParams.report) { + // Add popup actions to layers control layer titles + providers.layers.addPopupActionsToLayersControlLayerTitles(data_obj, map_params); + } + }); + } + + var configMap = function() { + if (queryParams.hasBoundingBox) { + map.fitBounds([[queryParams.SWlat, queryParams.SWlon], + [queryParams.NElat, queryParams.NElon]]); + } else { + map.setView([queryParams.centerLat, queryParams.centerLon], + queryParams.zoom); + } + + // Create lists of choropleth overlay layers (to be populated as data is loaded) + map.summaryOverlays = []; + map.baselineChoropleths = []; + + // Add map event handlers + addMapEventHandlers(); + + // Add logo, zoom, pan, scale and reset controls to the top left of map + if (!queryParams.report) { + addMoveZoomControls(); + } + + L.control.scale({ position: "topleft" }).addTo(map); + + // Add top center location for map controls + addTopCenterLocationForMapControls(); + + } + + var createChoroplethTools = function(dataset) { + dataset.choroplethLegend = L.control({"position":"bottomleft"}); + dataset.choroplethLegend.dataset = dataset; + dataset.choroplethLegend.variable = function(p) { + return getChoroplethVariable(dataset, p); }; + dataset.choroplethLegend.variable_label = function(p) { + return getChoroplethVariableLabel(dataset, p); }; + dataset.choroplethLegend.onAdd = function (map) { + this._div = L.DomUtil.create('div', 'choropleth-legend choropleth-legend-'+this.dataset.slug); + this.update(); + return this._div; + }; + dataset.choroplethLegend.display = MaxMap.providers.choropleth.createChoroplethDisplay(dataset); + dataset.choroplethLegend.update = MaxMap.providers.choropleth.choroplethUpdateCallback; + + } + + var getLayerGroup = function() { + return L.featureGroup(); + } + + var getNewTopoJSONLayer = function(dataset) { + var newLayer = new L.TopoJSON(); + if (dataset.type === "regions" || dataset.type === "points") { + newLayer.setStyle(dataset.style); + newLayer.options.pointToLayer = function(feature, latlng) { + var icon_name = dataset.icon ? dataset.icon : 'default'; + var smallIcon = MaxMap.providers.marker.getMarker(icon_name, dataset.style.color); + return L.marker(latlng,{icon: smallIcon}); + }; + } + return newLayer; + } + + var newGeoJSONLayer = function(feature) { + return L.geoJson.css(feature); + } + + var setBaseLayers = function() { + var tftransport = L.tileLayer.provider("Thunderforest.Transport"); + var tflandscape = L.tileLayer.provider("Thunderforest.Landscape"); + var osm = L.tileLayer.provider("OpenStreetMap"); + var stamenwc = L.tileLayer.provider("Stamen.Watercolor"); + MaxMap.shared.base_layers = { + "Thunderforest Transport": tftransport, + "Thunderforest Landscape": tflandscape, + "Open Street Map": osm, + "Stamen Watercolor": stamenwc + }; + } + + return { + createLayerControls: createLayerControls, + createMap: createMap, + configMap: configMap, + initSharedVars: initSharedVars, + createChoroplethTools: createChoroplethTools, + getLayerGroup: getLayerGroup, + getNewTopoJSONLayer: getNewTopoJSONLayer, + newGeoJSONLayer: newGeoJSONLayer, + setBaseLayers: setBaseLayers, + } +})(); diff --git a/assets/app/MaxMap.marker.js b/assets/app/MaxMap.marker.js new file mode 100644 index 0000000..82fb3f0 --- /dev/null +++ b/assets/app/MaxMap.marker.js @@ -0,0 +1,51 @@ +var MaxMapMarkers = (function() { + var marker_array = { //private list of icons, not meant for direct access + default: new L.divIcon({ + html: '', + iconSize:[14, 34], + iconAnchor: [7, 26], + className: 'leaflet-default-custom-marker', + }), + color: {}, + }; + + var getMarker = function(name, color) { + if (name == 'default' || !name) { + if (!color) { + return marker_array.default; + } else { + return getReplaceDefaultColor(color, '#4dea51'); + } + } else { + return getCustomMarker(name); + } + } + var getReplaceDefaultColor = function(newcolor, defaultcolor) { + if (!marker_array.color[newcolor]) { //only do this once for each color each load + var ret = marker_array.default.options.html.replace(defaultcolor, newcolor); + marker_array.color[newcolor] = new L.divIcon({ + html: ret, + iconSize: marker_array.default.options.iconSize, + iconAnchor: marker_array.default.options.iconAnchor, + className: marker_array.default.options.className, + }); + } + return marker_array.color[newcolor]; + } + + var getCustomMarker = function(name) { + if (!marker_array[name]) { + marker_array[name] = new L.Icon({ + iconUrl: 'assets/images/icons/' + name + '.svg', + iconAnchor: [10, 40], //assuming our icons are set internally as 20x40 + className: 'leaflet-' + name + '-custom-marker', + }); + } + return marker_array[name]; + } + + return{ + getMarker: getMarker, + } + +})(); diff --git a/assets/app/MaxMap.polygon.js b/assets/app/MaxMap.polygon.js new file mode 100644 index 0000000..deff241 --- /dev/null +++ b/assets/app/MaxMap.polygon.js @@ -0,0 +1,189 @@ +var MaxMapPolygonHelper = (function() { + + var queryParams, providers, base_layers, map, data_obj, map_params; + + var initSharedVars = function() { //convenience function + queryParams = MaxMap.shared.queryParams; + providers = MaxMap.providers; + base_layers = MaxMap.shared.base_layers; + map = MaxMap.shared.map; + data_obj = MaxMap.shared.data_obj; + map_params = MaxMap.shared.map_params; + } + + /******************************** + * Point in polygon functions + */ + var getLocationsForPoint = function(p) { + var locations = {}; + for (var dataset in data_obj) { + if (data_obj.hasOwnProperty(dataset)) { + locations[dataset] = getLocationsForPointInDataset(p, data_obj[dataset]); + } + } + return locations; + } + + var getLocationsForPointInDataset = function(p, dataset) { + var locations = []; + if (dataset) { + locations = []; + var result; + dataset.layer_data.eachLayer( function(l) { + result = leafletPip.pointInLayer(p, l); + Array.prototype.push.apply(locations, result); + result = getMarkersForPointInLayer(p, l); + Array.prototype.push.apply(locations, result); + }); + } + return locations; + } + + function getMarkersForPointInLayer(p, layer, result, depth) { + result = typeof result == 'undefined' ? [] : result; + depth = typeof depth == 'undefined' ? 0 : depth; + if (layer.hasOwnProperty("feature") && layer.feature.geometry.type === "Point") { + var lll = layer._latlng; + if (p == lll) { + // console.log(p, lll); + result.push(layer); + } + } else if (layer instanceof L.LayerGroup) { + layer.eachLayer(function(l) { + result.concat(getMarkersForPointInLayer(p, l, result, depth+1)); + }); + } + return result; + } + + /******************************** + * Layer in map bounds functions + */ + var getLocationsInBoundsForDatasets = function(datasets) { + var polygons = {}; + var dataset; + for (var k = 0; k < datasets.length; k++) { + dataset = datasets[k]; + if (data_obj.hasOwnProperty(dataset)) { + polygons[dataset] = getLocationsInBoundsForDataset(data_obj[dataset]); + } + } + return polygons; + } + + function getLocationsWithinBounds(layer, result, depth, bounds) { + result = typeof result == 'undefined' ? [] : result; + depth = typeof depth == 'undefined' ? 0 : depth; + bounds = typeof bounds == 'undefined' ? map.getBounds() : bounds; + if (layer.hasOwnProperty("feature")) { + var layerBounds; + if (layer.feature.geometry.type === "Point") { + layerBounds = layer._latlng; + if (bounds.contains(layerBounds)) { + result.push(layer); + } + } else { + layerBounds = layer.getBounds(); + if (bounds.contains(layerBounds) || bounds.intersects(layerBounds)) { + result.push(layer); + } + } + } else if (layer instanceof L.LayerGroup) { + layer.eachLayer(function(l) { + result.concat(getLocationsWithinBounds(l, result, depth+1)); + }); + } + return result; + } + + var getLocationsInBoundsForDataset = function (dataset) { + var polygons = []; + if (dataset && dataset.hasOwnProperty("layer_data")) { + polygons = getLocationsWithinBounds(dataset.layer_data); + } + return polygons; + } + + /******************************** + * Polygon counting/sorting functions + */ + var countLocationInitiatives = function(polys, by) { + return Object.keys(polys).map(function (dataset) { + if (polys.hasOwnProperty(dataset) && data_obj[dataset].category === "initiative") { + /* if (by === "state") { + var obj = toObject(map(getState, polys[dataset]), polys[dataset]); + var byState = {}; + Object.keys(obj).forEach(function (state) { + return byState[state] = obj[state].length; + }); + return byState; + } */ + return polys[dataset].length; + } + return 0; + }).reduce(function (prev, curr) { + /* if (by === "state") { + Object.keys(prev).forEach(function (state) { + prev[state] = prev[state] + curr[state]; + }); + return prev; + } */ + return prev + curr; + }); + } + + function getState(polygon) { + var state = ""; + if (polygon.hasOwnProperty("feature") && polygon.feature.hasOwnProperty("properties")) { + var props = polygon.feature.properties; + state = props.hasOwnProperty("state") ? props.state : + (props.hasOwnProperty("LocationDisplay") ? + props.LocationDisplay.substring(props.LocationDisplay.length - 3) : "ZZ"); + } + return state; + } + + var sortPolygonsByState = function(polygons) { + var partitions = {}; + var state; + for (var poly in polygons) { + if (polygons.hasOwnProperty(poly)) { + state = getState(polygons[poly]); + if (!partitions.hasOwnProperty(state)) { partitions[state] = []; } + partitions[state].push(polygons[poly]); + } + } + var counts = Object.keys(partitions).reduce(function(previous, current) { + previous[current] = partitions[current].length; + return previous; + }, {}); + //console.log(partitions); + //console.log(counts); + var states = Object.keys(partitions).sort(); + //console.log(states); + var result = []; + for (var k = 0; k < states.length; k++) { + state = states[k]; + partitions[state] = partitions[state].sort(function(p1, p2) { + var p1l = p1.feature.properties.LocationDisplay; + var p2l = p2.feature.properties.LocationDisplay; + if (p1l > p2l) return 1; + if (p2l > p1l) return -1; + return 0; + }); + result = partitions[state].reduce( function(ary, i) {ary.push(i); return ary;}, result); + } + return { countsByState: counts, sortedPolygons: result }; + } + + return { + initSharedVars: initSharedVars, + getLocationsForPointInDataset: getLocationsForPointInDataset, + getLocationsForPoint: getLocationsForPoint, + countLocationInitiatives: countLocationInitiatives, + sortPolygonsByState: sortPolygonsByState, + getLocationsInBoundsForDatasets: getLocationsInBoundsForDatasets, + getLocationsInBoundsForDataset: getLocationsInBoundsForDataset + } + +})(); diff --git a/assets/app/MaxMap.query.js b/assets/app/MaxMap.query.js new file mode 100644 index 0000000..6e44049 --- /dev/null +++ b/assets/app/MaxMap.query.js @@ -0,0 +1,133 @@ +var MaxMapQueryParser = (function() { + + var defaultParams = { + "zoom": 4, + "centerLat": 44.87144275016589, + "centerLon": -105.16113281249999, + "NElat": 67.57571741708057, + "NElon": -34.27734375, + "SWlat": 7.885147283424331, + "SWlon": -176.044921875, + "report": false, + "hasBoundingBox": false, + "hasCenter": false, + "hasZoom": false + }; + + + var init = function() { + var pn = window.location.pathname; + if (pn.substring(pn.length-5) === "print" || + pn.substring(pn.length-6) === "print/" || + pn.substring(pn.length-10) === "print.html") { + defaultParams.report = true; + } + // Get and parse query parameters + var queryParams = parseQueryParams(defaultParams.report); + // Merge defaults to fill in gaps + for (var prop in defaultParams) { + if (defaultParams.hasOwnProperty(prop)) { + queryParams[prop] = typeof queryParams[prop] == 'undefined' + ? defaultParams[prop] : queryParams[prop]; + } + } + MaxMap.shared.queryParams = queryParams; + // Load base map providers as needed + + MaxMap.providers.map.setBaseLayers(); + } + + function parseQueryParams(defaultGuessIsReport) { + var queryParams = {}; + var query = decodeURIComponent(window.location.search.substring(1)); + if (query[query.length-1] == '/') { + query = query.substring(0, query.length-1); + } + var vars = query.split("&"); + var results, pair, int_result, float_result; + for (var i=0;i> ./start.js -uglifyjs --prefix relative --output ${outpath}main.${now}.min.js --screw-ie8 --mangle --compress dead_code,loops,warnings,join_vars --reserved '$,L,map,window,data_obj' --stats --verbose -- ../assets/jquery-1.11.2/jquery.min.js ../assets/spin-2.1.0/spin.min.js ../assets/leaflet-0.7.3/js/leaflet.js ../assets/leaflet-0.7.3/js/Leaflet.vector-markers.js ../assets/leaflet-0.7.3/js/leaflet-providers.js ../assets/leaflet-0.7.3/js/leaflet-pip.min.js ../assets/leaflet-0.7.3/js/leaflet.spin.js ../assets/leaflet-0.7.3/js/leaflet.geojsoncss.min.js ../assets/leaflet-0.7.3/js/l.control.geosearch.js ../assets/leaflet-0.7.3/js/l.geosearch.provider.openstreetmap.js ../assets/leaflet-0.7.3/js/L.Control.Pan.js ../assets/leaflet-0.7.3/js/L.Control.ZoomBox.min.js ../assets/leaflet-0.7.3/js/leaflet.defaultextent.js ../assets/leaflet-0.7.3/js/leaflet.groupedlayercontrol.min.js ../assets/topojson-1.0/topojson.v1.min.js ../assets/leaflet-0.7.3/js/L.TopoJSON.min.js ../assets/main.js ../assets/CustomMarkers.js ./start.js +uglifyjs --prefix relative --output ${outpath}main.${now}.min.js --screw-ie8 --mangle --compress dead_code,loops,warnings,join_vars --reserved '$,L,map,window,data_obj' --stats --verbose -- ../assets/jquery-1.11.2/jquery.min.js ../assets/spin-2.1.0/spin.min.js ../assets/leaflet-0.7.3/js/leaflet.js ../assets/leaflet-0.7.3/js/Leaflet.vector-markers.js ../assets/leaflet-0.7.3/js/leaflet-providers.js ../assets/leaflet-0.7.3/js/leaflet-pip.min.js ../assets/leaflet-0.7.3/js/leaflet.spin.js ../assets/leaflet-0.7.3/js/leaflet.geojsoncss.min.js ../assets/leaflet-0.7.3/js/l.control.geosearch.js ../assets/leaflet-0.7.3/js/l.geosearch.provider.openstreetmap.js ../assets/leaflet-0.7.3/js/L.Control.Pan.js ../assets/leaflet-0.7.3/js/L.Control.ZoomBox.min.js ../assets/leaflet-0.7.3/js/leaflet.defaultextent.js ../assets/leaflet-0.7.3/js/leaflet.groupedlayercontrol.min.js ../assets/topojson-1.0/topojson.v1.min.js ../assets/leaflet-0.7.3/js/L.TopoJSON.min.js ../assets/app/MaxMap.choropleth.js ../assets/app/MaxMap.polygon.js ../assets/app/MaxMap.data.js ../assets/app/MaxMap.layers.js ../assets/app/MaxMap.display.js ../assets/app/MaxMap.query.js ../assets/app/MaxMap.driver.js ../assets/app/MaxMap.leaflet.js ../assets/app/MaxMap.marker.js ../assets/app/MaxMap.config.js # --source-map ${outpath}main.${now}.min.js.map --source-map-root http://localhost:8000/assets --source-map-url # IE 8 @@ -31,7 +31,7 @@ touch ./start-ie8.js echo "load_map_data(\"geojson\");" >> ./start-ie8.js -uglifyjs --prefix relative --output ${outpath}main.ie8.${now}.min.js --mangle --compress dead_code,loops,warnings,join_vars --reserved '$,L,map,window,data_obj' --stats --verbose -- ../assets/ie8_polyfills.js ../assets/jquery-1.11.2/jquery.min.js ../assets/spin-2.1.0/spin.min.js ../assets/leaflet-0.7.3/js/leaflet.js ../assets/leaflet-0.7.3/js/Leaflet.vector-markers.js ../assets/leaflet-0.7.3/js/leaflet-providers.js ../assets/leaflet-0.7.3/js/leaflet-pip.min.js ../assets/leaflet-0.7.3/js/leaflet.spin.js ../assets/leaflet-0.7.3/js/leaflet.geojsoncss.min.js ../assets/leaflet-0.7.3/js/l.control.geosearch.js ../assets/leaflet-0.7.3/js/l.geosearch.provider.openstreetmap.js ../assets/leaflet-0.7.3/js/L.Control.Pan.js ../assets/leaflet-0.7.3/js/L.Control.ZoomBox.min.js ../assets/leaflet-0.7.3/js/leaflet.defaultextent.js ../assets/leaflet-0.7.3/js/leaflet.groupedlayercontrol.min.js ../assets/main.js ../assets/CustomMarkers.js ./start-ie8.js +uglifyjs --prefix relative --output ${outpath}main.ie8.${now}.min.js --mangle --compress dead_code,loops,warnings,join_vars --reserved '$,L,map,window,data_obj' --stats --verbose -- ../assets/ie8_polyfills.js ../assets/jquery-1.11.2/jquery.min.js ../assets/spin-2.1.0/spin.min.js ../assets/leaflet-0.7.3/js/leaflet.js ../assets/leaflet-0.7.3/js/Leaflet.vector-markers.js ../assets/leaflet-0.7.3/js/leaflet-providers.js ../assets/leaflet-0.7.3/js/leaflet-pip.min.js ../assets/leaflet-0.7.3/js/leaflet.spin.js ../assets/leaflet-0.7.3/js/leaflet.geojsoncss.min.js ../assets/leaflet-0.7.3/js/l.control.geosearch.js ../assets/leaflet-0.7.3/js/l.geosearch.provider.openstreetmap.js ../assets/leaflet-0.7.3/js/L.Control.Pan.js ../assets/leaflet-0.7.3/js/L.Control.ZoomBox.min.js ../assets/leaflet-0.7.3/js/leaflet.defaultextent.js ../assets/leaflet-0.7.3/js/leaflet.groupedlayercontrol.min.js ../assets/app/MaxMap.choropleth.js ../assets/app/MaxMap.polygon.js ../assets/app/MaxMap.data.js ../assets/app/MaxMap.layers.js ../assets/app/MaxMap.display.js ../assets/app/MaxMap.query.js ../assets/app/MaxMap.driver.js ../assets/app/MaxMap.leaflet.js ../assets/app/MaxMap.marker.js ../assets/app/MaxMap.config.js # --source-map ${outpath}main.ie8.${now}.min.js.map --source-map-root http://localhost:8000/assets --source-map-url # Datasets Credits