From e4f8d0d808b143c2d298d71bcd0c81b823aae1fc Mon Sep 17 00:00:00 2001 From: Chandler Swift Date: Sat, 29 Nov 2025 13:23:35 -0600 Subject: [PATCH] Add MN Ambulance Service Areas layer Requested-By: Isaac Swift --- layers/index.js | 2 + layers/mn-ambulance-service-areas/README.md | 3 + layers/mn-ambulance-service-areas/get_data.sh | 56 +++++++++++++++++++ layers/mn-ambulance-service-areas/layer.js | 54 ++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 layers/mn-ambulance-service-areas/README.md create mode 100755 layers/mn-ambulance-service-areas/get_data.sh create mode 100644 layers/mn-ambulance-service-areas/layer.js diff --git a/layers/index.js b/layers/index.js index 2fd8c83..9258b7c 100644 --- a/layers/index.js +++ b/layers/index.js @@ -21,6 +21,7 @@ import survey_markers from './survey-markers/index.js'; import tjx from './tjx/index.js'; import minnesotaAdventureTrails from './minnesota-adventure-trails/index.js'; import cropHistory from './crop-history/index.js'; +import mnAmbulanceServiceAreas from './mn-ambulance-service-areas/layer.js'; const layerCategories = [ { // Base maps @@ -90,6 +91,7 @@ const layerCategories = [ name: "Bikepacking.com Routes", layer: bikepackingLayer, }, + mnAmbulanceServiceAreas, ] }, minnesotaAdventureTrails, diff --git a/layers/mn-ambulance-service-areas/README.md b/layers/mn-ambulance-service-areas/README.md new file mode 100644 index 0000000..608ee2d --- /dev/null +++ b/layers/mn-ambulance-service-areas/README.md @@ -0,0 +1,3 @@ +https://mn.gov/oems/ambulance-services/primary-service-area-maps.jsp + +https://experience.arcgis.com/experience/a222fe7ceaf44f868ec3c0f5dafe8446/page/Page diff --git a/layers/mn-ambulance-service-areas/get_data.sh b/layers/mn-ambulance-service-areas/get_data.sh new file mode 100755 index 0000000..fee5c1d --- /dev/null +++ b/layers/mn-ambulance-service-areas/get_data.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env nix-shell +#! nix-shell -i python3 -p python3 python3Packages.requests python3Packages.shapely + +import requests +import json +from collections import defaultdict +from shapely.geometry import shape, mapping +from shapely.ops import unary_union + +# https://services.arcgis.com/9OIuDHbyhmH91RfZ/arcgis/rest/services/EMSRB_Statewide/FeatureServer/0/query?f=pbf&cacheHint=true&maxRecordCountFactor=4&resultOffset=0&resultRecordCount=8000&where=1%3D1&orderByFields=OBJECTID%20ASC&outFields=AmbServNam%2CCounty%2COBJECTID&outSR=102100&spatialRel=esriSpatialRelIntersects + +url = "https://services.arcgis.com/9OIuDHbyhmH91RfZ/arcgis/rest/services/EMSRB_Statewide/FeatureServer/0/query" +params = { + "f": "geojson", + "cacheHint": "true", + "maxRecordCountFactor": "4", + "resultOffset": "0", + "resultRecordCount": "8000", + "where": "1=1", + "outFields": "AmbServNam,County", + "outSR": "102100", + "spatialRel": "esriSpatialRelIntersects", +} + +response = requests.get(url, params=params) +response.raise_for_status() + +data = json.loads(response.text) + +services = defaultdict(list) + +for feature in data["features"]: + service_name = feature["properties"].get("AmbServNam", "Unknown") + services[service_name].append(feature) + +merged_features = [] + +for service_name, features in services.items(): + merged_features.append( + { + "type": "Feature", + "properties": { + "AmbServNam": service_name, + }, + "geometry": mapping(unary_union([shape(feature["geometry"]) for feature in features])), + } + ) + +merged_data = { + "type": "FeatureCollection", + "crs": {"type": "name", "properties": {"name": "EPSG:3857"}}, + "features": merged_features, +} + +with open("mn-ambulance-service-areas.geojson", "w", encoding="utf-8") as f: + json.dump(merged_data, f) diff --git a/layers/mn-ambulance-service-areas/layer.js b/layers/mn-ambulance-service-areas/layer.js new file mode 100644 index 0000000..b24366d --- /dev/null +++ b/layers/mn-ambulance-service-areas/layer.js @@ -0,0 +1,54 @@ +import GeoJSON from 'ol/format/GeoJSON.js'; +import VectorLayer from 'ol/layer/Vector.js'; +import VectorSource from 'ol/source/Vector.js'; + +import serviceAreas from './mn-ambulance-service-areas.geojson?url'; + +import { Fill, Stroke, Style, Text } from 'ol/style.js'; + +window.chosenColors = {}; +window.chosenColors2 = {}; + +function style(feature){ + const name = feature.get('AmbServNam'); + // // djb2 -- results in very non-uniform distribution + // // https://web.archive.org/web/20251011200517/https://www.cse.yorku.ca/~oz/hash.html + // let hash = 5381; + // for (let i = 0; i < name.length; i++) { + // hash = hash * 33 + name.charCodeAt(i); + // } + // const colorDeg = hash % 360; + + // erichash -- thanks @villnoweric + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash += name.charCodeAt(i); + } + let colorDeg = hash % 360; + + return new Style({ + text: new Text({ + text: name, + }), + fill: new Fill({ + color: `lch(50% 50 ${colorDeg} / 40%)`, + }), + stroke: new Stroke({ + color: `lch(50% 50 ${colorDeg})`, + width: 1.25, + }), + }); +} + +const layer = { + name: "MN Ambulance Service Areas", + layer: new VectorLayer({ + source: new VectorSource({ + url: serviceAreas, + format: new GeoJSON(), + }), + style: style, + }), +}; + +export default layer;