Service area explorer
  • Home
  • About
stdlib = require("@observablehq/stdlib")
d3 = require("d3@7")

L = require('leaflet@1.9.4')

html`<link href='${resolve('leaflet@1.2.0/dist/leaflet.css')}' rel='stylesheet' />`
bootstrap=require("bootstrap")

Papa = require("papaparse")
service_areas = FileAttachment("service-areas-v2-metadata.geojson").json()
show_sidebar = function () {
  document.querySelector('#myCollapse').classList.add('show');
  const event = new Event('sidebar-opened');
  window.dispatchEvent(event);
  console.log('Sidebar opened');
  //viewof sidebar_message.querySelector(".btn-close").onclick = () => { hide_sidebar();  };
}

hide_sidebar = function () {
  document.querySelector('#myCollapse').classList.remove('show');
  const event = new Event('sidebar-closed');
  window.dispatchEvent(event);
  console.log('Sidebar closed');
}
button_code = `<button type="button" class="btn-close position-absolute top-0 end-0 m-2" aria-label="Close" onclick="(${hide_sidebar})()"></button>`;

welcome_message = `${button_code}<h3>Alaska electric utilities service area explorer</h3><br>This interactive map enables you to view Alaskan electric utility service areas.<br><br>Try browsing around the map and clicking on a utility service area `;
mutable name = ""; // TODO: find a way to do this without mutable
mutable cpcn_url = "";
mutable granted_year = "";

// Example data — replace with actual selected feature

mutable should_show_welcome_message = true;

viewof sidebar_message = should_show_welcome_message ? html`<p>${welcome_message}</p>` : html`<div class="position-relative p-3">
  ${button_code}
  <h5 class="mb-3">${name}</h5>
  <ul class="list-group list-group-flush">
    <li>Certificate granted in ${granted_year}*</li>
    <li>View <a href="${cpcn_url}" target="_blank">certificate information</a> on the Regulatory Commission of Alaska website</li>
  </ul>
  <br>
  <span style="font-size:10px">*Note: Year shown is the year that the application for a Certificate of Public Convenience and Necessity was granted. A utility can expand its service area under the same certificate over time, therefore, year shown might not the be same year that the communit(ies) inside this service area were first electrified or first received service from an electric utility. Also, RCA's online system doesn't list years older than 1964. If the year shown is 1964, the utility was probably formed earlier. </span>
</div>`;
viewof container = DOM.element('div', { style: `height:85vh` });
// TODO split into smaller cells
map = {

  // create map object
  let map = L.map(viewof container);
  map.on('load', function(event){
    // get the width of the screen after the resize event
    var width = document.documentElement.clientWidth;
    // tablets are between 768 and 922 pixels wide
    // phones are less than 768 pixels wide
    if (width < 768) {
        map.setZoom(4);
    }  
  });

  map.setView([62.945279601222396, -155.5946697727831], 5);
  // https://github.com/kbvernon/hndsr-watersheds/blob/main/observable-map.qmd#L122
  // add basemap
  
  window.___MAP = map;
  
  
  
  map.whenReady(()=> {
    //console.log('whenReady fired');
    //fetch('whenreadyfired.com')
    //show_sidebar();
  });
  
  // Tell leaflet that we closed/opened sidebar which caused map width to change
  addEventListener('sidebar-closed', ()=>{
    console.log('map recv sidebar close');
    previouslyClickedLayer.setStyle(myStyle);
    previouslyClickedLayer = null;
    map.invalidateSize();
  });
  addEventListener('sidebar-opened', ()=>{
    console.log('map recv sidebar open');
    map.invalidateSize();
  });
  
  const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'});
  // osm.addTo(map);
  
  const esri_topo = L.tileLayer(
    'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', 
    {attribution: 'Tiles &copy; Esri &mdash; Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community'});
  // esri_topo.addTo(map);
  
  
  // https://geoportal.alaska.gov/portal/home/item.html?id=d462231cc1454e1abb2dccd9a709a476
  const ak_2020_imagery = L.tileLayer(
    'https://geoportal.alaska.gov/arcgis/rest/services/ahri_2020_rgb_cache/MapServer/tile/{z}/{y}/{x}?blankTile=false', {
    //layers: '0',
        //format: 'image/png',
    attribution: 'Dynamic Mosaic &copy; 2020 Maxar Technologies Inc., Alaska Geospatial Office, USGS'});
  ak_2020_imagery.addTo(map);
  
  const aedg_communities = null;
  
  L.control.layers(
    {
      "OpenStreetMap": osm,
      "ESRI Topographic": esri_topo,
      "Alaska High Resolution Imagery (50cm)": ak_2020_imagery
    },
    null,
    {position: 'topleft'}
  ).addTo(map);

  L.control.scale({imperial: true, metric: true}).addTo(map);
  
  //let communityMarkers  = new L.FeatureGroup();
  let communities = [];
  
  // https://github.com/HandsOnDataViz/leaflet-map-csv/blob/main/index.html
  fetch('https://raw.githubusercontent.com/acep-aedg/aedg-data-pond/refs/heads/main/data/public/public_communities.csv')
    .then((response) => response.text())
    .then((csvString) => {
      let data = Papa.parse(csvString, {header: true, dynamicTyping: true, skipEmptyLines: true}).data;

      for (let i in data) {
        let row = data[i];
  
        /*let marker = L.circleMarker([row.latitude, row.longitude], {
          radius: 4,
          opacity: 1,
          color: '#000',
          stroke: false,
          fillOpacity: 0.75
        }).bindTooltip(row.name);*/
        communities.push({name: row.name, lat: row.latitude, lon: row.longitude, shown: false});
        //communityMarkers.addLayer(marker);
        //marker.addTo(map);
      }
    })

  // https://leafletjs.com/examples/geojson/
  // Add this last?
  
  var myStyle = {
    "color": "#ff932e",
    //"weight": 5,
    //"opacity": 0.65
  };
  
  var highlight = {
    "color": "#FF0000"
  };

  let previouslyClickedLayer = null; 
  
  L.geoJSON(service_areas, {
    style: myStyle,
    onEachFeature: function (feature, layer) {
      layer.on('click', function (e) {
        console.log('Feature clicked:');
        console.log(feature);
        if (previouslyClickedLayer && previouslyClickedLayer !== layer) {
          previouslyClickedLayer.setStyle(myStyle);
        }
        layer.setStyle(highlight);
        previouslyClickedLayer = layer;
        map.flyToBounds(layer.getBounds(), {duration: 0.4});
        // map.setView(layer.getBounds().getCenter());
        mutable name = layer.feature.properties.certificate_name;
        mutable cpcn_url = layer.feature.properties.certificate_url;
        mutable granted_year = layer.feature.properties.certificate_granted_year;
        mutable should_show_welcome_message = false;
        show_sidebar();
        // TODO: show list of communities/places within service area (served by clicked utility). Could check which pins are in bounds, but better approach will be to use AEDG data. In sidebar, city names should be hyperlinked, when you click the name it will zoom the map to that city (useful for utilities that service many cities).
        // Also, button to return to starting view of entire state
        
      });
    }
  }).addTo(map);
  
  let currentMarkers = [];

  // Instead of hiding all marker layers based on zoom level as shown in the link, which has poor performance on my phone, selectively show markers based on position in map bounds
  // https://gis.stackexchange.com/questions/258515/show-hide-markers-depending-on-zoom-level

  function renderMarkers()  {
    if (map.getZoom() >= 7) { // maybe 8 if desktop and 7 if mobile/small screen
      // Adapted from https://medium.com/@silvajohnny777/optimizing-leaflet-performance-with-a-large-number-of-markers-0dea18c2ec99
    
      // Get the current map bounds
        const bounds = map.getBounds();
        
      // Temp comment this out, I feel like it's probably OK for the user to scroll around and keep the communities scrolled out of view on the map  
      // Remove old markers
      //  currentMarkers.forEach((marker) => map.removeLayer(marker));
      //  currentMarkers.length = 0;
    
        // Render only markers that are within the map bounds
        communities.filter((x)=>!x.shown).forEach((community) => {
          const position = [community.lat, community.lon];
    
        // Check if the community is within the current map bounds
        if (bounds.contains(position)) {
          let marker = L.marker(position, {
            opacity: 1,
            color: 'red'
          }).bindTooltip(community.name);
          // console.log("Added marker for " + community.name);;
          marker.addTo(map);
          currentMarkers.push(marker);
          community.shown = true;
        }
      });
    } else {
      // Clear existing markers if zoomed out
      currentMarkers.forEach((marker) => map.removeLayer(marker));
      currentMarkers.length = 0;
      communities.forEach((community) => community.shown = false);
    }
  };
  
  map.on('moveend', renderMarkers);
  map.on('zoomend', renderMarkers);
  
  // TODO: add legend https://stackoverflow.com/questions/59453642/how-to-add-legend-in-leaflet-map
  



}