// There's no obvious static URL to use to fetch this from ArcGIS, so use a static mirror of it for now.
const PARCEL_JSON_PATH = "//jcornell.net/static/parcels/Tax_Parcels_(Assessor_Property_Information).geojson";
+const AMENITIES_PATH = "//jcornell.net/static/parcels/Madison_amenities.json";
+
const PARCEL_LOAD_KEY = "parcelsLoaded";
const PICK_ZONINGS = new Set([
)));
}
+function haversine(angle) {
+ return (1 - Math.cos(angle)) / 2;
+}
+
+function radians(degrees) {
+ return degrees / 360 * 2 * Math.PI;
+}
+
+// This gives the central angle in radians of the great-circle arc between the
+// points. https://en.wikipedia.org/wiki/Haversine_formula#Example
+function arcBetween([latA, lonA], [latB, lonB]) {
+ // h: haversine of the central angle between the points
+ const h = haversine(radians(latB - latA))
+ + Math.cos(radians(latA)) * Math.cos(radians(latB)) * haversine(radians(lonB - lonA));
+ return 2 * Math.asin(Math.sqrt(h));
+}
+
const map = L.map("map");
const statusSpan = document.querySelector("#status > span");
const progressBar = document.querySelector("#status > progress");
minLon = Math.min(minLon, ...longitudes);
maxLat = Math.max(maxLat, ...latitudes);
maxLon = Math.max(maxLon, ...longitudes);
+ // This is for the walkability analysis later. It doesn't go back in the database.
+ feature.centerPoint = [
+ (Math.min(...latitudes) + Math.max(...latitudes)) / 2,
+ (Math.min(...longitudes) + Math.max(...longitudes)) / 2
+ ];
}
flyBounds = L.latLngBounds(L.latLng(minLat, minLon), L.latLng(maxLat, maxLon));
}
if (flyBounds !== null) {
map.flyToBounds(flyBounds);
}
+
+ progressBar.removeAttribute("value");
+ statusSpan.innerText = "analyzing walkability"
+ const amenitiesResponse = await fetch(AMENITIES_PATH);
+ if (amenitiesResponse.ok) {
+ const amenities = await amenitiesResponse.json();
+ const featuresAndScores = [];
+ for (const [i, feature] of matches.entries()) {
+ progressBar.value = i / matches.length;
+ const arcLengths = Object.values(amenities).map(
+ locations => Math.min(...locations.map(
+ point => arcBetween(feature.centerPoint, point)))
+ );
+ arcLengths.sort();
+ // score: median across amenity types of arc length to nearest
+ const mid = arcLengths.length / 2;
+ const score = arcLengths.length % 2 == 0 ?
+ (arcLengths[mid] + arcLengths[mid - 1]) / 2
+ : arcLengths[Math.floor(mid)];
+ featuresAndScores.push([feature, score]);
+ }
+ featuresAndScores.sort(([a, aScore], [b, bScore]) => aScore - bScore);
+ const listElement = document.getElementById("walkable");
+ for (const [feature, _] of featuresAndScores.values().take(10)) {
+ const entryElement = document.createElement("li");
+ const linkElement = document.createElement("a");
+ entryElement.append(linkElement);
+ linkElement.setAttribute(
+ "href",
+ `https://www.cityofmadison.com/assessor/property/propertydata.cfm?ParcelN=${feature.properties.Parcel}`
+ );
+ linkElement.append(feature.properties.Address);
+ listElement.append(entryElement);
+ }
+ } else {
+ console.error(amenitiesResponse);
+ throw new Error("Amenities request failed");
+ }
statusSpan.innerText = "ready";
}