Skip to content

Commit

Permalink
Merge pull request #29 from phillycommunitywireless/27-add-broadband-…
Browse files Browse the repository at this point in the history
…access-layer

27 add broadband access layer
  • Loading branch information
eugenethreat authored Jan 21, 2025
2 parents eed1126 + 41e2b24 commit c7552a3
Show file tree
Hide file tree
Showing 11 changed files with 75,877 additions and 115 deletions.
75,540 changes: 75,540 additions & 0 deletions data/no-broadband-percent.geojson

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,27 @@
Show Popup
</label>
</div>
<div>
<label class='checkbox-container'>
<input type='checkbox' id="broadband-blocks" />
<div class='checkbox mr6 checkbox--s-label'>
<svg class='icon'>
<use xlink:href='#icon-check' />
</svg>
</div>
Broadband Percentage
</label>

<label class='checkbox-container ml24'>
<input type='checkbox' id="show-broadband-popup" checked />
<div class='checkbox mr6 checkbox--s-label'>
<svg class='icon'>
<use xlink:href='#icon-check' />
</svg>
</div>
Show Popup
</label>
</div>
</div>
<div style="font-weight: bold">Navigate to</div>
<div class="inline-flex flex--column">
Expand Down
15 changes: 15 additions & 0 deletions js/bind-elements.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
setBroadbandLayer,
setIncomeLayer,
setNeighborhoodLayer,
setNeighborhoodOutline,
Expand Down Expand Up @@ -89,6 +90,20 @@ export default () => {
}
});

document
.getElementById('broadband-blocks')
.addEventListener('change', function () {
setBroadbandLayer(this.checked);
});

document
.getElementById('show-broadband-popup')
.addEventListener('change', function () {
if (!this.checked) {
map.fire('close-broadband-popup');
}
});

// navigation bindings
const norrisSq = 'norris_square';
const norrisSqBtn = document.getElementById(norrisSq);
Expand Down
27 changes: 27 additions & 0 deletions js/bind-elements.util.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ const setIncomeVisibility = (visible) => {
visible ? 'visible' : 'none'
);
};
const setBroadbandVisibility = (visible) => {
map.setLayoutProperty(
'no-broadband-layer',
'visibility',
visible ? 'visible' : 'none'
);
};

/**
* internally checks 'outline' checked state. could be passed, but eh.
Expand Down Expand Up @@ -107,3 +114,23 @@ export const setIncomeLayer = (showLayer) => {
map.fire('close-income-popup');
}
};

/**
* @param {boolean} showLayer
*/
export const setBroadbandLayer = (showLayer) => {
setBroadbandVisibility(showLayer);

const showNeighborhoods = document.getElementById(
'neighborhood-boundaries'
).checked;

if (showLayer && showNeighborhoods) {
document.getElementById('neighborhood-outline-only').checked = true;
setNeighborhoodOutline();
}

if (!showLayer) {
map.fire('close-broadband-popup');
}
};
57 changes: 57 additions & 0 deletions js/layers/broadband-access-layer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import popupHandler from '../util/popup-handler.js';
import {
createRangeColorExpression,
fetchJSON,
generateCentroids,
generateLabelFromNeighborhood,
} from '../util/util.js';

const FeatureIdKey = 'spatial_id';
const LayerSource = 'no-broadband-source';
const LayerId = 'no-broadband-layer';

/** NB "no_broad" refers to the percent of households, per block, without access */
export default async () => {
const data_url = '/data/no-broadband-percent.geojson';
const data = await fetchJSON(data_url);

const centroids = generateCentroids(data, FeatureIdKey, 'name');
const colorExpression = createRangeColorExpression(data, 'no_broad', {
invert: true,
});
map.addSource(LayerSource, {
type: 'geojson',
data,
});

map.addLayer({
id: LayerId,
type: 'fill',
source: LayerSource,
layout: {
visibility: 'none',
},
paint: {
'fill-color': colorExpression,
'fill-opacity': 0.25,
},
filter: ['==', '$type', 'Polygon'],
});

const popupHtml = (feature, e) => {
const val = feature.properties.no_broad;
const useVal = val ? val + '%' : 'Unknown';
const useLabel = generateLabelFromNeighborhood(feature, e);
return `<strong>Area:</strong> ${useLabel}<br>
<strong>Households w/o Internet:</strong> ${useVal}`;
};

popupHandler({
centroids,
onCloseEventLabel: 'close-broadband-popup',
popupCheckboxId: 'show-broadband-popup',
popupHtml,
targetFeatureIdKey: FeatureIdKey,
targetLayerLabel: LayerId,
});
};
118 changes: 20 additions & 98 deletions js/layers/income-layer.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,12 @@
import popupHandler from '../util/popup-handler.js';
import {
createRangeColorExpression,
currencyFormatter,
fetchJSON,
generateCentroids,
} from '../util.js';

const getZoneId = (feature) =>
feature.properties.name.match(/^([A-Z0-9]+),\s/)?.[1];
const currencyFormatter = Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
trailingZeroDisplay: 'stripIfInteger',
});

/**
* Generates a label for the income block (feature). Leverages (event) to get
* cursor LatLng to cross-reference against the neighborhood-source
*
* Falls-back to income-source geojson census ID if a neighborhood label can't be found
*
* @param {FeatureData} feature
* @param {MapMouseEvent} event
* @returns {string}
*/
const generateIncomeLabel = (feature, event) => {
const zoneId = feature.properties.name.match(/^([A-Z0-9]+),\s/)?.[1];
if (!zoneId) {
console.warn("couldn't match expression for zoneId");
}
const point = turf.point(event.lngLat.toArray());
const neighborhood = map
.getSource('neighborhood-source')
._data.features.find((feature) =>
turf.booleanPointInPolygon(point, feature)
);
return neighborhood?.properties.name || zoneId;
};
generateLabelFromNeighborhood,
getZoneId,
} from '../util/util.js';

export default async () => {
const data_url = 'data/income-inequality.geojson';
Expand Down Expand Up @@ -68,72 +40,22 @@ export default async () => {
filter: ['==', '$type', 'Polygon'],
});

let currentPopup = null;
let currentFeatureId = null;
const cleanupPopup = () => {
currentPopup?.remove();
currentPopup = null;
currentFeatureId = null;
};

map.on('close-income-popup', cleanupPopup);

const handlePopup = (e) => {
const [feature] = map.queryRenderedFeatures(e.point, {
layers: ['income-layer'],
});

if (feature) {
const incomeValue = feature.properties.median_household_income;
const featureId = feature.properties.spatial_id;

if (!document.getElementById('show-income-popup').checked) {
return;
}

if (currentFeatureId !== featureId) {
const featureCentroid = centroidDict[featureId];
if (!currentPopup) {
currentPopup = new mapboxgl.Popup({
anchor: 'bottom',
closeOnClick: false,
});
currentPopup.on('close', () => {
currentPopup = null;
});

currentPopup.addTo(map);
}

const useLabel = generateIncomeLabel(feature, e);
const formattedIncome = incomeValue
? currencyFormatter.format(incomeValue)
: 'Unknown';
currentPopup
.setLngLat(featureCentroid?.geometry.coordinates || e.lngLat)
.setHTML(
`<strong>Area:</strong> ${useLabel}<br>
<strong>Median Household Income:</strong> ${formattedIncome}`
);

currentFeatureId = featureId;
}
} else if (!feature && currentFeatureId && currentPopup) {
cleanupPopup();
}

map.getCanvas().style.cursor = feature ? 'pointer' : '';
const popupHtml = (feature, e) => {
const incomeValue = feature.properties.median_household_income;
const useLabel = generateLabelFromNeighborhood(feature, e);
const formattedIncome = incomeValue
? currencyFormatter.format(incomeValue)
: 'Unknown';
return `<strong>Area:</strong> ${useLabel}<br>
<strong>Median Household Income:</strong> ${formattedIncome}`;
};

map.on('mousemove', 'income-layer', handlePopup);
map.on('touchstart', 'income-layer', handlePopup);
map.on('mouseleave', 'income-layer', (e) => {
// close any open popup on leaving the parent layer
const features = map.queryRenderedFeatures(e.point, {
layers: ['income-layer'],
});
if (!features.length) {
map.fire('close-income-popup');
}
popupHandler({
centroids,
onCloseEventLabel: 'close-income-popup',
popupCheckboxId: 'show-income-popup',
popupHtml,
targetFeatureIdKey: 'spatial_id',
targetLayerLabel: 'income-layer',
});
};
2 changes: 1 addition & 1 deletion js/layers/neighborhoods-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
createRandomColorExpression,
fetchJSON,
generateCentroids,
} from '../util.js';
} from '../util/util.js';

export default async () => {
const data_url =
Expand Down
2 changes: 1 addition & 1 deletion js/layers/network-layers.util.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetchJSON } from "../util.js";
import { fetchJSON } from "../util/util.js";

export const loadNetworkLayer = async (endpoint, name) => {
const api_endpoint =
Expand Down
2 changes: 2 additions & 0 deletions js/map-on-load.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { toggleSidebar } from './bind-elements.js';
import initHeatmap from './init-heatmap.js';
import loadBroadbandAccessLayer from './layers/broadband-access-layer.js';
import loadIncomeLayer from './layers/income-layer.js';
import loadNeighborhoodsLayer from './layers/neighborhoods-layer.js';
import { loadNetworkLayers, loadNetworkPoints } from './layers/network-layers.js';
Expand All @@ -18,6 +19,7 @@ export default () => {
loadNetworkLayers();
loadNeighborhoodsLayer();
loadIncomeLayer();
loadBroadbandAccessLayer();
// end async layers

// Create heatmap based on features' "type" property
Expand Down
Loading

0 comments on commit c7552a3

Please sign in to comment.