Repository: akrherz/iem Branch: main Commit: 6f5ebf7b4674 Files: 1813 Total size: 21.0 MB Directory structure: gitextract_wagou1ak/ ├── .deepsource.toml ├── .editorconfig ├── .github/ │ ├── ci_db_testdata.py │ ├── copilot-instructions.md │ ├── dependabot.yml │ ├── ms_environment.yml │ ├── setupdata.sh │ ├── setuppaths.sh │ └── workflows/ │ ├── build.yml │ ├── codeql.yml │ ├── etchosts.txt │ ├── mapserver.yml │ ├── publish-iemjs.yml │ └── test-iemjs.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierrc ├── LICENSE ├── README.md ├── cgi-bin/ │ ├── afos/ │ │ └── retrieve.py │ ├── climate/ │ │ └── orc.py │ ├── geocoder.py │ ├── index.php │ ├── mywindrose.py │ ├── onsite/ │ │ └── birthday/ │ │ └── getweather.py │ ├── precip/ │ │ ├── catAZOS.py │ │ └── catSNET.py │ ├── request/ │ │ ├── asos.py │ │ ├── asos1min.py │ │ ├── coop.py │ │ ├── coopobs.py │ │ ├── daily.py │ │ ├── feel.py │ │ ├── gis/ │ │ │ ├── awc_gairmets.py │ │ │ ├── cwas.py │ │ │ ├── lsr.py │ │ │ ├── misc.py │ │ │ ├── nexrad_storm_attrs.py │ │ │ ├── pireps.py │ │ │ ├── sigmets.py │ │ │ ├── spc_mcd.py │ │ │ ├── spc_outlooks.py │ │ │ ├── spc_watch.py │ │ │ ├── sps.py │ │ │ ├── watch_by_county.py │ │ │ ├── watchwarn.py │ │ │ └── wpc_mpd.py │ │ ├── grx_rings.py │ │ ├── hads.py │ │ ├── hml.py │ │ ├── hourlyprecip.py │ │ ├── index.php │ │ ├── isusm.py │ │ ├── metars.py │ │ ├── mos.py │ │ ├── nass_iowa.py │ │ ├── nlaeflux.py │ │ ├── normals.py │ │ ├── other.py │ │ ├── purpleair.py │ │ ├── raob.py │ │ ├── raster2netcdf.py │ │ ├── rwis.py │ │ ├── scan.py │ │ ├── scp.py │ │ ├── smos.py │ │ ├── ss.py │ │ ├── taf.py │ │ ├── talltowers.py │ │ ├── tempwind_aloft.py │ │ ├── uscrn.py │ │ └── wmo_bufr_srf.py │ └── wms/ │ ├── goes/ │ │ ├── alaska_ir.cgi │ │ ├── alaska_vis.cgi │ │ ├── alaska_wv.cgi │ │ ├── conus_ir.cgi │ │ ├── conus_vis.cgi │ │ ├── conus_wv.cgi │ │ ├── east_ir.cgi │ │ ├── east_vis.cgi │ │ ├── east_wv.cgi │ │ ├── goes.cgi │ │ ├── hawaii_ir.cgi │ │ ├── hawaii_vis.cgi │ │ ├── hawaii_wv.cgi │ │ ├── west_ir.cgi │ │ ├── west_vis.cgi │ │ └── west_wv.cgi │ ├── goes_east.cgi │ ├── goes_west.cgi │ ├── hrrr/ │ │ ├── refd.cgi │ │ └── refp.cgi │ ├── idep.cgi │ ├── index.php │ ├── iowa/ │ │ ├── rainfall.cgi │ │ └── roadcond.cgi │ ├── nexrad/ │ │ ├── daa.cgi │ │ ├── dta.cgi │ │ ├── eet.cgi │ │ ├── n0q-t.cgi │ │ ├── n0r-t.cgi │ │ ├── n1p.cgi │ │ ├── net.cgi │ │ ├── ntp.cgi │ │ └── ridge.cgi │ ├── q2.cgi │ └── us/ │ ├── counties.cgi │ ├── mrms.cgi │ ├── mrms_nn.cgi │ ├── obs.cgi │ ├── roadtemps.cgi │ └── states.cgi ├── config/ │ ├── 00iem-ssl.conf │ ├── 00iem.conf │ ├── backend.conf │ ├── iem-archive.conf │ ├── mesonet-longterm-vhost.conf │ ├── mesonet.inc │ ├── navbar.json │ └── settings.inc.php.in ├── conftest.py ├── data/ │ └── gis/ │ ├── avl/ │ │ └── iemrainfall.avl │ ├── base.sym │ ├── fonts.list │ ├── iem.mapinc │ ├── lsrs.mapinc │ ├── meta/ │ │ ├── 26914.prj │ │ ├── 26915.prj │ │ ├── 4326.prj │ │ ├── 5070.prj │ │ ├── current_ww.shp.xml │ │ └── stereo.prj │ ├── roads.mapinc │ ├── shape/ │ │ ├── basins.dbf │ │ ├── basins.sbn │ │ ├── basins.sbx │ │ ├── basins.shp │ │ └── basins.shx │ ├── stations.sym │ └── symbols/ │ ├── images/ │ │ └── iem_logo.png-save │ └── stations.sym ├── deployment/ │ ├── iem-tilecache.service │ ├── start_tc_wsgi.sh │ └── symlink_manager.py ├── docs/ │ ├── datasets/ │ │ ├── afos.md │ │ ├── climodat.md │ │ ├── iemre.md │ │ ├── metar.md │ │ ├── template.md │ │ └── vtec.md │ ├── deployment/ │ │ └── vendor-static-assets.md │ ├── meetings.md │ ├── nmp.md │ ├── soilmoisture.md │ └── yieldproject.md ├── environment.yml ├── eslint.config.js ├── htdocs/ │ ├── .well-known/ │ │ ├── ai-plugin.json │ │ └── traffic-advice │ ├── ASOS/ │ │ ├── current.phtml │ │ ├── index.phtml │ │ ├── precipnote.phtml │ │ ├── recent.css │ │ ├── recent.module.js │ │ ├── recent.phtml │ │ └── reports/ │ │ ├── mon_prec.css │ │ ├── mon_prec.module.js │ │ └── mon_prec.php │ ├── AWOS/ │ │ ├── current.phtml │ │ ├── index.phtml │ │ ├── reports/ │ │ │ └── mon_prec.php │ │ └── skyc.phtml │ ├── COOP/ │ │ ├── 7am-app.js │ │ ├── 7am.css │ │ ├── 7am.php │ │ ├── cat.module.js │ │ ├── cat.phtml │ │ ├── current.phtml │ │ ├── dl/ │ │ │ └── normals.phtml │ │ ├── extremes.css │ │ ├── extremes.js │ │ ├── extremes.php │ │ ├── freezing.php │ │ ├── hpd.php │ │ ├── index.css │ │ ├── index.phtml │ │ ├── map/ │ │ │ ├── index.css │ │ │ ├── index.js │ │ │ └── index.php │ │ ├── periods.phtml │ │ └── snowd_duration.phtml │ ├── DCP/ │ │ ├── cat.phtml │ │ ├── current.phtml │ │ ├── index.phtml │ │ ├── map.css │ │ ├── map.js │ │ ├── map.php │ │ ├── plot.module.js │ │ ├── plot.php │ │ ├── plot.phtml │ │ ├── site.css │ │ ├── site.phtml │ │ ├── tomb.module.js │ │ └── tomb.phtml │ ├── GIS/ │ │ ├── ams_030211.phtml │ │ ├── apps/ │ │ │ ├── agclimate/ │ │ │ │ ├── chill.php │ │ │ │ ├── dayplot.phtml │ │ │ │ ├── gsplot.css │ │ │ │ ├── gsplot.js │ │ │ │ ├── gsplot.php │ │ │ │ ├── gsplot.phtml │ │ │ │ ├── month.php │ │ │ │ └── plot.php │ │ │ ├── coop/ │ │ │ │ ├── gsplot.php │ │ │ │ ├── gsplot.phtml │ │ │ │ ├── index.php │ │ │ │ ├── plot.phtml │ │ │ │ └── request.php │ │ │ ├── iem/ │ │ │ │ ├── freeze.js │ │ │ │ └── freeze.phtml │ │ │ ├── onsite/ │ │ │ │ └── robins.php │ │ │ ├── profit/ │ │ │ │ ├── index.css │ │ │ │ ├── index.js │ │ │ │ └── index.php │ │ │ └── rview/ │ │ │ ├── anim_gif.php │ │ │ ├── compare.phtml │ │ │ ├── warnings.css │ │ │ ├── warnings.module.js │ │ │ ├── warnings.phtml │ │ │ ├── warnings_cat.phtml │ │ │ └── watch.phtml │ │ ├── awips211.aux.xml │ │ ├── awips211.prj │ │ ├── goes.phtml │ │ ├── index.php │ │ ├── isu_021120.phtml │ │ ├── maps/ │ │ │ └── pyims.xml │ │ ├── model.phtml │ │ ├── rad-by-year-fe.phtml │ │ ├── rad-by-year.php │ │ ├── radmap.php │ │ ├── radmap_api.phtml │ │ ├── radview.phtml │ │ ├── rasters.php │ │ ├── rby-overview.php │ │ ├── ridge.phtml │ │ ├── sbw-history.php │ │ └── tiff/ │ │ └── index.py │ ├── QC/ │ │ ├── index.phtml │ │ ├── madis/ │ │ │ ├── network.css │ │ │ └── network.phtml │ │ ├── offline.php │ │ └── tickets.phtml │ ├── RWIS/ │ │ ├── camera.module.js │ │ ├── camera.phtml │ │ ├── current.phtml │ │ ├── currentSF.phtml │ │ ├── index.phtml │ │ ├── soil.phtml │ │ └── traffic.phtml │ ├── agclimate/ │ │ ├── ames_precip.phtml │ │ ├── current.phtml │ │ ├── display.php │ │ ├── et.phtml │ │ ├── hist/ │ │ │ ├── daily.php │ │ │ ├── dailyRequest.php │ │ │ ├── hourly.php │ │ │ ├── hourlyRequest.php │ │ │ ├── inversion.php │ │ │ └── worker.php │ │ ├── index.css │ │ ├── index.js │ │ ├── index.phtml │ │ ├── info.phtml │ │ ├── smts.php │ │ ├── soilt-prob.php │ │ ├── soilt.css │ │ ├── soilt.module.js │ │ └── soilt.php │ ├── agweather/ │ │ └── index.php │ ├── api/ │ │ ├── index.php │ │ └── proxy_error_handler.py │ ├── apps.module.js │ ├── apps.php │ ├── archive/ │ │ ├── codsat/ │ │ │ ├── index.css │ │ │ ├── index.module.js │ │ │ └── index.phtml │ │ ├── index.php │ │ ├── mrms.php │ │ ├── raob/ │ │ │ ├── index.module.js │ │ │ ├── index.phtml │ │ │ ├── list.module.js │ │ │ └── list.phtml │ │ └── schema.php │ ├── c/ │ │ ├── c.py │ │ └── tile.py │ ├── cache/ │ │ └── tile.py │ ├── circa2001.phtml │ ├── clientaccesspolicy.xml │ ├── climate/ │ │ ├── index.phtml │ │ ├── records.phtml │ │ ├── week.phtml │ │ ├── year.phtml │ │ └── yesterday.phtml │ ├── climodat/ │ │ ├── index.css │ │ ├── index.phtml │ │ ├── monitor.module.js │ │ ├── monitor.php │ │ ├── monthly_ks.txt │ │ └── yearly_ks.txt │ ├── cocorahs/ │ │ ├── current.phtml │ │ ├── index.phtml │ │ └── obs.phtml │ ├── content/ │ │ ├── date.php │ │ └── pil.php │ ├── cool/ │ │ └── index.phtml │ ├── cow/ │ │ ├── chart.php │ │ ├── index.module.js │ │ ├── index.phtml │ │ ├── maplsr.phtml │ │ ├── sbwstats.css │ │ ├── sbwstats.phtml │ │ ├── sbwsum.phtml │ │ └── top10.phtml │ ├── crossdomain.xml │ ├── css/ │ │ ├── iastate-iem.css │ │ ├── iemss.css │ │ ├── main.css │ │ └── print.css │ ├── current/ │ │ ├── all.phtml │ │ ├── bloop.phtml │ │ ├── camera.module.js │ │ ├── camera.phtml │ │ ├── camlapse/ │ │ │ ├── app.js │ │ │ ├── index.phtml │ │ │ ├── kcrg.js │ │ │ └── kcrg.phtml │ │ ├── camrad.php │ │ ├── index.phtml │ │ ├── isucams.phtml │ │ ├── live.py │ │ ├── loop.phtml │ │ ├── mcview.module.js │ │ ├── mcview.phtml │ │ ├── radar.phtml │ │ ├── sel.phtml │ │ ├── severe.phtml │ │ ├── uscrn.phtml │ │ ├── viewer.css │ │ ├── viewer.js │ │ ├── viewer.phtml │ │ ├── webcam.css │ │ ├── webcam.js │ │ └── webcam.php │ ├── disclaimer.php │ ├── dm/ │ │ └── index.phtml │ ├── docs/ │ │ ├── forecast/ │ │ │ └── highs.phtml │ │ ├── nexrad_mosaic/ │ │ │ ├── index.phtml │ │ │ └── wmst.html │ │ ├── radmapserver/ │ │ │ ├── howto-0.rtf │ │ │ ├── howto-1.html │ │ │ ├── howto-2.html │ │ │ ├── howto-3.html │ │ │ ├── howto-4.html │ │ │ ├── howto-5.html │ │ │ ├── howto-6.html │ │ │ ├── howto-7.html │ │ │ ├── howto.html │ │ │ ├── howto.rtf │ │ │ ├── howto.sgml │ │ │ ├── howto.txt │ │ │ └── index.phtml │ │ ├── unidata2006/ │ │ │ ├── index.phtml │ │ │ └── wmst.html │ │ └── unidata2021/ │ │ └── index.phtml │ ├── explorer/ │ │ ├── index.css │ │ ├── index.js │ │ └── index.php │ ├── feature_rss.php │ ├── icons/ │ │ └── README │ ├── iemre/ │ │ ├── daily.py │ │ ├── hourly.py │ │ ├── index.phtml │ │ └── multiday.py │ ├── index.css │ ├── index.js │ ├── index.phtml │ ├── info/ │ │ ├── contacts.php │ │ ├── datasets.php │ │ ├── iem.php │ │ ├── links.php │ │ ├── nni.css │ │ ├── nni.phtml │ │ ├── nni_images/ │ │ │ └── morisita.xcf │ │ ├── refs.php │ │ └── variables.phtml │ ├── info.php │ ├── js/ │ │ ├── form2url.module.js │ │ ├── iastate-iem.js │ │ ├── iemss.js │ │ ├── jsani.js │ │ ├── maptable.js │ │ ├── olselect-lonlat.js │ │ ├── olselect.js │ │ └── select2.js │ ├── kml/ │ │ ├── network.php │ │ ├── roadcond.php │ │ ├── sbw_county_intersect.php │ │ ├── sbw_exact_time.php │ │ ├── sbw_interval.php │ │ ├── sbw_lsrs.php │ │ ├── timestamp.php │ │ ├── vtec.php │ │ └── webcams.php │ ├── layar/ │ │ └── l3attr.php │ ├── lsr/ │ │ ├── old.html │ │ ├── old.phtml │ │ ├── static.css │ │ ├── static.js │ │ └── wfos.js │ ├── metadata/ │ │ └── xml/ │ │ ├── index.php │ │ ├── pl.py │ │ ├── rp_IEM.xml │ │ ├── sc_CS_03002.xml │ │ ├── sc_CS_CS215.xml │ │ ├── sc_CS_CS300.xml │ │ ├── sc_CS_CS655.xml │ │ ├── sc_CS_TE525.xml │ │ ├── sd.py │ │ ├── sp_CS_03002_WindDirection.xml │ │ ├── sp_CS_03002_WindSpeed.xml │ │ ├── sp_CS_CS215_RH.xml │ │ ├── sp_CS_CS215_Temp.xml │ │ ├── sp_CS_CS300.xml │ │ ├── sp_CS_CS655_Moisture.xml │ │ ├── sp_CS_CS655_Temp.xml │ │ └── sp_CS_TE525.xml │ ├── mom.php │ ├── mos/ │ │ ├── csv.php │ │ ├── dl.php │ │ ├── fe.phtml │ │ ├── index.php │ │ └── table.phtml │ ├── my/ │ │ └── current.phtml │ ├── nstl_flux/ │ │ ├── index.phtml │ │ ├── plot.php │ │ ├── plots.phtml │ │ └── vars.phtml │ ├── nws/ │ │ ├── ccoop_current.php │ │ ├── cf6map.js │ │ ├── cf6map.php │ │ ├── cf6table.css │ │ ├── cf6table.module.js │ │ ├── cf6table.php │ │ ├── cli-audit/ │ │ │ ├── index.css │ │ │ ├── index.module.js │ │ │ └── index.php │ │ ├── climap.css │ │ ├── climap.js │ │ ├── climap.php │ │ ├── clitable.css │ │ ├── clitable.module.js │ │ ├── clitable.php │ │ ├── coop-cnts.php │ │ ├── debug_latlon/ │ │ │ ├── generate_plot.py │ │ │ ├── index.module.js │ │ │ └── index.phtml │ │ ├── index.php │ │ ├── list_tags.css │ │ ├── list_tags.module.js │ │ ├── list_tags.php │ │ ├── list_ugcs.css │ │ ├── list_ugcs.module.js │ │ ├── list_ugcs.php │ │ ├── mcd_top10.phtml │ │ ├── obs.php │ │ ├── pds_watches.css │ │ ├── pds_watches.module.js │ │ ├── pds_watches.php │ │ ├── snowfall_6hour.css │ │ ├── snowfall_6hour.module.js │ │ ├── snowfall_6hour.php │ │ ├── spc_outlook_search/ │ │ │ ├── index.css │ │ │ ├── index.js │ │ │ └── index.phtml │ │ ├── spc_top10.phtml │ │ ├── sps_search/ │ │ │ ├── index.css │ │ │ ├── index.js │ │ │ └── index.php │ │ ├── text.php │ │ ├── vtec_obs.php │ │ ├── watches.css │ │ ├── watches.module.js │ │ ├── watches.php │ │ ├── wfo_vtec_count.php │ │ ├── wpc_national_hilo/ │ │ │ ├── index.css │ │ │ ├── index.module.js │ │ │ └── index.php │ │ └── wpc_outlook_search/ │ │ ├── index.css │ │ ├── index.js │ │ └── index.phtml │ ├── ogc/ │ │ ├── arcgis_wtms.html │ │ ├── googlemaps_v3.html │ │ ├── index.phtml │ │ ├── ol3_example.html │ │ └── openlayers_example.html │ ├── one/ │ │ └── README.md │ ├── onsite/ │ │ ├── birthday/ │ │ │ ├── index.php │ │ │ └── pals.css │ │ ├── caucus2004/ │ │ │ ├── fx.txt │ │ │ └── index.phtml │ │ ├── features/ │ │ │ ├── cat.php │ │ │ ├── content.py │ │ │ ├── past.php │ │ │ ├── tags/ │ │ │ │ └── index.php │ │ │ ├── titles.php │ │ │ └── vote.py │ │ ├── news.phtml │ │ └── robins.phtml │ ├── other/ │ │ ├── current.phtml │ │ ├── daily_plot.phtml │ │ ├── index.phtml │ │ ├── plot_temps.php │ │ └── plot_winds.php │ ├── p.php │ ├── plotting/ │ │ ├── asos/ │ │ │ └── 1station_1min.phtml │ │ ├── auto/ │ │ │ ├── autoplot.py │ │ │ ├── gen_qrcode.py │ │ │ ├── index.css │ │ │ ├── index.module.js │ │ │ ├── index.py │ │ │ ├── js/ │ │ │ │ └── mapselect.js │ │ │ └── meta.py │ │ ├── awos/ │ │ │ ├── 1min.php │ │ │ ├── 1min_P.php │ │ │ ├── 1min_V.php │ │ │ └── 1station_1min.php │ │ ├── coop/ │ │ │ ├── acc.phtml │ │ │ ├── climate_fe.php │ │ │ ├── gddprobs.phtml │ │ │ ├── gspread.phtml │ │ │ ├── mspread.phtml │ │ │ ├── precip_cdf_fe.phtml │ │ │ ├── spread_fe.phtml │ │ │ ├── threshold_histogram.php │ │ │ └── threshold_histogram_fe.phtml │ │ ├── gs/ │ │ │ └── fe.phtml │ │ ├── index.php │ │ ├── isumet/ │ │ │ ├── 1min.php │ │ │ ├── 1min_P.php │ │ │ ├── 1min_V.php │ │ │ ├── 1min_inside.php │ │ │ ├── 1station_1min.phtml │ │ │ ├── ams2.phtml │ │ │ ├── rh.php │ │ │ └── srad.php │ │ ├── mesoeast/ │ │ │ ├── 1min.php │ │ │ ├── 1min_V.php │ │ │ ├── 1min_inside.php │ │ │ ├── baro.php │ │ │ ├── dailyrain.php │ │ │ ├── index.phtml │ │ │ ├── rh.php │ │ │ └── temp_rh.php │ │ ├── mesonorth/ │ │ │ ├── 1min_ot.php │ │ │ ├── battery.php │ │ │ └── ot_10min.phtml │ │ ├── month/ │ │ │ ├── rainfall.css │ │ │ ├── rainfall.phtml │ │ │ └── rainfall_plot.php │ │ ├── rwis/ │ │ │ ├── SFtemps.php │ │ │ ├── plot_soil.php │ │ │ ├── plot_traffic.php │ │ │ ├── sf_fe.css │ │ │ └── sf_fe.php │ │ ├── scan/ │ │ │ ├── index.phtml │ │ │ ├── precip.php │ │ │ ├── radn5temps.php │ │ │ ├── radn5temps2.php │ │ │ ├── smv.php │ │ │ └── winds.php │ │ └── snet/ │ │ ├── 1min_P.php │ │ ├── 1min_T.php │ │ ├── 1min_V.php │ │ ├── 1station_1min.php │ │ ├── data/ │ │ │ ├── SAMI4_051112.txt │ │ │ ├── SBOI4_051112.txt │ │ │ ├── SCHI4_060622.txt │ │ │ ├── SINI4_071002.txt │ │ │ ├── SMDI4_051112.txt │ │ │ ├── SPKI4_071002.txt │ │ │ └── STQI4_070506.txt │ │ ├── dataformat.php │ │ ├── indy_bore.php │ │ ├── polk_bore.php │ │ └── tama_bore.php │ ├── projects/ │ │ ├── iao/ │ │ │ ├── analog_download.php │ │ │ ├── iao_data_request_form.xlsx │ │ │ ├── index.css │ │ │ ├── index.php │ │ │ ├── sodar_download.php │ │ │ └── surface_download.php │ │ ├── iembot/ │ │ │ ├── channels.html │ │ │ ├── index.js │ │ │ ├── index.phtml │ │ │ ├── mastodon/ │ │ │ │ └── index.py │ │ │ ├── public.phtml │ │ │ ├── slack/ │ │ │ │ └── index.phtml │ │ │ └── twitter.php │ │ ├── index.phtml │ │ └── webcam.php │ ├── raccoon/ │ │ ├── index.phtml │ │ └── wait.phtml │ ├── radmapserver/ │ │ ├── gisdata/ │ │ │ ├── radar.tif │ │ │ ├── radar.wld │ │ │ ├── states.dbf │ │ │ ├── states.prj │ │ │ ├── states.sbn │ │ │ ├── states.sbx │ │ │ ├── states.shp │ │ │ └── states.shx │ │ └── radmapserver.html │ ├── rainfall/ │ │ ├── bypoint.phtml │ │ ├── dshape.php │ │ ├── index.phtml │ │ ├── mrms2img.py │ │ ├── obhour-json.php │ │ ├── obhour.css │ │ ├── obhour.module.js │ │ └── obhour.phtml │ ├── request/ │ │ ├── asos/ │ │ │ ├── 1min.phtml │ │ │ ├── 1min_dl.php │ │ │ ├── csv.php │ │ │ └── hourlyprecip.phtml │ │ ├── awos/ │ │ │ ├── 1min.php │ │ │ └── 1min_dl.php │ │ ├── coop/ │ │ │ ├── dl.php │ │ │ ├── fe.phtml │ │ │ ├── obs-dl.php │ │ │ └── obs-fe.phtml │ │ ├── daily.css │ │ ├── daily.phtml │ │ ├── dcp/ │ │ │ └── fe.phtml │ │ ├── download.css │ │ ├── download.phtml │ │ ├── gis/ │ │ │ ├── awc_gairmets.phtml │ │ │ ├── awc_sigmets.phtml │ │ │ ├── cwas.phtml │ │ │ ├── lsrs.phtml │ │ │ ├── misc.phtml │ │ │ ├── n0q2gtiff.php │ │ │ ├── n0r2gtiff.php │ │ │ ├── nexrad_storm_attrs.module.js │ │ │ ├── nexrad_storm_attrs.php │ │ │ ├── outlooks.phtml │ │ │ ├── pireps.module.js │ │ │ ├── pireps.php │ │ │ ├── spc_mcd.phtml │ │ │ ├── spc_outlooks.phtml │ │ │ ├── spc_watch.phtml │ │ │ ├── sps.phtml │ │ │ ├── watchwarn.module.js │ │ │ ├── watchwarn.phtml │ │ │ └── wpc_mpd.phtml │ │ ├── grx/ │ │ │ ├── asos.php │ │ │ ├── iadot_trucks.py │ │ │ ├── index.phtml │ │ │ ├── l3attr.py │ │ │ ├── roadcond.php │ │ │ ├── rwis.php │ │ │ ├── sbw.php │ │ │ ├── time_mot_loc.py │ │ │ ├── vtec.php │ │ │ ├── watch_by_county.php │ │ │ └── webcams.php │ │ ├── hml.php │ │ ├── hourlyprecip.phtml │ │ ├── ldm.php │ │ ├── maxcsv.php │ │ ├── maxcsv.py │ │ ├── rwis/ │ │ │ ├── fe.phtml │ │ │ ├── soil.phtml │ │ │ └── traffic.phtml │ │ ├── scan/ │ │ │ └── fe.phtml │ │ ├── taf.css │ │ ├── taf.php │ │ ├── tempwind_aloft.php │ │ ├── uscrn.php │ │ └── wmo_bufr_srf.php │ ├── river/ │ │ └── index.php │ ├── roads/ │ │ ├── gis.phtml │ │ ├── history.css │ │ ├── history.module.js │ │ ├── history.phtml │ │ ├── iem.php │ │ ├── index.phtml │ │ ├── maps.css │ │ ├── maps.js │ │ ├── maps.phtml │ │ ├── rc.phtml │ │ └── tv.php │ ├── robots.txt │ ├── rss.php │ ├── scan/ │ │ ├── current.phtml │ │ └── index.phtml │ ├── schoolnet/ │ │ ├── alerts/ │ │ │ └── index.phtml │ │ ├── dl/ │ │ │ ├── index.php │ │ │ ├── params.php │ │ │ └── worker.php │ │ └── index.php │ ├── sites/ │ │ ├── cal.phtml │ │ ├── current.php │ │ ├── dyn_windrose.module.js │ │ ├── dyn_windrose.phtml │ │ ├── hist.module.js │ │ ├── hist.phtml │ │ ├── locate.php │ │ ├── meteo.php │ │ ├── monthlysum.module.js │ │ ├── monthlysum.php │ │ ├── neighbors.php │ │ ├── networks.php │ │ ├── new-rss.php │ │ ├── obhistory.css │ │ ├── obhistory.module.js │ │ ├── obhistory.php │ │ ├── pics.php │ │ ├── porclimo.php │ │ ├── scp.php │ │ ├── site.js │ │ ├── site.php │ │ ├── taf.module.js │ │ ├── taf.php │ │ ├── test.py │ │ ├── windrose.css │ │ ├── windrose.module.js │ │ └── windrose.phtml │ ├── smos/ │ │ ├── index.php │ │ └── smosmap.js │ ├── timemachine/ │ │ ├── index.css │ │ ├── index.js │ │ └── index.php │ ├── topics/ │ │ ├── first_freeze.phtml │ │ ├── hardiness/ │ │ │ └── index.php │ │ └── pests/ │ │ ├── index.css │ │ ├── index.module.js │ │ └── index.php │ ├── uscrn/ │ │ └── index.phtml │ ├── vtec/ │ │ ├── emergencies.css │ │ ├── emergencies.js │ │ ├── emergencies.php │ │ ├── events.css │ │ ├── events.module.js │ │ ├── events.php │ │ ├── f.py │ │ ├── index.py │ │ ├── json-text.php │ │ ├── maxetn.css │ │ ├── maxetn.module.js │ │ ├── maxetn.php │ │ ├── mobile.php │ │ ├── pds.css │ │ ├── pds.module.js │ │ ├── pds.php │ │ ├── search.css │ │ ├── search.js │ │ ├── search.php │ │ ├── wfos.js │ │ └── yearly_counts.php │ ├── wfs/ │ │ └── ww.php │ └── wx/ │ └── afos/ │ ├── bottom.php │ ├── index.css │ ├── index.module.js │ ├── index.phtml │ ├── list.css │ ├── list.module.js │ ├── list.phtml │ ├── old.phtml │ ├── p.css │ ├── p.module.js │ ├── p.php │ ├── recent.php │ ├── retreive.php │ ├── text2png.py │ └── top.php ├── include/ │ ├── agclimate_boxinc.phtml │ ├── cameras.inc.php │ ├── composer.json │ ├── cow_worker.php │ ├── database.inc.php │ ├── dbase.stub.php │ ├── forms.php │ ├── generators.php │ ├── iemmap.php │ ├── iemprop.php │ ├── jpgraph/ │ │ ├── README │ │ ├── flag_mapping │ │ ├── fonts/ │ │ │ ├── FF_FONT0-Bold.gdf │ │ │ ├── FF_FONT0.gdf │ │ │ ├── FF_FONT1-Bold.gdf │ │ │ ├── FF_FONT1.gdf │ │ │ ├── FF_FONT2-Bold.gdf │ │ │ └── FF_FONT2.gdf │ │ ├── gd_image.inc.php │ │ ├── imageSmoothArc.php │ │ ├── imgdata_balls.inc.php │ │ ├── imgdata_bevels.inc.php │ │ ├── imgdata_diamonds.inc.php │ │ ├── imgdata_pushpins.inc.php │ │ ├── imgdata_squares.inc.php │ │ ├── imgdata_stars.inc.php │ │ ├── jpg-config.inc.php │ │ ├── jpgraph.php │ │ ├── jpgraph_antispam-digits.php │ │ ├── jpgraph_antispam.php │ │ ├── jpgraph_bar.php │ │ ├── jpgraph_canvas.php │ │ ├── jpgraph_canvtools.php │ │ ├── jpgraph_contour.php │ │ ├── jpgraph_date.php │ │ ├── jpgraph_errhandler.inc.php │ │ ├── jpgraph_error.php │ │ ├── jpgraph_flags.php │ │ ├── jpgraph_gantt.php │ │ ├── jpgraph_gb2312.php │ │ ├── jpgraph_gradient.php │ │ ├── jpgraph_iconplot.php │ │ ├── jpgraph_imgtrans.php │ │ ├── jpgraph_led.php │ │ ├── jpgraph_legend.inc.php │ │ ├── jpgraph_line.php │ │ ├── jpgraph_log.php │ │ ├── jpgraph_meshinterpolate.inc.php │ │ ├── jpgraph_mgraph.php │ │ ├── jpgraph_pie.php │ │ ├── jpgraph_pie3d.php │ │ ├── jpgraph_plotband.php │ │ ├── jpgraph_plotline.php │ │ ├── jpgraph_plotmark.inc.php │ │ ├── jpgraph_polar.php │ │ ├── jpgraph_radar.php │ │ ├── jpgraph_regstat.php │ │ ├── jpgraph_rgb.inc.php │ │ ├── jpgraph_scatter.php │ │ ├── jpgraph_stock.php │ │ ├── jpgraph_table.php │ │ ├── jpgraph_text.inc.php │ │ ├── jpgraph_theme.inc.php │ │ ├── jpgraph_ttf.inc.php │ │ ├── jpgraph_utils.inc.php │ │ ├── jpgraph_windrose.php │ │ ├── lang/ │ │ │ ├── de.inc.php │ │ │ ├── en.inc.php │ │ │ └── prod.inc.php │ │ └── themes/ │ │ ├── AquaTheme.class.php │ │ ├── GreenTheme.class.php │ │ ├── OceanTheme.class.php │ │ ├── OrangeTheme.class.php │ │ ├── PastelTheme.class.php │ │ ├── RoseTheme.class.php │ │ ├── SoftyTheme.class.php │ │ ├── UniversalTheme.class.php │ │ └── VividTheme.class.php │ ├── memcache.php │ ├── mesoeast.php │ ├── mlib.php │ ├── mos_lib.php │ ├── myview.php │ ├── network.php │ ├── reference.php │ ├── rview_lib.php │ ├── sites.php │ ├── station.php │ ├── templates/ │ │ ├── footer.phtml │ │ ├── full.phtml │ │ ├── header.phtml │ │ ├── navbar.phtml │ │ ├── single.phtml │ │ ├── sitebar.phtml │ │ ├── sites.phtml │ │ └── sortables.phtml │ ├── throttle.php │ ├── vendor/ │ │ ├── abraham/ │ │ │ └── twitteroauth/ │ │ │ ├── LICENSE.md │ │ │ ├── autoload.php │ │ │ ├── composer.json │ │ │ └── src/ │ │ │ ├── Config.php │ │ │ ├── Consumer.php │ │ │ ├── HmacSha1.php │ │ │ ├── Request.php │ │ │ ├── Response.php │ │ │ ├── SignatureMethod.php │ │ │ ├── Token.php │ │ │ ├── TwitterOAuth.php │ │ │ ├── TwitterOAuthException.php │ │ │ ├── Util/ │ │ │ │ └── JsonDecoder.php │ │ │ └── Util.php │ │ ├── autoload.php │ │ ├── composer/ │ │ │ ├── ClassLoader.php │ │ │ ├── InstalledVersions.php │ │ │ ├── LICENSE │ │ │ ├── autoload_classmap.php │ │ │ ├── autoload_namespaces.php │ │ │ ├── autoload_psr4.php │ │ │ ├── autoload_real.php │ │ │ ├── autoload_static.php │ │ │ ├── ca-bundle/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── composer.json │ │ │ │ ├── res/ │ │ │ │ │ └── cacert.pem │ │ │ │ └── src/ │ │ │ │ └── CaBundle.php │ │ │ ├── installed.json │ │ │ ├── installed.php │ │ │ └── platform_check.php │ │ ├── erusev/ │ │ │ └── parsedown/ │ │ │ ├── LICENSE.txt │ │ │ ├── Parsedown.php │ │ │ ├── README.md │ │ │ └── composer.json │ │ └── mapscript.php │ └── warnings_plot.php ├── package.json ├── pip_requirements.txt ├── pylib/ │ └── iemweb/ │ ├── GIS/ │ │ ├── __init__.py │ │ └── tiff/ │ │ ├── __init__.py │ │ └── index.py │ ├── __init__.py │ ├── afos/ │ │ ├── __init__.py │ │ └── retrieve.py │ ├── agclimate/ │ │ ├── __init__.py │ │ ├── ames_precip.py │ │ ├── isusm.py │ │ └── nmp_csv.py │ ├── autoplot/ │ │ ├── __init__.py │ │ ├── autoplot.py │ │ ├── barchart.py │ │ ├── gen_qrcode.py │ │ ├── index.py │ │ ├── meta.py │ │ ├── scripts/ │ │ │ ├── __init__.py │ │ │ ├── p1.py │ │ │ ├── p10.py │ │ │ ├── p11.py │ │ │ ├── p12.py │ │ │ ├── p13.py │ │ │ ├── p14.py │ │ │ ├── p15.py │ │ │ ├── p16.py │ │ │ ├── p17.py │ │ │ ├── p18.py │ │ │ ├── p19.py │ │ │ ├── p2.py │ │ │ ├── p20.py │ │ │ ├── p21.py │ │ │ ├── p22.py │ │ │ ├── p23.py │ │ │ ├── p24.py │ │ │ ├── p25.py │ │ │ ├── p26.py │ │ │ ├── p27.py │ │ │ ├── p28.py │ │ │ ├── p29.py │ │ │ ├── p3.py │ │ │ ├── p30.py │ │ │ ├── p31.py │ │ │ ├── p32.py │ │ │ ├── p33.py │ │ │ ├── p34.py │ │ │ ├── p35.py │ │ │ ├── p36.py │ │ │ ├── p37.py │ │ │ ├── p38.py │ │ │ ├── p39.py │ │ │ ├── p4.py │ │ │ ├── p40.py │ │ │ ├── p41.py │ │ │ ├── p42.py │ │ │ ├── p43.py │ │ │ ├── p44.py │ │ │ ├── p45.py │ │ │ ├── p46.py │ │ │ ├── p47.py │ │ │ ├── p48.py │ │ │ ├── p49.py │ │ │ ├── p5.py │ │ │ ├── p50.py │ │ │ ├── p51.py │ │ │ ├── p52.py │ │ │ ├── p53.py │ │ │ ├── p54.py │ │ │ ├── p55.py │ │ │ ├── p56.py │ │ │ ├── p57.py │ │ │ ├── p58.py │ │ │ ├── p59.py │ │ │ ├── p6.py │ │ │ ├── p60.py │ │ │ ├── p61.py │ │ │ ├── p62.py │ │ │ ├── p63.py │ │ │ ├── p64.py │ │ │ ├── p65.py │ │ │ ├── p66.py │ │ │ ├── p67.py │ │ │ ├── p68.py │ │ │ ├── p69.py │ │ │ ├── p7.py │ │ │ ├── p70.py │ │ │ ├── p71.py │ │ │ ├── p72.py │ │ │ ├── p73.py │ │ │ ├── p74.py │ │ │ ├── p75.py │ │ │ ├── p76.py │ │ │ ├── p77.py │ │ │ ├── p78.py │ │ │ ├── p79.py │ │ │ ├── p8.py │ │ │ ├── p80.py │ │ │ ├── p81.py │ │ │ ├── p82.py │ │ │ ├── p83.py │ │ │ ├── p84.py │ │ │ ├── p85.py │ │ │ ├── p86.py │ │ │ ├── p87.py │ │ │ ├── p88.py │ │ │ ├── p89.py │ │ │ ├── p9.py │ │ │ ├── p90.py │ │ │ ├── p91.py │ │ │ ├── p92.py │ │ │ ├── p93.py │ │ │ ├── p94.py │ │ │ ├── p95.py │ │ │ ├── p96.py │ │ │ ├── p97.py │ │ │ ├── p98.py │ │ │ └── p99.py │ │ ├── scripts100/ │ │ │ ├── __init__.py │ │ │ ├── p100.py │ │ │ ├── p101.py │ │ │ ├── p102.py │ │ │ ├── p103.py │ │ │ ├── p104.py │ │ │ ├── p105.py │ │ │ ├── p106.py │ │ │ ├── p107.py │ │ │ ├── p108.py │ │ │ ├── p109.py │ │ │ ├── p110.py │ │ │ ├── p111.py │ │ │ ├── p112.py │ │ │ ├── p113.py │ │ │ ├── p114.py │ │ │ ├── p115.py │ │ │ ├── p116.py │ │ │ ├── p117.py │ │ │ ├── p118.py │ │ │ ├── p119.py │ │ │ ├── p120.py │ │ │ ├── p121.py │ │ │ ├── p122.py │ │ │ ├── p123.py │ │ │ ├── p124.py │ │ │ ├── p125.py │ │ │ ├── p126.py │ │ │ ├── p127.py │ │ │ ├── p128.py │ │ │ ├── p129.py │ │ │ ├── p130.py │ │ │ ├── p131.py │ │ │ ├── p132.py │ │ │ ├── p133.py │ │ │ ├── p134.py │ │ │ ├── p135.py │ │ │ ├── p136.py │ │ │ ├── p137.py │ │ │ ├── p138.py │ │ │ ├── p139.py │ │ │ ├── p140.py │ │ │ ├── p141.py │ │ │ ├── p142.py │ │ │ ├── p143.py │ │ │ ├── p144.py │ │ │ ├── p145.py │ │ │ ├── p146.py │ │ │ ├── p147.py │ │ │ ├── p148.py │ │ │ ├── p149.py │ │ │ ├── p150.py │ │ │ ├── p151.py │ │ │ ├── p152.py │ │ │ ├── p153.py │ │ │ ├── p154.py │ │ │ ├── p155.py │ │ │ ├── p156.py │ │ │ ├── p157.py │ │ │ ├── p158.py │ │ │ ├── p159.py │ │ │ ├── p160.py │ │ │ ├── p161.py │ │ │ ├── p162.py │ │ │ ├── p163.py │ │ │ ├── p164.py │ │ │ ├── p165.py │ │ │ ├── p166.py │ │ │ ├── p167.py │ │ │ ├── p168.py │ │ │ ├── p169.py │ │ │ ├── p170.py │ │ │ ├── p171.py │ │ │ ├── p172.py │ │ │ ├── p173.py │ │ │ ├── p174.py │ │ │ ├── p175.py │ │ │ ├── p176.py │ │ │ ├── p177.py │ │ │ ├── p178.py │ │ │ ├── p179.py │ │ │ ├── p180.py │ │ │ ├── p181.py │ │ │ ├── p182.py │ │ │ ├── p183.py │ │ │ ├── p184.py │ │ │ ├── p185.py │ │ │ ├── p186.py │ │ │ ├── p187.py │ │ │ ├── p188.py │ │ │ ├── p189.py │ │ │ ├── p190.py │ │ │ ├── p191.py │ │ │ ├── p192.py │ │ │ ├── p193.py │ │ │ ├── p194.py │ │ │ ├── p195.py │ │ │ ├── p196.py │ │ │ ├── p197.py │ │ │ ├── p198.py │ │ │ └── p199.py │ │ └── scripts200/ │ │ ├── __init__.py │ │ ├── p200.py │ │ ├── p201.py │ │ ├── p202.py │ │ ├── p203.py │ │ ├── p204.py │ │ ├── p205.py │ │ ├── p206.py │ │ ├── p207.py │ │ ├── p208.py │ │ ├── p209.py │ │ ├── p210.py │ │ ├── p211.py │ │ ├── p212.py │ │ ├── p213.py │ │ ├── p214.py │ │ ├── p215.py │ │ ├── p216.py │ │ ├── p217.py │ │ ├── p218.py │ │ ├── p219.py │ │ ├── p220.py │ │ ├── p221.py │ │ ├── p222.py │ │ ├── p223.py │ │ ├── p224.py │ │ ├── p225.py │ │ ├── p226.py │ │ ├── p227.py │ │ ├── p228.py │ │ ├── p229.py │ │ ├── p230.py │ │ ├── p231.py │ │ ├── p232.py │ │ ├── p233.py │ │ ├── p234.py │ │ ├── p235.py │ │ ├── p236.py │ │ ├── p237.py │ │ ├── p238.py │ │ ├── p239.py │ │ ├── p240.py │ │ ├── p241.py │ │ ├── p242.py │ │ ├── p243.py │ │ ├── p244.py │ │ ├── p245.py │ │ ├── p246.py │ │ ├── p247.py │ │ ├── p248.py │ │ ├── p249.py │ │ ├── p250.py │ │ ├── p251.py │ │ ├── p252.py │ │ ├── p253.py │ │ ├── p254.py │ │ ├── p255.py │ │ ├── p256.py │ │ ├── p257.py │ │ ├── p258.py │ │ ├── p259.py │ │ ├── p260.py │ │ ├── p261.py │ │ └── p262.py │ ├── c/ │ │ ├── __init__.py │ │ ├── tile.py │ │ └── tilecache.cfg │ ├── cache/ │ │ ├── __init__.py │ │ ├── tile.py │ │ └── tilecache.cfg │ ├── climate/ │ │ ├── __init__.py │ │ └── orc.py │ ├── current/ │ │ ├── __init__.py │ │ └── live.py │ ├── dispatch.py │ ├── fields.py │ ├── geocoder.py │ ├── geojson/ │ │ ├── __init__.py │ │ ├── agclimate.py │ │ ├── cf6.py │ │ ├── cli.py │ │ ├── climodat_dayclimo.py │ │ ├── convective_sigmet.py │ │ ├── coopobs.py │ │ ├── lsr.py │ │ ├── network.py │ │ ├── networks.py │ │ ├── nexrad_attr.py │ │ ├── recent_metar.py │ │ ├── sbw.py │ │ ├── sbw_county_intersect.py │ │ ├── seven_am.py │ │ ├── sps.py │ │ ├── station_neighbors.py │ │ ├── usdm.py │ │ ├── vtec_event.py │ │ ├── webcam.py │ │ └── winter_roads.py │ ├── getweather.py │ ├── iemre/ │ │ ├── __init__.py │ │ ├── daily.py │ │ ├── hourly.py │ │ └── multiday.py │ ├── json/ │ │ ├── __init__.py │ │ ├── cf6.py │ │ ├── cli.py │ │ ├── cli_audit.py │ │ ├── climodat_dd.py │ │ ├── climodat_stclimo.py │ │ ├── current.py │ │ ├── dcp_vars.py │ │ ├── ibw_tags.py │ │ ├── mcd_bysize.py │ │ ├── network.py │ │ ├── nwstext.py │ │ ├── nwstext_center_date.py │ │ ├── nwstext_search.py │ │ ├── outlook_progression.py │ │ ├── prism.py │ │ ├── products.py │ │ ├── radar.py │ │ ├── raob.py │ │ ├── reference.py │ │ ├── ridge_current.py │ │ ├── sbw_by_point.py │ │ ├── snowfall_observations_v2.py │ │ ├── spc_bysize.py │ │ ├── spcmcd.py │ │ ├── spcoutlook.py │ │ ├── spcwatch.py │ │ ├── sps_by_point.py │ │ ├── stage4.py │ │ ├── state_ugc.py │ │ ├── stations.py │ │ ├── tms.py │ │ ├── vtec_emergencies.py │ │ ├── vtec_event.py │ │ ├── vtec_events.py │ │ ├── vtec_events_bypoint.py │ │ ├── vtec_events_bystate.py │ │ ├── vtec_events_byugc.py │ │ ├── vtec_events_bywfo.py │ │ ├── vtec_max_etn.py │ │ ├── vtec_pds.py │ │ ├── watches.py │ │ ├── webcam.py │ │ ├── webcams.py │ │ ├── wpcmpd.py │ │ └── wpcoutlook.py │ ├── metadata/ │ │ ├── __init__.py │ │ └── xml/ │ │ ├── __init__.py │ │ ├── pl.py │ │ └── sd.py │ ├── mlib.py │ ├── mywindrose.py │ ├── nws/ │ │ ├── __init__.py │ │ └── debug_latlon/ │ │ ├── __init__.py │ │ └── generate_plot.py │ ├── onsite/ │ │ ├── __init__.py │ │ └── features/ │ │ ├── __init__.py │ │ ├── content.py │ │ └── vote.py │ ├── precip/ │ │ ├── __init__.py │ │ ├── catAZOS.py │ │ └── catSNET.py │ ├── projects/ │ │ ├── __init__.py │ │ └── iembot/ │ │ ├── __init__.py │ │ └── mastodon/ │ │ ├── __init__.py │ │ └── index.py │ ├── proxy_error_handler.py │ ├── rainfall/ │ │ ├── __init__.py │ │ └── mrms2img.py │ ├── request/ │ │ ├── __init__.py │ │ ├── asos.py │ │ ├── asos1min.py │ │ ├── coop.py │ │ ├── coopobs.py │ │ ├── daily.py │ │ ├── feel.py │ │ ├── gis/ │ │ │ ├── __init__.py │ │ │ ├── awc_gairmets.py │ │ │ ├── cwas.py │ │ │ ├── lsr.py │ │ │ ├── misc.py │ │ │ ├── nexrad_storm_attrs.py │ │ │ ├── pireps.py │ │ │ ├── sigmets.py │ │ │ ├── spc_mcd.py │ │ │ ├── spc_outlooks.py │ │ │ ├── spc_watch.py │ │ │ ├── sps.py │ │ │ ├── watch_by_county.py │ │ │ ├── watchwarn.py │ │ │ └── wpc_mpd.py │ │ ├── grx/ │ │ │ ├── __init__.py │ │ │ ├── iadot_trucks.py │ │ │ ├── l3attr.py │ │ │ └── time_mot_loc.py │ │ ├── grx_rings.py │ │ ├── hads.py │ │ ├── hml.py │ │ ├── hourlyprecip.py │ │ ├── isusm.py │ │ ├── maxcsv.py │ │ ├── metars.py │ │ ├── mos.py │ │ ├── nass_iowa.py │ │ ├── nlaeflux.py │ │ ├── normals.py │ │ ├── other.py │ │ ├── purpleair.py │ │ ├── raob.py │ │ ├── raster2netcdf.py │ │ ├── rwis.py │ │ ├── scan.py │ │ ├── scp.py │ │ ├── smos.py │ │ ├── ss.py │ │ ├── taf.py │ │ ├── talltowers.py │ │ ├── tempwind_aloft.py │ │ ├── uscrn.py │ │ └── wmo_bufr_srf.py │ ├── search.py │ ├── sites/ │ │ ├── __init__.py │ │ └── test.py │ ├── tilecache_dispatch.py │ ├── util.py │ ├── vtec/ │ │ ├── __init__.py │ │ ├── f.py │ │ └── index.py │ └── wx/ │ ├── __init__.py │ └── afos/ │ ├── __init__.py │ └── text2png.py ├── pyproject.toml ├── scripts/ │ ├── 00z/ │ │ ├── asos_high.py │ │ └── generate_rtp.py │ ├── 12z/ │ │ ├── asos_low.py │ │ └── generate_rtp.py │ ├── GIS/ │ │ ├── 24h_lsr.py │ │ ├── attribute2shape.py │ │ ├── current_ww.shp.xml │ │ └── wwa2shp.py │ ├── RUN_0Z.sh │ ├── RUN_0Z_ERA5LAND.sh │ ├── RUN_10MIN.sh │ ├── RUN_10_AFTER.sh │ ├── RUN_12Z.sh │ ├── RUN_1MIN.sh │ ├── RUN_20MIN.sh │ ├── RUN_20_AFTER.sh │ ├── RUN_2AM.sh │ ├── RUN_40_AFTER.sh │ ├── RUN_50_AFTER.sh │ ├── RUN_59_AFTER.sh │ ├── RUN_5MIN.sh │ ├── RUN_CLIMODAT_STATE.sh │ ├── RUN_COOP.sh │ ├── RUN_HRRR_REF.sh │ ├── RUN_MIDNIGHT.sh │ ├── RUN_NOON.sh │ ├── RUN_STAGE4.sh │ ├── RUN_SUMMARY.sh │ ├── asos/ │ │ ├── adjust_report_type.py │ │ ├── cf6_to_iemaccess.py │ │ ├── iem_scraper_example.py │ │ ├── iem_scraper_example2.py │ │ └── use_acis.py │ ├── cache/ │ │ ├── download_cpc.sh │ │ ├── midwest_winter_roads.py │ │ ├── nws_wawa_archive.py │ │ └── warn_cache.py │ ├── climate/ │ │ ├── today_hilo.py │ │ └── today_rec_hilo.py │ ├── climodat/ │ │ ├── avg_temp.py │ │ ├── check_database.py │ │ ├── compute4regions.py │ │ ├── compute_climate.py │ │ ├── daily_estimator.py │ │ ├── era5land_extract.py │ │ ├── estimate_missing.py │ │ ├── hrrr_solarrad.py │ │ ├── ks_monthly.py │ │ ├── ks_yearly.py │ │ ├── merra_solarrad.py │ │ ├── narr_solarrad.py │ │ ├── nldas_extract.py │ │ ├── power_extract.py │ │ ├── precip_days.py │ │ ├── run.sh │ │ ├── sync_coop_updates.py │ │ ├── use_acis.py │ │ └── yearly_precip.py │ ├── coop/ │ │ ├── PREC.sh │ │ ├── cfs_extract.py │ │ ├── data.desc │ │ ├── day_precip.py │ │ ├── email_rr3_to_harry.py │ │ ├── extract_coop_obs.py │ │ ├── extract_idhs.py │ │ ├── first_guess_for_harry.py │ │ ├── month_precip.py │ │ ├── ndfd_extract.py │ │ ├── plot_coop.py │ │ ├── plot_precip_12z.py │ │ ├── today_precip.py │ │ ├── use_acis.py │ │ └── year_precip.py │ ├── crontab │ ├── current/ │ │ ├── ifc_today_total.py │ │ ├── lsr_snow_mapper.py │ │ ├── mrms_today_total.py │ │ ├── plot_hilo.py │ │ ├── q3_xhour.py │ │ ├── rwis_station.py │ │ ├── stage4_hourly.py │ │ ├── stage4_today_total.py │ │ ├── stage4_xhour.py │ │ ├── temperature.py │ │ ├── today_gust.py │ │ ├── today_high.py │ │ ├── today_min_windchill.py │ │ ├── today_precip.py │ │ └── vsby.py │ ├── dailyb/ │ │ ├── spammer.py │ │ └── wwa.py │ ├── dbutil/ │ │ ├── SYNC_STATIONS.sh │ │ ├── add_iem_data_entry.py │ │ ├── asos2archive.py │ │ ├── clean_afos.py │ │ ├── clean_mos.py │ │ ├── clean_telemetry.py │ │ ├── clean_unknown_hads.py │ │ ├── compute_alldata_sts.py │ │ ├── compute_climate_sts.py │ │ ├── compute_cocorahs_sts.py │ │ ├── compute_coop_sts.py │ │ ├── compute_hads_sts.py │ │ ├── compute_isusm_sts.py │ │ ├── compute_network_extent.py │ │ ├── compute_rwis_sts.py │ │ ├── delete_station.py │ │ ├── hads_delete_dups.py │ │ ├── mine_autoplot.py │ │ ├── network_timezone.py │ │ ├── rwis2archive.py │ │ ├── set_attribute_phour.py │ │ ├── set_climate.py │ │ ├── set_county.py │ │ ├── set_elevation.py │ │ ├── set_timezone.py │ │ ├── set_wfo.py │ │ ├── sync_stations.py │ │ ├── unknown_stations.py │ │ ├── uscrn.py │ │ ├── xcheck_SFQ.py │ │ └── xcheck_madis.py │ ├── dl/ │ │ ├── archive_composite.py │ │ ├── download_cfs.py │ │ ├── download_ffg.py │ │ ├── download_gfs.py │ │ ├── download_hrrr_rad.py │ │ ├── download_hrrr_tsoil.py │ │ ├── download_imerg.py │ │ ├── download_nam.py │ │ ├── download_narr.py │ │ ├── download_ndfd.py │ │ ├── download_rtma_ru.py │ │ ├── fetch_merra.py │ │ ├── fetch_power.py │ │ ├── ncep_stage4.py │ │ └── radar_composite.py │ ├── era5/ │ │ ├── fetch_era5.py │ │ └── init_hourly.py │ ├── gfs/ │ │ ├── gfs2csv.py │ │ ├── gfs2iemre.py │ │ └── gfs_4inch.py │ ├── gs/ │ │ └── plot_gdd.py │ ├── hads/ │ │ ├── assign_has_hml.py │ │ ├── compute_hads_pday.py │ │ ├── compute_hads_phour.py │ │ ├── dedup_hml_forecasts.py │ │ ├── process_hads_inbound.py │ │ ├── process_nwps_stages.py │ │ ├── raw2obs.py │ │ └── sync_idpgis.py │ ├── hrrr/ │ │ ├── dl_hrrrref.py │ │ ├── hrrr_jobs.py │ │ ├── hrrr_ref2raster.py │ │ └── plot_ref.py │ ├── iemplot/ │ │ ├── IAMESONET_plot.csh │ │ ├── RUN.csh │ │ ├── coltbl.xwp │ │ ├── dump_altm.py │ │ ├── oa.csh │ │ ├── pres_plot.csh │ │ └── templates/ │ │ ├── createFile.csh │ │ ├── grid_25_25.grd │ │ ├── grid_50_50.grd │ │ ├── sf.pack │ │ ├── surface.gem │ │ └── use.stns │ ├── iemre/ │ │ ├── README.md │ │ ├── daily_analysis.py │ │ ├── db_to_netcdf.py │ │ ├── grid_climate.py │ │ ├── grid_climate_ifc.py │ │ ├── grid_climate_stage4.py │ │ ├── grid_rsds.py │ │ ├── hourly_analysis.py │ │ ├── ingest_nohrsc.py │ │ ├── init_daily.py │ │ ├── init_daily_ifc.py │ │ ├── init_dailyc.py │ │ ├── init_hourly.py │ │ ├── init_ifc_dailyc.py │ │ ├── init_narr.py │ │ ├── init_stage4_daily.py │ │ ├── init_stage4_dailyc.py │ │ ├── init_stage4_hourly.py │ │ ├── merge_ifc.py │ │ ├── merge_narr.py │ │ ├── por_dailyc.py │ │ ├── precip_ingest.py │ │ ├── prism_adjust_stage4.py │ │ ├── stage4_12z_adjust.py │ │ ├── use_icon.py │ │ └── use_ifs.py │ ├── ingestors/ │ │ ├── asos_1minute/ │ │ │ ├── p1_examples.txt │ │ │ ├── p2_examples.txt │ │ │ └── parse_ncei_asos1minute.py │ │ ├── awos/ │ │ │ └── parse_monthly_maint.py │ │ ├── cocorahs/ │ │ │ ├── cocorahs_data_ingest.py │ │ │ └── cocorahs_stations.py │ │ ├── dot_plows.py │ │ ├── dot_truckcams.py │ │ ├── dotcams/ │ │ │ ├── ingest_dot_webcams.py │ │ │ └── sync_roadcam_meta.py │ │ ├── elnino.py │ │ ├── flux_ingest.py │ │ ├── ifc/ │ │ │ └── ingest_ifc_precip.py │ │ ├── isusm/ │ │ │ ├── ingest_isusm.py │ │ │ └── run_ingest_isusm.sh │ │ ├── madis/ │ │ │ ├── extract_hfmetar.py │ │ │ ├── extract_madis.py │ │ │ ├── extract_metar.py │ │ │ ├── sync_stations.py │ │ │ └── to_iemaccess.py │ │ ├── ncei/ │ │ │ ├── 91_20/ │ │ │ │ ├── ingest.py │ │ │ │ └── merge_stations.py │ │ │ ├── ingest_fisherporter.py │ │ │ ├── ingest_isd.py │ │ │ ├── run_network_isd_ingest.py │ │ │ └── xcheck_ghcn_stations.py │ │ ├── onewire.py │ │ ├── other/ │ │ │ ├── feel_ingest.py │ │ │ ├── parse0006.py │ │ │ ├── parse0010.py │ │ │ └── purpleair.py │ │ ├── parse0002.py │ │ ├── rwis/ │ │ │ ├── process_rwis.py │ │ │ ├── process_rwis_dtn.py │ │ │ ├── process_soil.py │ │ │ └── process_traffic.py │ │ ├── soilm_ingest.py │ │ ├── squaw/ │ │ │ └── ingest_squaw.py │ │ └── uscrn_ingest.py │ ├── isuag/ │ │ └── fix_temps.py │ ├── isusm/ │ │ ├── agg_1minute.py │ │ ├── agg_precip.py │ │ ├── archiver.py │ │ ├── backfill_summary.py │ │ ├── csv2ldm.py │ │ ├── fancy_4inch.py │ │ ├── fix_high_low.py │ │ ├── fix_precip.py │ │ ├── fix_soil4t.py │ │ ├── fix_solar.py │ │ ├── isusm2rr5.py │ │ ├── nmp_monthly_email.py │ │ ├── run_plots.sh │ │ ├── zap_temp_humid.py │ │ └── zero_daily_precip.py │ ├── model/ │ │ └── motherlode_ingest.py │ ├── month/ │ │ ├── obs_precip.py │ │ ├── obs_precip_coop.py │ │ ├── plot_avgt.py │ │ ├── plot_gdd.py │ │ └── plot_sdd.py │ ├── mos/ │ │ └── current_bias.py │ ├── mrms/ │ │ ├── README.md │ │ ├── copy_daily_24h.py │ │ ├── gr2ae.txt │ │ ├── init_daily_mrms.py │ │ ├── init_mrms_dailyc.py │ │ ├── make_mrms_rasters.py │ │ ├── merge_mrms_q3.py │ │ ├── mesh_contours.py │ │ ├── mrms_lcref_comp.py │ │ ├── mrms_monthly_plot.py │ │ └── mrms_rainrate_comp.py │ ├── nass/ │ │ ├── ingest_iowa_pdf.py │ │ └── nass_quickstats.py │ ├── ncei/ │ │ └── ingest_climdiv.py │ ├── ndfd/ │ │ ├── grid_climate_ndfd.py │ │ ├── init_ndfd_dailyc.py │ │ ├── ndfd2iemre.py │ │ ├── ndfd2netcdf.py │ │ └── plot_temps.py │ ├── nldas/ │ │ ├── init_hourly.py │ │ └── process_nldasv2_noah.py │ ├── other/ │ │ ├── ot2archive.py │ │ └── update_daily_srad.py │ ├── outgoing/ │ │ └── madis2csv.py │ ├── plots/ │ │ ├── ARX_overlay.csh │ │ ├── ASOS_plot.csh │ │ ├── DEWPS_plot.csh │ │ ├── DMX_overlay.csh │ │ ├── DVN_overlay.csh │ │ ├── EAX_overlay.csh │ │ ├── FSD_overlay.csh │ │ ├── HEAT_plot.csh │ │ ├── HOURLY_PLOTS │ │ ├── MPX_overlay.csh │ │ ├── MW_mesonet.csh │ │ ├── OAX_overlay.csh │ │ ├── RELH_plot.csh │ │ ├── RUN_PLOTS │ │ ├── SDMESONET_plot.csh │ │ ├── TEMPS_plot.csh │ │ ├── WCHT_plot.csh │ │ ├── WINDS_plot.csh │ │ ├── black/ │ │ │ └── surfaceContours.csh │ │ ├── coltbl.xwp │ │ ├── createGrids.csh │ │ ├── plot_rwis_sf.py │ │ └── radar.tbl │ ├── prism/ │ │ ├── README.md │ │ ├── grid_climate_prism.py │ │ ├── ingest_prism.py │ │ ├── init_daily.py │ │ └── init_prism_dailyc.py │ ├── qc/ │ │ ├── check_afos.py │ │ ├── check_awos_online.py │ │ ├── check_isusm_online.py │ │ ├── check_n0q.py │ │ ├── check_station_geom.py │ │ ├── check_vtec_eventids.py │ │ └── check_webcams.py │ ├── ridge/ │ │ ├── ReflectivityColorCurveManager.xml │ │ └── ZDRColorCurveManager.xml │ ├── roads/ │ │ ├── archive_roadsplot.py │ │ ├── check_roads_geom.py │ │ ├── ingest_roads_rest.py │ │ ├── init_roads_database.py │ │ └── merge_roads.py │ ├── rtma/ │ │ ├── rtma_backfill.py │ │ └── wind_power.py │ ├── sbw/ │ │ ├── compute_shared_border_pct.py │ │ └── raccoon_sbw_to_ppt.py │ ├── scan/ │ │ ├── init_stations.py │ │ └── scan_ingest.py │ ├── season/ │ │ ├── plot_4month_stage4.py │ │ └── plot_cli_jul1_snow.py │ ├── smos/ │ │ ├── ingest_smos.py │ │ └── plot.py │ ├── summary/ │ │ ├── compute_daily.py │ │ ├── hourly_precip.py │ │ ├── max_reflect.py │ │ └── update_dailyrain.py │ ├── swat/ │ │ └── swat_realtime.py │ ├── ua/ │ │ ├── compute_params.py │ │ ├── compute_sts_ets.py │ │ ├── igra2_ingest.py │ │ └── ingest_from_spc.py │ ├── uscrn/ │ │ └── compute_uscrn_pday.py │ ├── usdm/ │ │ └── process_usdm.py │ ├── util/ │ │ ├── list_stale_autoplots.py │ │ ├── make_archive_baseline.py │ │ ├── monthly.sh │ │ ├── pick_state.py │ │ ├── poker2afos.py │ │ └── set_iemdb_etc_hosts.py │ ├── webalizer/ │ │ ├── agclimate.conf │ │ ├── combine_logs.py │ │ ├── datateam.conf │ │ ├── depbackend.conf │ │ ├── mesonet.conf │ │ ├── processlogs.sh │ │ ├── sustainablecorn.conf │ │ └── weatherim.conf │ ├── week/ │ │ └── plot_obs.py │ ├── windrose/ │ │ ├── daily_drive_network.py │ │ ├── drive_network_windrose.py │ │ └── make_windrose.py │ ├── year/ │ │ ├── plot_gdd.py │ │ ├── plot_stage4.py │ │ └── precip.py │ └── yieldfx/ │ ├── README.md │ ├── baseline/ │ │ ├── ames.met │ │ ├── cobs.met │ │ ├── crawfordsville.met │ │ ├── kanawha.met │ │ ├── lewis.met │ │ ├── mcnay.met │ │ ├── muscatine.met │ │ ├── nashua.met │ │ └── sutherland.met │ ├── baseline2db.py │ ├── cfs2iemre_netcdf.py │ ├── cfs_tiler.py │ ├── cfs_tiler_lastyear.py │ ├── counties.csv │ ├── county_csv.py │ ├── dump_hybridmaize.py │ ├── dumpbaseline.py │ ├── psims_baseline.py │ └── yieldfx_workflow.py ├── src/ │ ├── iemjs/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── domUtils.d.ts │ │ │ ├── domUtils.js │ │ │ ├── iemdata.d.ts │ │ │ ├── iemdata.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ └── tests/ │ │ ├── runner.js │ │ ├── test-exports.js │ │ └── test-imports.js │ └── release-iemjs.sh ├── tests/ │ ├── iemweb/ │ │ ├── autoplot/ │ │ │ ├── test_api.py │ │ │ ├── test_barchar.py │ │ │ ├── test_extweb_failures.py │ │ │ ├── test_index.py │ │ │ ├── test_meta.py │ │ │ ├── test_one_offs.py │ │ │ ├── test_urls.py │ │ │ ├── urllist.txt │ │ │ └── urllist_index.txt │ │ ├── current/ │ │ │ └── test_live.py │ │ ├── geojson/ │ │ │ ├── test_geojson_index.py │ │ │ └── test_sbw.py │ │ ├── iembot/ │ │ │ └── test_mastodon.py │ │ ├── json/ │ │ │ ├── test_json_index.py │ │ │ ├── test_radar.py │ │ │ └── test_spcoutlook.py │ │ ├── onsite/ │ │ │ └── test_features.py │ │ ├── request/ │ │ │ ├── gis/ │ │ │ │ ├── test_lsr.py │ │ │ │ └── test_watchwarn.py │ │ │ ├── test_asos.py │ │ │ ├── test_coop.py │ │ │ ├── test_maxcsv.py │ │ │ └── test_metars.py │ │ ├── test_afos.py │ │ ├── test_all_application.py │ │ ├── test_dispatch.py │ │ ├── test_doc_urls.py │ │ ├── test_fields.py │ │ ├── test_iemweb_urls.py │ │ ├── test_module.py │ │ ├── test_proxy_error_handler.py │ │ ├── test_util.py │ │ ├── urls.txt │ │ ├── urls422.txt │ │ └── vtec/ │ │ └── test_vtec.py │ ├── run_feature_autoplots.py │ ├── run_mapserver.sh │ ├── stress_tilecache.py │ ├── test_mod_wsgi.py │ ├── test_php.py │ ├── urls.txt │ └── urls405.txt └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .deepsource.toml ================================================ version = 1 test_patterns = ["**/tests/**"] exclude_patterns = [ "include/vendor/**", "include/jpgraph/**", ] [[analyzers]] name = "shell" [[analyzers]] name = "javascript" [analyzers.meta] environment = ["browser"] [[analyzers]] name = "python" [analyzers.meta] runtime_version = "3.x.x" max_line_length = 188 [[analyzers]] name = "php" [analyzers.meta] bootstrap_files = ["include/dbase.stub.php", "config/settings.inc.php.in"] ================================================ FILE: .editorconfig ================================================ # EditorConfig helps maintain consistent coding styles between editors and IDEs. # See https://editorconfig.org # Root marker root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true # Let linters/formatters (ruff, eslint) enforce line length max_line_length = off [*.{py,php,js,jsx,ts,tsx,cjs,mjs,sh,bash}] indent_style = space indent_size = 4 tab_width = 4 [*.{yml,yaml}] indent_style = space indent_size = 2 [*.{json,jsonc}] indent_style = space indent_size = 2 [*.md] # Preserve intentional double-space line breaks and trailing spaces in tables trim_trailing_whitespace = false insert_final_newline = true [Makefile] # Tabs are required for make recipes indent_style = tab [Dockerfile] indent_style = space indent_size = 4 [*.{ini,conf}] indent_style = space indent_size = 4 ================================================ FILE: .github/ci_db_testdata.py ================================================ """Update The Test Database. The test database instance for CI has static data for testing. This is static from the time of the docker image build. This script runs from GHA and does some mucking with the database to improve coverage. """ from pyiem.database import sql_helper, with_sqlalchemy_conn from pyiem.network import Table as NetworkTable from pyiem.util import logger from sqlalchemy.engine import Connection LOG = logger() @with_sqlalchemy_conn("isuag") def create_realtime_isuag(conn: Connection | None = None) -> None: """Create the realtime table if needed.""" nt = NetworkTable("ISUSM") for sid in nt.sts: conn.execute( sql_helper(""" insert into sm_minute (station, valid, tair_c_avg_qc, sv_t2_qc, sv_vwc2_qc) values (:sid, now(), :tmpc, :tmpc, :vwc)"""), { "sid": sid, "tmpc": None if sid == "AHDI4" else 20.0, "vwc": None if sid in ["AMFI4", "AHDI4"] else 0.2, }, ) conn.execute( sql_helper(""" insert into sm_daily (station, valid, tair_c_max_qc) values (:sid, now(), :tmpc)"""), {"sid": sid, "tmpc": 20.0}, ) if sid in ["CRFI4", "BOOI4", "CAMI4"]: conn.execute( sql_helper(""" insert into sm_inversion (station, valid, tair_15_c_avg_qc) values (:sid, now(), :tmpc)"""), {"sid": sid, "tmpc": 20.0}, ) conn.commit() @with_sqlalchemy_conn("iem") def create_iemaccess_isuag(conn: Connection | None = None) -> None: """Create the realtime table if needed.""" nt = NetworkTable("ISUSM") for sid in nt.sts: conn.execute( sql_helper(""" insert into current (iemid, valid, tmpf) values (:iemid, now(), :tmpf)"""), { "iemid": nt.sts[sid]["iemid"], "tmpf": 20.0 if sid[2] > "F" else None, }, ) conn.commit() @with_sqlalchemy_conn("id3b") def ldm_product_log(conn: Connection | None = None) -> None: """Update these to the future.""" conn.execute( sql_helper(""" update ldm_product_log SET entered_at = now() - ('2024-12-03 19:30+00'::timestamptz - entered_at), valid_at = now() - ('2024-12-03 19:30+00'::timestamptz - valid_at), wmo_valid_at = now() - ('2024-12-03 19:30+00'::timestamptz - wmo_valid_at) where entered_at between '2024-12-03 16:00+00' and '2024-12-03 20:00+00' """) ) conn.commit() @with_sqlalchemy_conn("radar") def nexrad_attributes(conn: Connection | None = None) -> None: """Update these to be current.""" res = conn.execute( sql_helper("update nexrad_attributes SET valid = now()") ) LOG.warning("Updated %s nexrad_attributes to current time", res.rowcount) conn.commit() def main(): """Go Main.""" ldm_product_log() nexrad_attributes() create_realtime_isuag() create_iemaccess_isuag() if __name__ == "__main__": main() ================================================ FILE: .github/copilot-instructions.md ================================================ # IEM Repo Every time you choose to apply a rule(s), explicitly state the rule(s) in the output. You can abbreviate the rule description to a single word or phrase. ## Code Stack - PHP 8.4 - Tabulator JavaScript library for interactive tables - Python 3.14 - PostgreSQL 18 - Bootstrap 5 - OpenLayers for interactive maps ## Code Organization and Abstractions - Before creating new utility functions, check if similar functionality exists in `/htdocs/js/iemjs/` or `/src/iemjs/`. - When refactoring for complexity, prefer creating reusable abstractions in `/htdocs/js/iemjs/` or `/src/iemjs/`. - Look for patterns that appear across multiple modules and extract them to shared utilities. - When fixing ESLint complexity warnings, prioritize creating shared utilities over module-specific helper functions. - Always search the codebase for similar patterns before creating new functions. - Common patterns that should be abstracted: - Form validation and input handling - URL parameter parsing/updating and hash migration - API request handling with error management and loading states - DOM element selection and manipulation beyond what's in domUtils.js - Date/time formatting and parsing utilities - Table/data processing and formatting - Keyboard navigation and event handling patterns ## Refactoring Strategy - When asked to "address complexity", first analyze if the complexity comes from: 1. **Reusable logic** → extract to shared utilities in `/src/iemjs/` 2. **Module-specific orchestration** → break into smaller, focused functions 3. **Repetitive patterns** → create abstractions that multiple modules can use - Before creating 3+ similar helper functions across different modules, consolidate into a shared utility. - When creating shared utilities, design them to handle the most common use cases across the codebase, not just the immediate problem. **Note**: This abstraction strategy applies only to ES modules (files using `import`/`export`). Legacy JavaScript files should keep complexity-reducing functions within their own modules. ## Rules - It is not acceptable to rewrite a file by creating a new file and then overwriting the original file with the new file. Instead, you should edit the original file directly. - Jquery should not be used and any instances of it should be replaced with vanilla JavaScript. - Code comments should explain functionality, not detail why the code was added. - JavaScript code should not be embedded in HTML files. - Jquery-UI should not be used and any instances of it should be replaced with vanilla JavaScript. - Avoid usage of `this` in JavaScript code, as it can lead to confusion and bugs. Use arrow functions or bind methods to the correct context instead. - When you suggest URLs to open for testing, you should use the domain name of `iem.local` instead of `localhost`. HTTPS is required. ## ESLint Usage - **ALWAYS use `npx eslint ` for linting individual files.** - Use the direct ESLint command to get accurate, file-specific linting results. ## Project Context This repo does a lot of different things with weather data modification. ## Code Style and Structure ```text cgi-bin/ # One line front end references to pylib application code config/ # PHP configuration data/ # Stuff used by PHP and python scripts deployment/ # Stuff associated with deployment of this code docs/ # centralized docs htdocs/ # The apache webroot with mostly PHP stuff and python pointers # to things within pylib ├── agclimate/ # ISU Soil Moisture Network include/ # PHP include scripts pylib/ # python library stuff used within this repo only scripts/ # python cron jobs that process data, these are not web accessible src/iemjs/ # Centralized `iemjs` npm ES package, used by this repo and friends. # This is where new utility functions should be added. tests/ # Python testing code mostly for pylib and for integration tests ``` ## Code Quality and Technical Debt - Avoid creating many small, single-purpose functions when a more general utility would serve multiple modules. - When fixing the same type of complexity issue across multiple files, step back and create a shared solution. - If you find yourself writing similar JSDoc comments across different modules, that's a signal the functionality should be abstracted. - Consider the maintenance burden: 10 lines of shared utility code is better than 50 lines of duplicated helper functions across 5 modules. ## Decision Making Process - When approaching complexity refactoring: 1. First, semantic_search for similar patterns in other modules 2. Check existing utilities in `/src/iemjs/` and `/htdocs/js/iemjs/` 3. If pattern exists 2+ times, create shared utility 4. If truly module-specific, create focused helper functions 5. Document the decision rationale in comments ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" labels: - "Bot" groups: github-actions: patterns: - '*' - package-ecosystem: "npm" directory: "/" schedule: interval: "monthly" labels: - "Bot" groups: npm: patterns: - '*' ================================================ FILE: .github/ms_environment.yml ================================================ dependencies: - mapserver ================================================ FILE: .github/setupdata.sh ================================================ # Ensure we error out set -x -e # Paths are setup in setuppaths.sh python .github/ci_db_testdata.py python scripts/dbutil/sync_stations.py python scripts/mrms/init_daily_mrms.py --year=2024 python scripts/mrms/init_mrms_dailyc.py python scripts/prism/init_daily.py --year=2024 python scripts/iemre/init_daily.py --year=2022 --domain=conus python scripts/iemre/init_daily.py --year=2023 --ci python scripts/iemre/init_daily.py --year=2024 --domain=conus python scripts/iemre/init_daily.py --year=2024 --domain=europe python scripts/iemre/init_daily.py --year=2024 --domain=sa python scripts/iemre/init_hourly.py --year=2023 --ci python scripts/iemre/init_dailyc.py python scripts/iemre/init_stage4_hourly.py --year=2024 --ci python scripts/iemre/init_stage4_daily.py --year=2024 --ci python scripts/iemre/init_daily_ifc.py --year=2024 --ci curl -o /mesonet/share/features/2022/03/220325.png \ https://mesonet.agron.iastate.edu/onsite/features/2022/03/220325.png curl -o /mesonet/ldmdata/gis/images/4326/USCOMP/n0q_0.json \ https://mesonet.agron.iastate.edu/data/gis/images/4326/USCOMP/n0q_0.json curl -o /mesonet/ldmdata/gis/images/4326/USCOMP/n0r_0.json \ https://mesonet.agron.iastate.edu/data/gis/images/4326/USCOMP/n0r_0.json curl -o /mesonet/share/pickup/yieldfx/ames.met \ https://mesonet.agron.iastate.edu/pickup/yieldfx/ames.met curl -o /mesonet/data/iemre/ndfd_current.nc \ https://mesonet.agron.iastate.edu/onsite/iemre/ndfd_current.nc curl -o /mesonet/data/iemre/gfs_current.nc \ https://mesonet.agron.iastate.edu/onsite/iemre/gfs_current.nc curl -o /opt/iem/htdocs/vtec/assets.json \ https://mesonet.agron.iastate.edu/vtec/assets.json # A corrupted RTMA file mkdir -p /mesonet/ARCHIVE/data/2024/01/01/model/rtma/00 echo > /mesonet/ARCHIVE/data/2024/01/01/model/rtma/00/rtma2p5_ru.t0000z.2dvaranl_ndfd.grb2 # A corrupted HRRR refd file mkdir -p /mesonet/ARCHIVE/data/2024/01/01/model/hrrr/00 echo > /mesonet/ARCHIVE/data/2024/01/01/model/hrrr/00/hrrr.t00z.refd.grib2 ================================================ FILE: .github/setuppaths.sh ================================================ set -e -x # Only path setups here, adding data handled in setupdata.sh sudo ln -s `pwd` /opt/iem # Kind of hacky, but that is what daryl does # needed by all kinds of things # /mesonet was setup in ci_tooling mkdir -p /mesonet/ldmdata/gis/images/4326/USCOMP mkdir -p /mesonet/share/pickup/yieldfx mkdir -p /mesonet/share/features/2022/03 mkdir _webtmp sudo ln -s `pwd`/_webtmp /var/webtmp ================================================ FILE: .github/workflows/build.yml ================================================ name: IEM CI on: pull_request: branches: - main push: branches: - main jobs: build: defaults: run: # Ensures environment gets sourced right shell: bash -l -e {0} name: Python (${{ matrix.PYTHON_VERSION }}) Data (${{ matrix.WITH_TEST_DATA }}) Test Web (${{ matrix.TEST_WEB }}) runs-on: ubuntu-latest strategy: matrix: PYTHON_VERSION: ["3.14"] WITH_TEST_DATA: ["test_data", "no_test_data"] TEST_WEB: ["YES", "NO"] exclude: # This combination is not all that useful - PYTHON_VERSION: "3.14" TEST_WEB: "NO" WITH_TEST_DATA: "no_test_data" env: PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} WITH_TEST_DATA: ${{ matrix.WITH_TEST_DATA }} TEST_WEB: ${{ matrix.TEST_WEB }} steps: - uses: actions/checkout@v6 # Lots of daryl's codes use aliases defined in /etc/hosts - name: Add /etc/hosts entries run: | cat .github/workflows/etchosts.txt | sudo tee -a /etc/hosts # Required for all matrix jobs - name: Create Docker Network run: docker network create iem_network # Required for all matrix jobs - name: Run IEM Database Container label:${{ env.WITH_TEST_DATA }} run: | docker run -d --name iem_database --network iem_network -p 5432:5432 ghcr.io/akrherz/iem_database:${{ env.WITH_TEST_DATA }} n=0 until docker exec iem_database pg_isready -h localhost; do n=$((n+1)) if [ $n -ge 10 ]; then echo "iem_database did not become ready in time" docker logs iem_database exit 1 fi sleep 6 done # Required for all matrix jobs - name: Run IEM Web Services Container run: | docker run -d --name iem_web_services --network iem_network -p 8080:8000 -e IEMWS_DBHOST=iem_database -e IEMWS_DBUSER=mesonet ghcr.io/akrherz/iem-web-services:latest n=0 # Test something that does a db query until curl -sf http://localhost:8080/networks.geojson; do n=$((n+1)) if [ $n -ge 10 ]; then echo "iem-web-services did not become ready in time" docker logs iem_web_services exit 1 fi sleep 1 done # Required for all matrix jobs - name: Run Memcached container run: | docker run -d --name iem_memcached -p 11211:11211 memcached:1.6.9 - uses: akrherz/ci_tooling/actions/iemwebfarm@main with: environment-file: environment.yml python-version: ${{ matrix.PYTHON_VERSION }} environment-name: prod # Copy repo's default settings into the real position - name: Copy PHP Setting Defaults run: | cp config/settings.inc.php.in config/settings.inc.php # All jobs need to have directories laid out - name: Setup Directory Paths run: sh .github/setuppaths.sh # Only one job needs to load the test data - name: Setup IEM Data if: ${{ matrix.WITH_TEST_DATA == 'test_data' }} run: sh .github/setupdata.sh - name: IEM TileCache Backend if: ${{ matrix.TEST_WEB == 'YES' }} run: | nohup bash deployment/start_tc_wsgi.sh > /tmp/iem-tilecache.log 2>&1 & echo $! > /tmp/iem-tilecache.pid n=0 until curl -sS -o /dev/null http://127.0.0.1:9081/; do n=$((n+1)) if [ $n -ge 30 ]; then echo "iem-tilecache did not become ready in time" cat /tmp/iem-tilecache.log exit 1 fi sleep 1 done - name: Configure Webfarm Server if: ${{ matrix.TEST_WEB == 'YES' }} run: | echo '' | sudo tee /etc/apache2/sites-enabled/iem.conf > /dev/null cat config/mesonet.inc | sudo tee -a /etc/apache2/sites-enabled/iem.conf > /dev/null echo '' | sudo tee -a /etc/apache2/sites-enabled/iem.conf > /dev/null # ci_tooling places a mod_wsgi conf with startup disabled, we enable it sudo sed -i 's/# WSGIImportScript/WSGIImportScript/' /etc/apache2/sites-enabled/mod_wsgi.conf # restart apache sudo service apache2 restart sudo systemctl status apache2.service -l - name: Run IEM Production checks if: ${{ matrix.TEST_WEB == 'YES' }} run: | git clone --depth 1 https://github.com/akrherz/iem-production-checks.git .ipc SERVICE=http://iem.local pytest -n 4 .ipc/tests/test_*.py - name: Run mod_wsgi smoke test if: ${{ matrix.TEST_WEB == 'YES' }} run: pytest -n 4 tests/test_mod_wsgi.py - name: Test PHP webscripts if: ${{ matrix.TEST_WEB == 'YES' }} run: pytest -n 4 tests/test_php.py # - name: Setup upterm session # if: ${{ matrix.TEST_WEB == 'NO' && matrix.WITH_TEST_DATA == 'test_data' }} # uses: owenthereal/action-upterm@v1 # with: # limit-access-to-actor: true - name: Run IEMWeb Python Check if: ${{ matrix.TEST_WEB == 'NO' && matrix.WITH_TEST_DATA == 'test_data' }} run: | export PYTHONPATH=/opt/iem/pylib python -m pytest --mpl --cov=iemweb --durations=10 -n 4 -W error::FutureWarning tests/iemweb/ python -m coverage xml - name: Upload to Codecov if: ${{ matrix.TEST_WEB == 'NO' && matrix.WITH_TEST_DATA == 'test_data' }} uses: codecov/codecov-action@v6 with: files: coverage.xml fail_ci_if_error: true - name: View Apache Logs if: ${{ failure() && matrix.TEST_WEB == 'YES' }} run: | sudo systemctl status apache2 -l sudo cat /var/log/apache2/error.log - name: View TileCache Logs if: ${{ failure() && matrix.TEST_WEB == 'YES' }} run: | if [ -f /tmp/iem-tilecache.pid ]; then ps -fp "$(cat /tmp/iem-tilecache.pid)" || true fi if [ -f /tmp/iem-tilecache.log ]; then cat /tmp/iem-tilecache.log fi - name: View PHP-FPM Logs if: ${{ failure() && matrix.TEST_WEB == 'YES' }} run: | sudo cat /var/log/php*-fpm.log ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL" on: push: branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - cron: "22 12 * * 1" jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest timeout-minutes: 360 permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ javascript, python ] steps: - name: Checkout repository uses: actions/checkout@v6 - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild uses: github/codeql-action/autobuild@v4 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: category: "/language:${{ matrix.language }}" upload: true ================================================ FILE: .github/workflows/etchosts.txt ================================================ 127.0.0.1 iem.local 127.0.0.1 iemdb.local 127.0.0.1 iemdb-hads.local 127.0.0.1 iemdb-mos.local 127.0.0.1 iemdb-idep.local 127.0.0.1 iemdb-dep_china.local 127.0.0.1 iemdb-dep_europe.local 127.0.0.1 iemdb-dep_sa.local 127.0.0.1 iemdb-iembot.local 127.0.0.1 iemdb-iemre.local 127.0.0.1 iemdb-iemre_china.local 127.0.0.1 iemdb-iemre_europe.local 127.0.0.1 iemdb-iemre_sa.local 127.0.0.1 iemdb-postgis.local 127.0.0.1 iemdb-mesosite.local 127.0.0.1 iemdb-afos.local 127.0.0.1 iemdb-asos.local 127.0.0.1 iemdb-hml.local 127.0.0.1 iemdb-id3b.local 127.0.0.1 iemdb-nldn.local 127.0.0.1 iemdb-snet.local 127.0.0.1 iemdb-mos.local 127.0.0.1 iemdb-raob.local 127.0.0.1 iemdb-rwis.local 127.0.0.1 iemdb-squaw.local 127.0.0.1 iemdb-awos.local 127.0.0.1 iemdb-iem.local 127.0.0.1 iemdb-other.local 127.0.0.1 iemdb-radar.local 127.0.0.1 iemdb-scan.local 127.0.0.1 iemdb-wepp.local 127.0.0.1 iemdb-coop.local 127.0.0.1 iemdb-isuag.local 127.0.0.1 iemdb-portfolio.local 127.0.0.1 iemdb-smos.local 127.0.0.1 iemdb-talltowers.local 127.0.0.1 iemdb-td.local 127.0.0.1 iemdb-uscrn.local 127.0.0.1 iemdb-sustainablecorn.local 127.0.0.1 iemdb-asos1min.local 127.0.0.1 iem-memcached3 127.0.0.1 iem-memcached 127.0.0.1 iem-web-services 127.0.0.1 iem-web-services.agron.iastate.edu ================================================ FILE: .github/workflows/mapserver.yml ================================================ name: Test Mapserver Files on: pull_request: branches: - main push: branches: - main jobs: build-linux: name: Test Mapserver Files defaults: run: # Ensures environment gets sourced right shell: bash -l -e {0} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Add /etc/hosts entries run: | cat .github/workflows/etchosts.txt | sudo tee -a /etc/hosts # setup conda-forge with micromamba - name: Setup Python uses: mamba-org/setup-micromamba@v3 with: environment-file: .github/ms_environment.yml condarc: | channels: - conda-forge create-args: >- python=3.14 environment-name: prod cache-environment: true - name: Run IEM Database container run: | docker run -d --name iem_database -p 5432:5432 ghcr.io/akrherz/iem_database:test_data until docker exec iem_database pg_isready -h localhost; do sleep 6 done - name: Setup CI from ci-tooling run: | set -e sudo ln -s `pwd` /opt/iem sudo mkdir -p /mesonet/ldmdata/ wget -q http://mesonet.agron.iastate.edu/pickup/ci_msinc.tgz sudo tar -C / -xzf ci_msinc.tgz - name: Run map2img run: | set -e bash tests/run_mapserver.sh ================================================ FILE: .github/workflows/publish-iemjs.yml ================================================ name: Publish iemjs to npm on: push: tags: - 'iemjs-v*' # Triggers on tags like iemjs-v1.0.0, iemjs-v1.2.3, etc. jobs: publish: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: '18' registry-url: 'https://registry.npmjs.org' - name: Extract version from tag id: extract_version run: | # Extract version from tag (iemjs-v1.0.0 -> 1.0.0) VERSION=${GITHUB_REF#refs/tags/iemjs-v} echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Publishing version: $VERSION" - name: Validate package run: | cd src/iemjs npm pack --dry-run - name: Run basic syntax check run: | cd src/iemjs node -c src/domUtils.js node -c src/iemdata.js node -c src/index.js - name: Publish to npm run: | cd src/iemjs npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Extract changelog for this version id: changelog run: | cd src/iemjs if [ -f "CHANGELOG.md" ]; then # Extract changelog section for this version VERSION=${{ steps.extract_version.outputs.version }} CHANGELOG_SECTION=$(sed -n "/## \[$VERSION\]/,/## \[/p" CHANGELOG.md | head -n -1) if [ -z "$CHANGELOG_SECTION" ]; then CHANGELOG_SECTION="See CHANGELOG.md for details." fi # Save to output, escaping newlines echo "content<> $GITHUB_OUTPUT echo "$CHANGELOG_SECTION" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT else echo "content=No changelog available." >> $GITHUB_OUTPUT fi - name: Create GitHub Release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: iemjs v${{ steps.extract_version.outputs.version }} body: | ## iemjs v${{ steps.extract_version.outputs.version }} Published to npm: https://www.npmjs.com/package/iemjs ### Changes ${{ steps.changelog.outputs.content }} ### Installation ```bash npm install iemjs@${{ steps.extract_version.outputs.version }} ``` draft: false prerelease: false ================================================ FILE: .github/workflows/test-iemjs.yml ================================================ name: Test iemjs package on: push: branches: [ main, master ] paths: - 'src/iemjs/**' - '.github/workflows/test-iemjs.yml' pull_request: branches: [ main, master ] paths: - 'src/iemjs/**' - '.github/workflows/test-iemjs.yml' jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [16, 18, 20] steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Run iemjs tests run: | cd src/iemjs npm test - name: Test npm pack (dry run) run: | cd src/iemjs npm pack --dry-run echo "✅ npm pack simulation successful" - name: Run ESLint on iemjs files run: | # Check ESLint against our iemjs files npx eslint src/iemjs/src/*.js --format=compact || echo "⚠️ ESLint warnings found (not failing build)" # Test on different operating systems cross-platform: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node-version: [18] steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Test basic functionality run: | cd src/iemjs npm run test:syntax shell: bash ================================================ FILE: .gitignore ================================================ /.settings /.buildpath /.project /.pydevproject **/fe.py /.vscode **/gemglb.nts **/last.nts /config/settings.inc.php node_modules **/.coverage coverage.xml # Stuff built by iemvtec htdocs/vtec/assets* htdocs/vtec/legends htdocs/vtec/_index_content.html ================================================ FILE: .pre-commit-config.yaml ================================================ ci: autoupdate_schedule: quarterly repos: - repo: https://github.com/pre-commit/mirrors-eslint rev: 'v10.3.0' hooks: - id: eslint additional_dependencies: - "eslint" - "@eslint/js" - "globals" files: \.js?$ types: [file] args: - --fix language_version: 'system' # Use system Node.js which should support ES modules - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.15.13" hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] - id: ruff-format ================================================ FILE: .prettierrc ================================================ { "semi": true, "trailingComma": "es5", "singleQuote": true, "printWidth": 100, "tabWidth": 4, "useTabs": false, "bracketSpacing": true, "arrowParens": "avoid", "endOfLine": "lf", "overrides": [ { "files": "*.php", "options": { "parser": "php", "printWidth": 120 } }, { "files": "*.html", "options": { "printWidth": 120, "htmlWhitespaceSensitivity": "css" } } ] } ================================================ FILE: LICENSE ================================================ Copyright (c) 2005 Iowa State University Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Iowa Environmental Mesonet If using this code causes your server to have kittens, it is your own fault. This monolith drives much of the ingest, processing, product generation, and web presence of the [IEM](https://mesonet.agron.iastate.edu). Hopefully it can be found useful for others to at least look at to see how some of the magic happens. Limited integration testing is done on Github Actions: [![Build Status](https://github.com/akrherz/iem/workflows/IEM%20CI/badge.svg)](https://github.com/akrherz/iem) [![DeepSource](https://app.deepsource.com/gh/akrherz/iem.svg/?label=active+issues&show_trend=true&token=WvZunVBligt7HgkO2JGg5uMe)](https://app.deepsource.com/gh/akrherz/iem/) [![codecov](https://codecov.io/gh/akrherz/iem/graph/badge.svg?token=zKXnLZdxIk)](https://codecov.io/gh/akrherz/iem) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/akrherz/iem/main.svg)](https://results.pre-commit.ci/latest/github/akrherz/iem/main) See [@akrherz Github Profile](https://github.com/akrherz) for an overview of repositories found here and how the fit together. ## Requirements - Python 3.10+ (CI tests with 3.14) - PHP 8 ## Deployment Notes - Vendor static assets used by web pages are documented in [docs/deployment/vendor-static-assets.md](docs/deployment/vendor-static-assets.md). ================================================ FILE: cgi-bin/afos/retrieve.py ================================================ """Implementation at https://github.com/akrherz/iem/blob/main/pylib/iemweb/afos/retrieve.py User documentation available at https://mesonet.agron.iastate.edu/cgi-bin/afos/retrieve.py?help""" from iemweb.afos.retrieve import application __all__ = ["application"] ================================================ FILE: cgi-bin/climate/orc.py ================================================ """Implementation at https://github.com/akrherz/iem/blob/main/pylib/iemweb/climate/orc.py User documentation available at https://mesonet.agron.iastate.edu/cgi-bin/climate/orc.py?help""" from iemweb.climate.orc import application __all__ = ["application"] ================================================ FILE: cgi-bin/geocoder.py ================================================ """Implementation at https://github.com/akrherz/iem/blob/main/pylib/iemweb/geocoder.py User documentation available at https://mesonet.agron.iastate.edu/cgi-bin/geocoder.py?help""" from iemweb.geocoder import application __all__ = ["application"] ================================================ FILE: cgi-bin/index.php ================================================ # Use a daily deadicated log file, this avoids server reloads every day # which are not much fun when servicing a 1,000 req/sec CustomLog "|/usr/sbin/rotatelogs -l ${IEMWEBFARM_LOGROOT}/iemssl-%Y%m%d 86400" combined SSLEngine on # using ISU Certs due to cross-signing ugliness with LE and ancient kiosks # https://mesonet.agron.iastate.edu/onsite/news.phtml?id=1423 # SSLCertificateChainFile was removed in Apache 2.4.8 # Termination happening at the F5, so the cert here is simple # Note that I manually reorder the chain to put the ISU cert first SSLCertificateFile /etc/pki/tls/iem.cert_and_chain SSLCertificateKeyFile /etc/pki/tls/iem.key Include conf.d/mesonet.inc ================================================ FILE: config/00iem.conf ================================================ # Use a daily deadicated log file, this avoids server reloads every day # which are not much fun when servicing a 1,000 req/sec CustomLog "|/usr/sbin/rotatelogs -l ${IEMWEBFARM_LOGROOT}/iem-%Y%m%d 86400" combined Include conf.d/mesonet.inc ================================================ FILE: config/backend.conf ================================================ # It turns out that backending everything with one mapserv.fcgi instance # was not ideal as having one slow node would quickly back everything up # as other nodes could then not reach this backend. So instead, lets keep # things isolated and make the nodes backend themselves ServerName iem-backend.local DocumentRoot /opt/iem/htdocs # Don't log any accesses CustomLog /dev/null common # Point to system-installed mapserv binary (from conda env) ScriptAlias /cgi-bin/mapserv/mapserv.fcgi ${IEMWEBFARM_MAPSERV} ScriptAlias /cgi-bin/mapserv/mapserv ${IEMWEBFARM_MAPSERV} SetHandler fcgid-script Options +ExecCGI # Only need cgi-bin, note we have AllowOverride None, which saves a bit # in performance as apache does not need to look for .htaccess with each # request ScriptAlias /cgi-bin/ "/opt/iem/cgi-bin/" AllowOverride None Order allow,deny Allow from all ================================================ FILE: config/iem-archive.conf ================================================ # backend for /archive/data requests from webfarm ServerName iem-archive.agron.iastate.edu ServerAlias iem-archive.local DocumentRoot /var/www/html # Don't log any accesses CustomLog /dev/null common Alias /archive/data /mesonet/ARCHIVE/data Options Indexes FollowSymLinks IndexOptions NameWidth=* AllowOverride None Require all granted ================================================ FILE: config/mesonet-longterm-vhost.conf ================================================ # apache vhost file for mesonet-longterm.agron.iastate.edu # This now resides on anticyclone ServerName mesonet-longterm.agron.iastate.edu ServerAlias mesonet-longterm.local Alias "/archive/nexrad" "/mnt/mesonet2/longterm/nexrad3_iowa" # Need FollowSymLinks for mod_rewrite to work! Options Indexes FollowSymLinks Order allow,deny Allow from all Alias "/archive/gempak" "/mnt/mesonet2/longterm/gempak" # Need FollowSymLinks for mod_rewrite to work! Options Indexes Order allow,deny Allow from all Alias "/archive/raw" "/mnt/mesonet2/longterm/raw" # Need FollowSymLinks for mod_rewrite to work! Options Indexes Order allow,deny Allow from all ================================================ FILE: config/mesonet.inc ================================================ # Apache vhost configuration for IEM # mod_wsgi is configured from https://github.com/akrherz/iemwebfarm # DirectoryIndex index.py # WSGIDaemonProcess iemwsgi_ap processes=24 threads=1 display-name=%{GROUP} maximum-requests=100 Include /opt/iemwebfarm/config/vhost_common.conf ServerName mesonet.agron.iastate.edu ServerAlias www.mesonet.agron.iastate.edu ServerAlias mesonet1.agron.iastate.edu ServerAlias mesonet2.agron.iastate.edu ServerAlias mesonet3.agron.iastate.edu ServerAlias iem.local ServerAdmin akrherz@iastate.edu DocumentRoot /opt/iem/htdocs # http://enable-cors.org/server_apache.html Header set Access-Control-Allow-Origin "*" # RewriteEngine is not enabled for vhosts by default RewriteEngine On # Useful for debugging # LogLevel alert rewrite:trace3 Redirect permanent /archive/nexrad https://mesonet-longterm.agron.iastate.edu/archive/nexrad Redirect permanent /archive/gempak https://mesonet-longterm.agron.iastate.edu/archive/gempak Redirect permanent /archive/raw https://mesonet-longterm.agron.iastate.edu/archive/raw Redirect permanent /data/nexrd2/raw https://nomads.ncep.noaa.gov/pub/data/nccf/radar/nexrad_level2 # Need FollowSymLinks for mod_rewrite to work! Options Indexes FollowSymLinks AllowOverride None Require all granted # Default handler for python scripts WSGIProcessGroup iemwsgi_ap # Allow wsgi scripts to emit 404s that can come back to the handler. # FIXME this causes all error handling to come back, which is a problem # https://github.com/GrahamDumpleton/mod_wsgi/issues/825 # WSGIErrorOverride On AddHandler wsgi-script .py Options +ExecCGI Options Indexes FollowSymLinks AllowOverride None Require all granted Options Indexes FollowSymLinks AllowOverride None Require all granted Options Indexes FollowSymLinks AllowOverride None Require all granted # Point to system-installed mapserv binary (from conda env) ScriptAlias /cgi-bin/mapserv/mapserv.fcgi ${IEMWEBFARM_MAPSERV} ScriptAlias /cgi-bin/mapserv/mapserv ${IEMWEBFARM_MAPSERV} SetHandler fcgid-script Options +ExecCGI Alias /cgi-bin/ "/opt/iem/cgi-bin/" AllowOverride None Options FollowSymLinks Require all granted # Default handler for python scripts WSGIProcessGroup iemwsgi_ap AddHandler wsgi-script .py AddHandler cgi-script .cgi Options +ExecCGI # Prevent caching of index.html Header set Cache-Control "no-cache, no-store, must-revalidate" Header set Pragma "no-cache" Header set Expires "0" Alias /usage "/mesonet/share/usage/mesonet.agron.iastate.edu/" Options Indexes MultiViews AllowOverride None Require all granted Alias /agclimate/usage "/mesonet/share/usage/agclimate/" Options Indexes MultiViews AllowOverride None Require all granted RewriteRule wfo.phtml index.php Alias /data "/mesonet/ldmdata" Options Indexes MultiViews FollowSymLinks AllowOverride None Require all granted Redirect permanent /data/gis/images/unproj /data/gis/images/4326 Redirect permanent /data/gis/shape/unproj /data/gis/shape/4326 Alias "/onsite/lapses" "/mesonet/share/lapses" Alias "/onsite/windrose" "/mesonet/share/windrose" Alias "/onsite/iemre" "/mesonet/data/iemre" Alias "/onsite/iemre_china" "/mesonet/data/iemre_china" Alias "/onsite/iemre_europe" "/mesonet/data/iemre_europe" Alias "/onsite/iemre_sa" "/mesonet/data/iemre_sa" Alias "/onsite/mrms" "/mesonet/data/mrms" Alias "/onsite/prism" "/mesonet/data/prism" Alias "/onsite/stage4" "/mesonet/data/stage4" Alias "/onsite/era5land" "/mesonet/data/era5" Alias "/present" "/mesonet/share/present" Alias "/cases" "/mesonet/share/cases" Alias "/GIS/data/gis" "/mesonet/data/gis" Alias "/archive/awos" "/mesonet/ARCHIVE/awos" Alias "/archive/raw" "/mesonet/ARCHIVE/raw" # 7 Mar 2026 removeme at some point Alias "/archive/rer" "/mesonet/ARCHIVE/rer" Alias "/m/img" "/mesonet/share/iemmaps" Alias "/sites/pics" "/mesonet/share/pics" Alias "/climodat/reports" "/mesonet/share/climodat/reports" Alias "/climodat/ks" "/mesonet/share/climodat/ks" Alias "/pickup" "/mesonet/share/pickup" AllowOverride None Options FollowSymLinks Indexes MultiViews Require all granted Alias /tmp /var/webtmp AllowOverride None RewriteRule n0r.cgi /cgi-bin/mapserv/mapserv.fcgi?map=/opt/iem/data/wms/nexrad/n0r.map& [QSA,L] RewriteRule n0q.cgi /cgi-bin/mapserv/mapserv.fcgi?map=/opt/iem/data/wms/nexrad/n0q.map& [QSA,L] RewriteRule wwa.cgi /cgi-bin/mapserv/mapserv.fcgi?map=/opt/iem/data/wms/us/wwa.map& [QSA,L] # The backend doesn't really care about the hostname attm ProxyPreserveHost Off RequestHeader set "Host" "iem.local" # Attempt to reduce random proxy noise ProxySet connectiontimeout=5 timeout=55 retry=0 disablereuse=On ProxyPass /c http://127.0.0.1:9081/c ProxyPass /cache http://127.0.0.1:9081/cache RewriteRule vtec_(.*).png radmap.php?layers[]=places&layers[]=nexrad&layers[]=cities&layers[]=interstates&layers[]=uscounties&vtec=$1&width=797&height=400 # Lots of old links out there to bad URIs RewriteRule index\.(html|pl|php)$ /agclimate/ [R] Redirect /agclimate/daily-pics https://mesonet.agron.iastate.edu/agclimate/daily_pics Redirect /agclimate/daily_pics https://mesonet.agron.iastate.edu/data/agclimate Redirect /agclimate/info.txt https://mesonet.agron.iastate.edu/agclimate/info.phtml # RewriteRule smplot/([0-9]{1,12})/(.*).png$ isusm.py?t=$1&v=$2 RewriteRule isusm.csv nmp_csv.py RewriteRule datasets/([a-z_]+).html$ datasets.php?id=$1 [QSA] RewriteRule datasets/?$ datasets.php [QSA] # As a reminder, we are going via this route to take advantage of caching # port 8080 is nginx, which proxies to fastapi on 8000 # Always immediately retry a connection # We wait up to 5 seconds to connect to the remote server # We wait up to 60 seconds for the remote server to respond ProxyPass "http://iem-web-services.agron.iastate.edu:8080" retry=0 connectiontimeout=5 timeout=60 ProxyPassReverse "http://iem-web-services.agron.iastate.edu:8080" ProxyErrorOverride On 502 503 504 ErrorDocument 502 /api/proxy_error_handler.py ErrorDocument 503 /api/proxy_error_handler.py ErrorDocument 504 /api/proxy_error_handler.py RequestHeader set "Host" "iem-archive.local" ProxyPreserveHost Off # Send 404s back here ProxyErrorOverride On # hacky to keep the paths matching in auto-index ProxyPass "http://iem-archive.local/archive/data" ProxyPassReverse "http://iem-archive.local/archive/data" RewriteRule pil_([A-Z0-9]{3,6}).png$ pil.php?pil=$1 # Allow for python scripts to act has directories NETWORK/SID/TOOL RewriteRule ^([^/]{3,20})/([^/]{3,20})/([^/]+)$ $3.py RewriteRule ([0-9]{12})_([A-Z0-9\s]+).png$ text2png.py?e=$1&pil=$2 [B,L] Require all granted WSGIProcessGroup iemwsgi_ap AddHandler wsgi-script .py # note the preceding directory config WSGIScriptAlias /geojson /opt/iem/pylib/iemweb/dispatch.py WSGIScriptAlias /json /opt/iem/pylib/iemweb/dispatch.py WSGIScriptAlias /search /opt/iem/pylib/iemweb/dispatch.py WSGIScriptAlias /agclimate/ames_precip.py /opt/iem/pylib/iemweb/dispatch.py WSGIScriptAlias /agclimate/isusm.py /opt/iem/pylib/iemweb/dispatch.py WSGIScriptAlias /agclimate/nmp_csv.py /opt/iem/pylib/iemweb/dispatch.py RewriteRule daily/([0-9\-]+)/([0-9\.\-]+)/([0-9\.\-]+)/(json) daily.py?date=$1&lat=$2&lon=$3&format=$4 RewriteRule hourly/([0-9\-]+)/([0-9\.\-]+)/([0-9\.\-]+)/(json) hourly.py?date=$1&lat=$2&lon=$3&format=$4 RewriteRule multiday/([0-9\-]+)/([0-9\-]+)/([0-9\.]+)/([0-9\.\-]+)/(json) multiday.py?sdate=$1&edate=$2&lat=$3&lon=$4&format=$5 RewriteRule cum/([0-9\-]+)/([0-9\-]+)/(shp) cum.py?date0=$1&date1=$2&format=$3&base=50&ceil=86 RewriteRule cumcounty/([0-9]+)/([0-9\-]+)/([0-9\-]+)/([0-9]+)/([0-9]+)/(json) cum.py?county=$1&date0=$2&date1=$3&format=$6&base=$4&ceil=$5 RewriteRule roadcond.kml roadcond.php RewriteRule roadcond_v2.kml roadcond.php?linewidth=6&maxtype=2 RewriteRule days_since_([A-Z][A-Z])_([A-Z]).png /plotting/auto/plot/92/phenomena:$1::significance:$2::dpi:100.png RewriteRule tags/([^\.]+)\.html tags/index.php?tag=$1 [L] RewriteRule vote/(good|bad|abstain).json$ vote.py?vote=$1 RewriteRule vote.json$ vote.py RewriteRule ^[0-9]{4}/[0-9]{2}/.*$ content.py [PT] RewriteRule live/(.*).jpg live.py?id=$1 [L] RewriteRule qrcode/([0-9]{1,12})/(.*).png$ gen_qrcode.py?p=$1&q=$2&fmt=png [B] RewriteRule plot/([0-9]{1,12})/(.*).(png|csv|txt|xlsx|js|geojson|pdf|svg|geotiff)$ autoplot.py?p=$1&q=$2&fmt=$3 [B] RewriteRule meta/([0-9]{1,12}).json meta.py?p=$1 [QSA,B] RewriteRule maxcsv/(.*).txt$ maxcsv.py?q=$1 RewriteRule grx/iadot_trucks.txt grx/iadot_trucks.py # Legacy link was removed 5 Jan 2015 RewriteRule grx/idot_trucks.php grx/iadot_trucks.py # PHP replaced by python 24 May 2020 RewriteRule grx/time_mot_loc.(php|txt) grx/time_mot_loc.py [QSA] # .php was legacy stuff, lets not break old paths RewriteRule grx/l3attr.(php|txt) grx/l3attr.py [QSA] RewriteRule kcau.jpg kcau.php RewriteRule ktiv.jpg ktiv.php RewriteRule kwwl.jpg kwwl.php?v2 RewriteRule ^event/.* index.py [L] RewriteRule ([^\.]+)\.html index.py?vtec=$1 [L] RewriteRule f/(.*)$ f.py [QSA] RewriteRule pl_(.*)_(.*).xml pl.py?network=$1&station=$2 [QSA] RewriteRule sd_(.*)_(.*).xml sd.py?network=$1&station=$2 [QSA] ================================================ FILE: config/navbar.json ================================================ { "tabs": [ { "title": "Apps", "subs": [ { "title": "Application Index", "url": "/apps.php" }, { "title": "Automated Data Plotting", "url": "/plotting/auto/" }, { "title": "Climodat", "url": "/climodat/" }, { "title": "Climodat Monitor", "url": "/climodat/monitor.php" }, { "title": "IEM Explorer", "url": "/explorer/" }, { "title": "Hourly Precip", "url": "/rainfall/obhour.phtml" }, { "title": "Interactive Radar", "url": "/GIS/apps/rview/warnings.phtml" }, { "title": "Pest Maps + Forecasting", "url": "/topics/pests/" }, { "title": "Sortable Currents", "url": "/my/current.phtml" }, { "title": "Time Machine", "url": "/timemachine/" }, { "title": "Wind Roses", "url": "/sites/windrose.phtml?station=AMW&network=IA_ASOS" } ] }, { "title": "Areas", "subs": [ { "title": "Ag Weather/Climate Info", "url": "/agweather/" }, { "title": "Archive Mainpage", "url": "/archive/" }, { "title": "Climate Mainpage", "url": "/climate/" }, { "title": "Current Mainpage", "url": "/current/" }, { "title": "Drought", "url": "/dm/" }, { "title": "GIS Mainpage", "url": "/GIS/" }, { "title": "NWS Mainpage", "url": "/nws/" }, { "title": "Severe Weather Mainpage", "url": "/current/severe.phtml" } ] }, { "title": "Datasets", "subs": [ { "title": "Daily Climatology", "url": "/COOP/extremes.php" }, { "title": "Daily Observations", "url": "/request/daily.phtml" }, { "title": "Dataset Documentation", "url": "/info/datasets/" }, { "title": "IEM Reanalysis", "url": "/iemre/" }, { "title": "Model Output Statistics", "url": "/mos/" }, { "title": "NEXRAD Mosaic", "url": "/docs/nexrad_mosaic/" }, { "title": "PIREP - Pilot Reports", "url": "/request/gis/pireps.php" }, { "title": "Roads Mainpage", "url": "/roads/" }, { "title": "RADAR & Satellite", "url": "/current/radar.phtml" }, { "title": "Rainfall Data", "url": "/rainfall/" }, { "title": "Sounding Archive", "url": "/archive/raob/" }, { "title": "Soil Moisture Satellite", "url": "/smos/" } ] }, { "title": "Info", "subs": [ { "title": "Info Mainpage", "url": "/info.php" }, { "title": "Daily Features", "url": "/onsite/features/past.php" }, { "title": "Links", "url": "/info/links.php" }, { "title": "News", "url": "/onsite/news.phtml" }, { "title": "Presentations", "url": "/present/" }, { "title": "Referenced By", "url": "/info/refs.php" }, { "title": "Station Data and Metadata", "url": "/sites/locate.php" }, { "title": "Quality Control", "url": "/QC/" }, { "title": "Variables", "url": "/info/variables.phtml" } ] }, { "title": "Networks", "subs": [ { "title": "Network Tables", "url": "/sites/networks.php" }, { "title": "ASOS/AWOS Airports", "url": "/ASOS/" }, { "title": "CoCoRaHS - Citizen Science", "url": "/cocorahs/" }, { "title": "DCP/HADS/SHEF - Hydrological", "url": "/DCP/" }, { "title": "NWS COOP - Daily Climate", "url": "/COOP/" }, { "title": "ISU Soil Moisture", "url": "/agclimate/" }, { "title": "NLAE Flux", "url": "/nstl_flux/" }, { "title": "RWIS - Roadway Weather", "url": "/RWIS/" }, { "title": "SCAN - NRCS Soil Climate", "url": "/scan/" }, { "title": "Other", "url": "/other/" }, { "title": "US Climate Reference", "url": "/uscrn/" } ] }, { "title": "NWS Data", "subs": [ { "title": "NWS Mainpage", "url": "/nws/" }, { "title": "Local Storm Report App", "url": "/lsr/" }, { "title": "IEM Cow (SBW Verification)", "url": "/cow/" }, { "title": "IEM Raccoon (SBW Powerpoints)", "url": "/raccoon/" }, { "title": "River Summary", "url": "/river/" }, { "title": "Satellite Data", "url": "/GIS/goes.phtml" }, { "title": "Search for Warnings", "url": "/vtec/search.php" }, { "title": "Special Weather Statement (SPS) Search", "url": "/nws/sps_search/" }, { "title": "SPC Convective Outlook / MCD Search", "url": "/nws/spc_outlook_search/" }, { "title": "SPC Watches", "url": "/GIS/apps/rview/watch.phtml" }, { "title": "Text Archives Mainpage", "url": "/nws/text.php" }, { "title": "Text Listing by WFO/Center/Product", "url": "/wx/afos/list.phtml" }, { "title": "Text by Product ID", "url": "/wx/afos/" }, { "title": "VTEC Browser", "url": "/vtec/" } ] }, { "title": "Services", "subs": [ { "title": "API Mainpage", "url": "/api/" }, { "title": "CGI / Bulk Data", "url": "/api/#cgi" }, { "title": "Gibson Ridge Placefiles", "url": "/request/grx/" }, { "title": "iembot", "url": "/projects/iembot/" }, { "title": "JSON Webservices", "url": "/api/#json" }, { "title": "LDM", "url": "/request/ldm.php" }, { "title": "Max CSV", "url": "/request/maxcsv.py?help" }, { "title": "OGC Webservices", "url": "/ogc/" }, { "title": "RadMap API", "url": "/GIS/radmap_api.phtml" }, { "title": "RADAR Services", "url": "/GIS/radview.phtml" } ] }, { "title": "Webcams", "subs": [ { "title": "Webcam mainpage", "url": "/projects/webcam.php" }, { "title": "Build your own lapses", "url": "/current/bloop.phtml" }, { "title": "Cool lapses", "url": "/cool/" }, { "title": "IEM Webcam Viewer", "url": "/current/viewer.phtml" }, { "title": "ISU Campus Webcams", "url": "/current/isucams.phtml" }, { "title": "Recent lapses", "url": "/current/camlapse/" }, { "title": "Still images", "url": "/current/webcam.php" } ] } ] } ================================================ FILE: config/settings.inc.php.in ================================================ CURRENT_TIMESTAMP - '70 minutes'::interval) and tmpf is not null ) as foo using unique foid using srid=4326" STATUS OFF TYPE POINT LABELCACHE ON PROJECTION "init=epsg:4326" END CLASS EXPRESSION ([tmpf] >= 35 AND [tmpf] < 120) TEXT ([tmpf]) STYLE COLOR -1 -1 -1 END LABEL COLOR 30 190 20 OUTLINECOLOR 0 0 0 TYPE TRUETYPE FONT 'liberation' SIZE 10 POSITION UL OFFSET 2 2 BUFFER 1 PARTIALS TRUE FORCE FALSE END END CLASS EXPRESSION ([tmpf] >= 34) TEXT ([tmpf]) STYLE COLOR -1 -1 -1 END LABEL COLOR 215 255 0 OUTLINECOLOR 0 0 0 TYPE TRUETYPE FONT 'liberation' SIZE 10 POSITION UL OFFSET 2 2 BUFFER 1 PARTIALS TRUE FORCE FALSE END END CLASS EXPRESSION ([tmpf] >= 33) TEXT ([tmpf]) STYLE COLOR -1 -1 -1 END LABEL COLOR 255 164 0 OUTLINECOLOR 0 0 0 TYPE TRUETYPE FONT 'liberation' SIZE 10 POSITION UL OFFSET 2 2 BUFFER 1 PARTIALS TRUE FORCE FALSE END END CLASS EXPRESSION ([tmpf] >= 32) TEXT ([tmpf]) STYLE COLOR -1 -1 -1 END LABEL COLOR 255 50 0 OUTLINECOLOR 0 0 0 TYPE TRUETYPE FONT 'liberation' SIZE 10 POSITION UL OFFSET 2 2 BUFFER 1 PARTIALS TRUE FORCE FALSE END END CLASS EXPRESSION ([tmpf] >= 31) TEXT ([tmpf]) STYLE COLOR -1 -1 -1 END LABEL COLOR 255 0 144 OUTLINECOLOR 0 0 0 TYPE TRUETYPE FONT 'liberation' SIZE 10 POSITION UL OFFSET 2 2 BUFFER 1 PARTIALS TRUE FORCE FALSE END END CLASS EXPRESSION ([tmpf] >= 30) TEXT ([tmpf]) STYLE COLOR -1 -1 -1 END LABEL #COLOR 232 164 226 COLOR 255 0 255 OUTLINECOLOR 0 0 0 TYPE TRUETYPE FONT 'liberation' SIZE 10 POSITION UL OFFSET 2 2 BUFFER 1 PARTIALS TRUE FORCE FALSE END END CLASS EXPRESSION ([tmpf] > -40) TEXT ([tmpf]) STYLE COLOR -1 -1 -1 END LABEL #COLOR 232 214 226 COLOR 255 255 255 OUTLINECOLOR 0 0 0 TYPE TRUETYPE FONT 'liberation' SIZE 10 POSITION UL OFFSET 2 2 BUFFER 1 PARTIALS TRUE FORCE FALSE END END END # # Generic stuff useful to all IEM base*.map # # USDM! LAYER NAME usdm STATUS OFF TYPE LINE DATA /mesonet/ldmdata/gis/shape/4326/us/dm_current.shp PROJECTION "init=epsg:4326" END CLASSITEM "DM" CLASS EXPRESSION /0/ STYLE COLOR 100 0 0 SIZE 4 SYMBOL 'circle' END TEXT "D0" LABEL COLOR 255 255 255 END END CLASS EXPRESSION /1/ TEXT "D1" STYLE COLOR 120 0 0 SIZE 4 SYMBOL 'circle' END LABEL COLOR 255 255 255 END END CLASS EXPRESSION /2/ TEXT "D2" STYLE COLOR 160 0 0 SIZE 4 SYMBOL 'circle' END LABEL COLOR 255 255 255 END END CLASS EXPRESSION /3/ TEXT "D3" STYLE COLOR 200 0 0 SYMBOL 'circle' SIZE 4 END LABEL COLOR 255 255 255 END END CLASS EXPRESSION /4/ TEXT "D4" STYLE COLOR 255 0 0 SIZE 4 SYMBOL 'circle' END LABEL COLOR 255 255 255 END END END LAYER NAME "iem_headerbar" TYPE POLYGON TRANSFORM FALSE UNITS pixels FEATURE POINTS 0 0 0 53 2000 53 2000 0 0 0 END END STATUS OFF LABELCACHE OFF CLASS STYLE COLOR 0 0 0 END END END LAYER NAME "iem_headerbar_logo" TYPE POINT TRANSFORM FALSE LABELCACHE ON UNITS pixels FEATURE POINTS 40 30 END END STATUS OFF LABELCACHE OFF CLASS STYLE SYMBOL 'iem_logo' END END END LAYER NAME "iem_headerbar_title" TYPE POINT STATUS OFF LABELCACHE ON TRANSFORM FALSE UNITS pixels CLASS LABEL COLOR 255 255 0 TYPE TRUETYPE SIZE 18 FONT 'liberation-bold' POSITION UR FORCE TRUE PARTIALS TRUE END END CLASS LABEL COLOR 255 255 255 TYPE TRUETYPE SIZE 12 FONT 'liberation-mono' POSITION UR FORCE TRUE PARTIALS TRUE END END END LAYER NAME "station_plot" TYPE POINT PROJECTION "init=epsg:4326" END LABELCACHE ON STATUS OFF CLASS NAME "station-cr" STYLE COLOR 0 0 0 END LABEL COLOR 0 0 0 FONT 'liberation' POSITION CR TYPE TRUETYPE FORCE TRUE SIZE 12 PARTIALS FALSE END END CLASS NAME "station-ul" STYLE COLOR 0 0 0 END LABEL COLOR 255 0 0 FONT 'liberation' POSITION UL TYPE TRUETYPE FORCE TRUE SIZE 12 PARTIALS FALSE END END CLASS NAME "station-ll" STYLE COLOR 0 0 255 END LABEL COLOR 0 0 255 FONT 'liberation' POSITION LL TYPE TRUETYPE FORCE TRUE SIZE 12 PARTIALS FALSE END END END LAYER NAME cwas STATUS OFF TYPE POLYGON DATA /mesonet/data/gis/static/shape/4326/nws/cwas.shp PROJECTION "init=epsg:4326" END LABELITEM "WFO" CLASS STYLE OUTLINECOLOR 255 165 0 END LABEL MINFEATURESIZE 50 OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION CC PARTIALS FALSE END END END LAYER NAME cwsu STATUS OFF TYPE POLYGON DATA /mesonet/data/gis/static/shape/4326/nws/cwsu.shp PROJECTION "init=epsg:4326" END LABELITEM "ID" CLASS STYLE OUTLINECOLOR 255 165 0 END LABEL MINFEATURESIZE 50 OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION CC PARTIALS FALSE END END END LAYER CONNECTIONTYPE postgis NAME warnings0_c CONNECTION "user=nobody dbname=postgis host=iemdb-postgis.local" DATA "geom from (select phenomena, significance, u.geom, random() as oid from warnings w JOIN ugcs u on (u.gid = w.gid) WHERE expire > CURRENT_TIMESTAMP) as foo using unique oid using SRID=4326" STATUS OFF TYPE LINE PROJECTION "init=epsg:4326" END CLASS NAME "Flash Flood Warn" EXPRESSION (('[phenomena]' = 'FF' or '[phenomena]' = 'MA') and '[significance]' = 'W') STYLE COLOR 0 255 0 SIZE 2 SYMBOL 'circle' END END CLASS NAME "Svr T'storm Warn" EXPRESSION ('[phenomena]' = 'SV' and '[significance]' = 'W') STYLE COLOR 0 0 0 SIZE 4 SYMBOL 'circle' END STYLE COLOR 255 255 0 SIZE 2 SYMBOL 'circle' END END CLASS NAME "Tornado Warn" EXPRESSION ('[phenomena]' = 'TO' and '[significance]' = 'W') STYLE COLOR 0 0 0 SIZE 4 SYMBOL 'circle' END STYLE COLOR 255 0 0 SIZE 2 SYMBOL 'circle' END END # Advisories CLASS NAME "Winter Advisory" EXPRESSION (('[phenomena]' = 'WW' or '[phenomena]' = 'BZ' or '[phenomena]' = 'WS' or '[phenomena]' = 'IP' or '[phenomena]' = 'HP' or '[phenomena]' = 'ZR' or '[phenomena]' = 'IS') and '[significance]' = 'Y') STYLE COLOR 222 184 135 SIZE 3 SYMBOL 'circle' END #STYLE # COLOR 222 184 135 # SIZE 1 # SYMBOL 'circle' #END END # Snow and Blowing snow CLASS NAME "Sn or Blow Sn Adv" EXPRESSION (('[phenomena]' = 'LE' or '[phenomena]' = 'SN' or '[phenomena]' = 'BS' or '[phenomena]' = 'SB') and '[significance]' = 'Y') STYLE COLOR 176 224 230 SIZE 3 SYMBOL 'circle' END STYLE COLOR 255 255 255 SIZE 1 SYMBOL 'circle' END END # Heavy Snow CLASS NAME "Heavy Snow Warn" EXPRESSION ('[phenomena]' = 'HS' and '[significance]' = 'W') STYLE COLOR 138 43 226 SIZE 3 SYMBOL 'circle' END STYLE COLOR 255 255 255 SIZE 1 SYMBOL 'circle' END END # WW Warning CLASS NAME "Winter Storm Warn" EXPRESSION (('[phenomena]' = 'LE' or '[phenomena]' = 'WS' or '[phenomena]' = 'WW') and '[significance]' = 'W') STYLE COLOR 255 105 180 SIZE 3 SYMBOL 'circle' END END # Sleet Fz CLASS NAME "Ice/Frz/Sleet Warn" EXPRESSION (('[phenomena]' = 'IP' or '[phenomena]' = 'HP' or '[phenomena]' = 'ZR' or '[phenomena]' = 'IS') and '[significance]' = 'W') STYLE COLOR 255 20 147 SIZE 3 SYMBOL 'circle' END END # Blizzard CLASS NAME "Blizzard Warn" EXPRESSION ('[phenomena]' = 'BZ' and '[significance]' = 'W') STYLE COLOR 255 0 0 SIZE 3 SYMBOL 'circle' END END # Freeze Warning CLASS NAME "Freeze Warn" EXPRESSION ('[phenomena]' = 'FZ' and '[significance]' = 'W') STYLE COLOR 255 0 0 SIZE 3 SYMBOL 'circle' END END # Marine Statement? CLASS NAME "Marine Statement" EXPRESSION ('[phenomena]' = 'MA' and '[significance]' = 'S') STYLE COLOR 0 200 0 SIZE 3 SYMBOL 'circle' END END END # Bar Header Bar! LAYER NAME bar640t TYPE POLYGON TRANSFORM FALSE STATUS OFF FEATURE POINTS 0 0 0 36 640 36 640 0 0 0 END END LABELCACHE FALSE CLASS STYLE COLOR 0 0 0 END END END LAYER NAME "n0q-ramp" TYPE POINT STATUS default TRANSFORM FALSE CLASS LABEL END STYLE SYMBOL 'n0q-ramp' COLOR 0 0 0 END END END # IEM Logo LAYER NAME logo TYPE POINT STATUS default TRANSFORM FALSE FEATURE WKT "POINT(100 100)" END CLASS STYLE SYMBOL 'iem_logo' END END END LAYER NAME "n0r-ramp" TYPE POINT STATUS default TRANSFORM FALSE CLASS LABEL END STYLE SYMBOL 'n0r-ramp' COLOR 0 0 0 END END END ================================================ FILE: data/gis/lsrs.mapinc ================================================ LAYER CONNECTIONTYPE postgis NAME lsrs CONNECTION "user=nobody dbname=postgis host=iemdb-postgis.local" DATA "geom from (select distinct city, magnitude, valid, geom, type as ltype, city || magnitude || x(geom) || y(geom) as k from lsrs WHERE valid > 'YESTERDAY'::timestamp) as foo USING unique k USING SRID=4326 " STATUS OFF TYPE POINT PROJECTION "init=epsg:4326" END CLASSITEM "ltype" CLASS EXPRESSION /W/ TEXT 'WATERSPOUT' LABEL OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION AUTO PARTIALS FALSE END STYLE COLOR 255 255 255 SYMBOL 'tornado' SIZE 10 END END CLASS EXPRESSION /G/ TEXT 'G[magnitude] MPH' LABEL OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION AUTO PARTIALS FALSE END STYLE COLOR 255 255 255 SYMBOL 'circle' SIZE 10 END END CLASS EXPRESSION /M/ TEXT 'G[magnitude] MPH' LABEL OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION AUTO PARTIALS FALSE END STYLE COLOR 255 255 255 SYMBOL 'circle' SIZE 10 END END CLASS EXPRESSION /T/ TEXT 'TORNADO [magnitude]' LABEL OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION AUTO PARTIALS FALSE END STYLE COLOR 255 255 255 SYMBOL 'tornado' SIZE 10 END END CLASS EXPRESSION /R/ TEXT 'HEAVY RAIN [magnitude]' LABEL OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION AUTO PARTIALS FALSE END STYLE COLOR 255 255 255 SYMBOL 'circle' SIZE 10 END END CLASS EXPRESSION /F/ TEXT 'FLASH FLOOD' LABEL OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION AUTO PARTIALS FALSE END STYLE COLOR 255 255 255 SYMBOL 'circle' SIZE 10 END END CLASS EXPRESSION /H/ TEXT 'HAIL [magnitude]' LABEL OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION AUTO PARTIALS FALSE END STYLE COLOR 255 255 255 SYMBOL 'triangle' SIZE 10 END END CLASS EXPRESSION /S/ TEXT 'SNOW [magnitude]inch' LABEL OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION AUTO PARTIALS FALSE END STYLE COLOR 255 255 255 SYMBOL 'circle' SIZE 7 END END CLASS EXPRESSION /5/ TEXT 'ICE [magnitude]inch' LABEL OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION AUTO PARTIALS FALSE END STYLE COLOR 255 255 255 SYMBOL 'circle' SIZE 7 END END CLASS EXPRESSION /D/ TEXT 'WND DMG [city]' LABEL OUTLINECOLOR 0 0 0 COLOR 255 255 255 TYPE BITMAP SIZE MEDIUM POSITION AUTO PARTIALS FALSE END STYLE COLOR 255 255 255 SYMBOL 'triangle' SIZE 10 END END END ================================================ FILE: data/gis/meta/26914.prj ================================================ PROJCS["NAD_1983_UTM_Zone_14N",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-99.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]] ================================================ FILE: data/gis/meta/26915.prj ================================================ PROJCS["NAD_1983_UTM_Zone_15N",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-93.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]] ================================================ FILE: data/gis/meta/5070.prj ================================================ PROJCS["NAD_1983_Contiguous_USA_Albers",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Albers"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-96.0],PARAMETER["Standard_Parallel_1",29.5],PARAMETER["Standard_Parallel_2",45.5],PARAMETER["Latitude_Of_Origin",23.0],UNIT["Meter",1.0]] ================================================ FILE: data/gis/meta/current_ww.shp.xml ================================================ {FE96CD88-E2E3-4F1F-80D8-3DA8C64033D4}2008062511070300FALSE20080625111732002008062511232900Red Hat Enterprise Linux 5; PostGISenShapefile of National Weather Service issued short and long fuse watch/warning/advisories. This file is by no means complete, but covers most of the high impact weather statements.Provide GIS users with something usable without worry about parsing lots of nasty text formats :)REQUIRED: The name of an organization or individual that developed the data set.REQUIRED: The date when the data set is published or otherwise made available for release.Current Watch Warningscurrent_wwvector digital data\\AKRHERZ-VM222\C$\Documents and Settings\akrherz\Desktop\IEM\current_ww.shppublication dateunknownIn workContinually-112.700000-66.56666648.64579818.150459-112.700000-66.56666618.15045948.645798unknownwarnings, nwsNoneNoneShapefileDaryl HerzmannIowa State UniversityAssistant Scientist515 294 5978akrherz@iastate.eduNational Weather ServiceUnclassifiedMicrosoft Windows XP Version 5.1 (Build 2600) Service Pack 3; ESRI ArcCatalog 9.2.4.1420current_ww-112.7-66.56666648.64579818.1504591-112.7-66.56666648.64579818.1504591enFGDC Content Standards for Digital Geospatial MetadataFGDC-STD-001-1998local timeREQUIRED: The person responsible for the metadata information.REQUIRED: The organization responsible for the metadata information.REQUIRED: The mailing and/or physical address for the organization or individual.REQUIRED: The city of the address.REQUIRED: The state or province of the address.REQUIRED: The ZIP or other postal code of the address.REQUIRED: The telephone number by which individuals can speak to the organization or individual.20080625http://www.esri.com/metadata/esriprof80.htmlESRI Metadata ProfileISO 19115 Geographic Information - MetadataDIS_ESRI1.0datasetDownloadable Data0.5000.500002file://\\AKRHERZ-VM222\C$\Documents and Settings\akrherz\Desktop\IEM\current_ww.shpLocal Area Network0.500ShapefileVectorSimplePolygonFALSE137FALSEFALSEG-polygon137GCS_North_American_1983Decimal degrees0.0000000.000000North American Datum of 1983Geodetic Reference System 806378137.000000298.257222GCS_North_American_1983137current_wwFeature Class137FIDFIDOID400Internal feature number.ESRISequential unique whole numbers that are automatically generated.ShapeShapeGeometry000Feature geometry.ESRICoordinates defining the features.ISSUEDISSUEDString12EXPIREDEXPIREDString12TYPETYPEString2GTYPEGTYPEString1SIGSIGString120080625 ================================================ FILE: data/gis/roads.mapinc ================================================ LAYER CONNECTIONTYPE postgis NAME "roads" CONNECTION "user=nobody dbname=postgis host=iemdb-postgis.local" DATA "geom from (select b.type as rtype, b.int1, random() as boid, b.segid, c.cond_code, b.geom from roads_base b, roads_current c WHERE b.segid = c.segid and b.type > 1 ORDER by b.segid DESC) as foo using UNIQUE boid using SRID=26915" STATUS OFF TYPE LINE METADATA "wfs_title" "Iowa Non-Interstates Conditions" "wms_title" "Iowa Non-Interstates Conditions" "wms_srs" "EPSG:4326 EPSG:26915 EPSG:3857" END #FILTER "expire > CURRENT_TIMESTAMP and gtype = 'C'" PROJECTION "init=epsg:26915" END TEMPLATE "roadsq.html" LABELCACHE ON CLASS NAME 'normal' EXPRESSION ([cond_code] = 0 or [cond_code] = 76) STYLE COLOR 153 153 153 SIZE 5 SYMBOL 'circle' END STYLE COLOR 0 0 0 WIDTH 3 ANTIALIAS TRUE END END CLASS NAME 'wet' EXPRESSION ([cond_code] = 1) STYLE COLOR 0 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 0 204 0 WIDTH 3 ANTIALIAS TRUE END END CLASS NAME 'frost' EXPRESSION ([cond_code] >= 3 and [cond_code] <= 14) STYLE COLOR 0 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 240 240 0 WIDTH 3 ANTIALIAS TRUE END END CLASS NAME 'closed' EXPRESSION ([cond_code] = 86) STYLE COLOR 250 250 0 WIDTH 7 ANTIALIAS TRUE END STYLE COLOR 230 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 250 250 0 SIZE 3 SYMBOL 'fill45' END END CLASS NAME 'not advised' EXPRESSION ([cond_code] = 51) STYLE COLOR 0 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 232 95 1 WIDTH 3 ANTIALIAS TRUE END END # SLUSH! CLASS NAME 'pc-mix' EXPRESSION ([cond_code] = 56 or [cond_code] = 27 or [cond_code] = 15) STYLE COLOR 0 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 255 197 197 WIDTH 3 ANTIALIAS TRUE END END CLASS NAME 'mc-mix' EXPRESSION ([cond_code] = 60 or [cond_code] = 31 or [cond_code] = 19) STYLE COLOR 0 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 254 51 153 WIDTH 3 ANTIALIAS TRUE END END CLASS NAME 'cc-mix' EXPRESSION ([cond_code] = 64 or [cond_code] = 35 or [cond_code] = 23) STYLE COLOR 0 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 181 0 181 WIDTH 3 ANTIALIAS TRUE END END # Snow! CLASS NAME 'pc-snow' EXPRESSION ([cond_code] = 39) STYLE COLOR 0 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 153 255 255 WIDTH 3 ANTIALIAS TRUE END END CLASS NAME 'mc-snow' EXPRESSION ([cond_code] = 43) STYLE COLOR 0 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 0 153 254 WIDTH 3 ANTIALIAS TRUE END END CLASS NAME 'cc-snow' EXPRESSION ([cond_code] = 47) STYLE COLOR 0 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 0 0 158 WIDTH 3 ANTIALIAS TRUE END END # CLASS # NAME 'others' # STYLE # COLOR 255 255 0 # WIDTH 3 # ANTIALIAS TRUE # END # END TOLERANCE 50 END #____________________ # We label interstates a bit larger! # LAYER CONNECTIONTYPE postgis NAME "roads-inter" CONNECTION "user=nobody dbname=postgis host=iemdb-postgis.local" DATA "geom from (select b.type as rtype, b.int1, random() as boid, b.segid, c.cond_code, b.geom from roads_base b, roads_current c WHERE b.segid = c.segid and b.type = 1 ORDER by b.segid DESC) as foo using UNIQUE boid using SRID=26915" STATUS OFF TYPE LINE METADATA "wfs_title" "Iowa Interstates Conditions" "wms_title" "Iowa Interstates Conditions" "wms_srs" "EPSG:4326 EPSG:26915 EPSG:3857" END #FILTER "expire > CURRENT_TIMESTAMP and gtype = 'C'" PROJECTION "init=epsg:26915" END TEMPLATE "roadsq.html" LABELCACHE ON CLASS NAME 'normal' EXPRESSION ([cond_code] = 0 or [cond_code] = 76) STYLE COLOR 220 220 220 SIZE 7 SYMBOL 'circle' END STYLE COLOR 0 0 0 WIDTH 5 ANTIALIAS TRUE END END CLASS NAME 'wet' EXPRESSION ([cond_code] = 1) STYLE COLOR 220 220 220 SIZE 7 SYMBOL 'circle' END STYLE COLOR 0 204 0 WIDTH 5 ANTIALIAS TRUE END END CLASS NAME 'frost' EXPRESSION ([cond_code] >= 3 and [cond_code] <= 14) STYLE COLOR 220 220 220 SIZE 7 SYMBOL 'circle' END STYLE #COLOR 152 152 152 COLOR 240 240 0 WIDTH 5 ANTIALIAS TRUE END END CLASS NAME 'closed-int' EXPRESSION ([cond_code] = 86) STYLE COLOR 250 250 0 WIDTH 7 ANTIALIAS TRUE END STYLE COLOR 230 0 0 SIZE 5 SYMBOL 'circle' END STYLE COLOR 250 250 0 SIZE 3 SYMBOL 'fill45' END END CLASS NAME 'not advised' EXPRESSION ([cond_code] = 51) STYLE COLOR 220 220 220 SIZE 7 SYMBOL 'circle' END STYLE COLOR 232 95 1 WIDTH 5 ANTIALIAS TRUE END END # SLUSH! CLASS NAME 'pc-mix' EXPRESSION ([cond_code] = 56 or [cond_code] = 27 or [cond_code] = 15) STYLE COLOR 0 0 0 SIZE 7 SYMBOL 'circle' END STYLE COLOR 255 197 197 WIDTH 5 ANTIALIAS TRUE END END CLASS NAME 'mc-mix' EXPRESSION ([cond_code] = 60 or [cond_code] = 31 or [cond_code] = 19) STYLE COLOR 0 0 0 SIZE 7 SYMBOL 'circle' END STYLE COLOR 254 51 153 WIDTH 5 ANTIALIAS TRUE END END CLASS NAME 'cc-mix' EXPRESSION ([cond_code] = 64 or [cond_code] = 35 or [cond_code] = 23) STYLE COLOR 0 0 0 SIZE 7 SYMBOL 'circle' END STYLE COLOR 181 0 181 WIDTH 5 ANTIALIAS TRUE END END CLASS NAME 'snow' EXPRESSION ([cond_code] = 39) STYLE COLOR 220 220 220 SIZE 7 SYMBOL 'circle' END STYLE COLOR 153 255 255 WIDTH 5 ANTIALIAS TRUE END END CLASS NAME 'snow' EXPRESSION ([cond_code] = 43) STYLE COLOR 220 220 220 SIZE 7 SYMBOL 'circle' END STYLE COLOR 0 153 254 WIDTH 5 ANTIALIAS TRUE END END CLASS NAME 'snow' EXPRESSION ([cond_code] = 47) STYLE COLOR 220 220 220 SIZE 7 SYMBOL 'circle' END STYLE COLOR 0 0 158 WIDTH 5 ANTIALIAS TRUE END END TOLERANCE 50 END ================================================ FILE: data/gis/stations.sym ================================================ SYMBOLSET SYMBOL NAME 'circle' TYPE ELLIPSE Filled TRUE POINTS 1 1 END END SYMBOL TYPE vector POINTS 0 0 6 6 END END SYMBOL NAME 'fill45' TYPE Ellipse FILLED TRUE POINTS 1 1 END # STYLE 1 5 END END SYMBOL NAME "triangle" TYPE vector POINTS 0 4 2 0 4 4 0 4 END END SYMBOL NAME "tornado" TYPE vector FILLED TRUE POINTS 0 0 2 4 4 0 0 0 END END Symbol Name 'airplane' Type PIXMAP Image 'images/airplane.png' Transparent 0 END Symbol Name 'airplane_yellow' Type PIXMAP Image 'images/airplane_yellow.png' Transparent 0 END Symbol Name 'water' Type PIXMAP Image 'images/water.png' Transparent 0 END Symbol Name 'schoolhouse' Type PIXMAP Image 'images/schoolhouse.png' Transparent 1 END Symbol Name 'nws' Type PIXMAP Image 'images/nws.png' Transparent 0 END Symbol Name 'cyclone' Type PIXMAP Image 'images/cyclone.png' Transparent 18 END Symbol Name 'nrcs' Type PIXMAP Image 'images/nrcs.png' Transparent 0 END Symbol Name 'roadcond' Type PIXMAP Image 'images/roadcond.png' END Symbol Name 'n0r-ramp' Type PIXMAP Image 'images/n0r-ramp.png' END Symbol Name 'n0q-ramp' Type PIXMAP Image 'images/n0q-ramp.png' Transparent 0 END Symbol Name 'interstate_shield' Type PIXMAP Image 'interstate-2.png' Transparent 0 END Symbol Name 'us_highway_shield' Type PIXMAP Image 'images/ushwy.png' Transparent 1 END Symbol Name 'state_highway_shield' Type PIXMAP Image 'images/sthwy.png' END Symbol Name 'county_highway_shield' Type PIXMAP Image 'images/ctyhwy.png' Transparent 0 END Symbol Name 'x' Type PIXMAP Image 'images/x.png' Transparent 1 END Symbol Name 'kcci8' Type PIXMAP Image 'images/kcci8.png' # Transparent 1 END Symbol NAME 'iem_logo' TYPE PIXMAP IMAGE 'images/iem_logo.png' TRANSPARENT 0 END Symbol Name 'nws_logo' Type PIXMAP Image 'images/nws.png' Transparent 0 END Symbol Name 'doppler8' Type PIXMAP Image 'images/doppler8.png' Transparent 253 END Symbol Name 'kcci-lsd2007' Type PIXMAP Image 'images/kcci-lsd2007.png' Transparent 0 END Symbol Name 'clrbar' Type PIXMAP Image 'images/clrbar.png' Transparent 17 END END ================================================ FILE: data/gis/symbols/stations.sym ================================================ SYMBOLSET SYMBOL NAME 'circle' TYPE ELLIPSE Filled TRUE POINTS 1 1 END END SYMBOL TYPE vector POINTS 0 0 6 6 END END SYMBOL NAME "triangle" TYPE vector Filled TRUE POINTS 0 4 2 0 4 4 0 4 END END SYMBOL NAME "tornado" TYPE vector FILLED TRUE POINTS 0 0 2 4 4 0 0 0 END END Symbol Name 'airplane' Type PIXMAP Image 'images/airplane.png' Transparent 0 END Symbol Name 'precip' Type PIXMAP Image 'bars/rainbow2.png' #Transparent 0 END Symbol Name 'airplane_yellow' Type PIXMAP Image 'images/airplane_yellow.png' Transparent 0 END Symbol Name 'water' Type PIXMAP Image 'images/water.png' Transparent 0 END Symbol Name 'schoolhouse' Type PIXMAP Image 'images/schoolhouse.png' Transparent 1 END Symbol Name 'nws' Type PIXMAP Image 'images/nws.png' Transparent 0 END Symbol Name 'cyclone' Type PIXMAP Image 'images/cyclone.png' Transparent 18 END Symbol Name 'nrcs' Type PIXMAP Image 'images/nrcs.png' Transparent 0 END Symbol Name 'interstate_shield' Type PIXMAP Image 'interstate-2.png' Transparent 0 END Symbol Name 'us_highway_shield' Type PIXMAP Image 'images/ushwy.png' Transparent 1 END Symbol Name 'state_highway_shield' Type PIXMAP Image 'images/sthwy.png' END Symbol Name 'county_highway_shield' Type PIXMAP Image 'images/ctyhwy.png' Transparent 0 END Symbol Name 'x' Type PIXMAP Image 'images/x.png' Transparent 1 END Symbol Name 'kcci8' Type PIXMAP Image 'images/kcci8.png' # Transparent 1 END Symbol Name 'iem_logo' Type PIXMAP Image 'images/iem_logo.png' Transparent 0 END Symbol Name 'doppler8' Type PIXMAP Image 'images/doppler8.png' Transparent 253 END Symbol Name 'clrbar' Type PIXMAP Image 'images/clrbar.png' Transparent 17 END END ================================================ FILE: deployment/iem-tilecache.service ================================================ [Unit] Description=IEM TileCache Gunicorn backend (/c and /cache) After=network.target Before=httpd.service [Service] Type=simple User=apache Environment=PATH=/opt/miniconda3/envs/prod/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin ExecStartPre=/usr/bin/test -x /opt/miniconda3/envs/prod/bin/gunicorn ExecStartPre=/usr/bin/test -x /opt/iem/deployment/start_tc_wsgi.sh ExecStart=/opt/iem/deployment/start_tc_wsgi.sh Restart=on-failure RestartSec=5 SyslogIdentifier=iem-tc StandardOutput=syslog StandardError=syslog [Install] WantedBy=multi-user.target ================================================ FILE: deployment/start_tc_wsgi.sh ================================================ #!/bin/bash # Start the dedicated Gunicorn backend for TileCache (/c and /cache). # # Mirrors the legacy iemwsgi_tc daemon sizing while moving the Python app # server off Apache. PORT=${1:-9081} CONDA_PREFIX=/opt/miniconda3/envs/prod MAX_REQUESTS=${MAX_REQUESTS:-10000000} MAX_REQUESTS_JITTER=${MAX_REQUESTS_JITTER:-5000} # Use the conda env's binaries directly rather than relying on interactive # shell activation (systemd runs non-interactive shells). export PATH="${CONDA_PREFIX}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin" # Attempt to get some logging when bad things happen export PYTHONFAULTHANDLER=1 # Keep request-count recycling enabled to cap Python memory leaks. Gunicorn # rotates individual workers instead of restarting an embedded Apache stack. exec "${CONDA_PREFIX}/bin/gunicorn" \ --bind "127.0.0.1:${PORT}" \ --no-control-socket \ --workers 3 \ --worker-class gthread \ --threads 15 \ --backlog 2048 \ --pythonpath /opt/iem/pylib/ \ --max-requests "$MAX_REQUESTS" \ --max-requests-jitter "$MAX_REQUESTS_JITTER" \ --timeout 60 \ --graceful-timeout 60 \ --error-logfile - \ --capture-output \ --log-level warn \ --log-syslog \ --log-syslog-facility local1 \ iemweb.tilecache_dispatch:application ================================================ FILE: deployment/symlink_manager.py ================================================ """Attempt to manage the disaster that is IEM symlinking""" from pathlib import Path from pyiem.util import logger LOG = logger() # LINK , TARGET M2 = "/mnt/mesonet2" PAIRS = [ ["/mesonet/data/merra2", f"{M2}/data/merra2"], ["/mesonet/nawips", f"{M2}/gempak"], ["/mesonet/wepp", f"{M2}/idep"], ["/mesonet/ARCHIVE/gempak", f"{M2}/longterm/gempak"], ["/mesonet/ARCHIVE/raw", f"{M2}/longterm/raw"], ["/mesonet/ARCHIVE/rer", f"{M2}/ARCHIVE/rer"], ["/mesonet/data/conus404", f"{M2}/data/conus404"], ["/mesonet/data/dotcams", f"{M2}/data/dotcams"], ["/mesonet/data/era5", f"{M2}/data/era5"], ["/mesonet/data/era5_china", f"{M2}/data/era5_china"], ["/mesonet/data/era5_europe", f"{M2}/data/era5_europe"], ["/mesonet/data/era5_sa", f"{M2}/data/era5_sa"], ["/mesonet/data/gempak", f"{M2}/data/gempak"], ["/mesonet/data/iemre", f"{M2}/data/iemre"], ["/mesonet/data/iemre_china", f"{M2}/data/iemre_china"], ["/mesonet/data/iemre_europe", f"{M2}/data/iemre_europe"], ["/mesonet/data/iemre_sa", f"{M2}/data/iemre_sa"], ["/mesonet/data/prism", f"{M2}/data/prism"], ["/mesonet/data/incoming", f"{M2}/data/incoming"], ["/mesonet/data/madis", f"{M2}/data/madis"], ["/mesonet/data/model", f"{M2}/data/model"], ["/mesonet/data/mrms", f"{M2}/data/mrms"], ["/mesonet/data/ndfd", f"{M2}/data/ndfd"], ["/mesonet/data/nldas", f"{M2}/data/nldas"], ["/mesonet/data/smos", f"{M2}/data/smos"], ["/mesonet/data/stage4", f"{M2}/data/stage4"], ["/mesonet/data/text", f"{M2}/data/text"], ["/mesonet/ldmdata", f"{M2}/ldmdata"], # May fail if node writes data ["/mesonet/share/cases", f"{M2}/share/cases"], ["/mesonet/share/climodat", f"{M2}/share/climodat"], ["/mesonet/share/features", f"{M2}/share/features"], ["/mesonet/share/iemmaps", f"{M2}/share/iemmaps"], ["/mesonet/share/lapses", f"{M2}/share/lapses"], ["/mesonet/share/pickup", f"{M2}/share/pickup"], ["/mesonet/share/pics", f"{M2}/share/pics"], ["/mesonet/share/present", f"{M2}/share/present"], ["/mesonet/share/usage", f"{M2}/share/usage"], ["/mesonet/share/windrose", f"{M2}/share/windrose"], ["/mesonet/home", f"{M2}/home"], ] def workflow(link: Path, target: Path): """Do things""" if not target.exists(): LOG.info("ERROR: link target: %s is not found", target) return if not link.is_symlink() and link.is_dir(): LOG.info("ERROR: symlink: %s is already a directory!", link) return if link.is_symlink(): oldtarget = link.resolve() if oldtarget == target: return link.unlink() LOG.info("%s -> %s", link, target) link.symlink_to(target) def main(): """Go Main""" # Ensure some base folders exist for mysubdir in ["share", "ARCHIVE", "data"]: path = Path("/mesonet") / Path(mysubdir) if not path.is_dir(): path.mkdir(parents=True) # Quasi dynamic generation of /mesonet/ARCHIVE/data/YYYY links if not Path("/mesonet/ARCHIVE/data").is_dir(): Path("/mesonet/ARCHIVE/data").mkdir(parents=True) for year in range(1893, 2015): link = Path("/mesonet/ARCHIVE/data") / str(year) target = Path(f"/mnt/archive32/ARCHIVE/data/{year}") workflow(link, target) for year in range(2015, 2019): link = Path("/mesonet/ARCHIVE/data") / str(year) target = Path(f"/mnt/archive5/ARCHIVE/data/{year}") workflow(link, target) for year in range(2019, 2024): link = Path("/mesonet/ARCHIVE/data") / str(year) target = Path(f"/mnt/archive32/ARCHIVE/data/{year}") workflow(link, target) for year in range(2024, 2025): link = Path("/mesonet/ARCHIVE/data") / str(year) target = Path(f"/mnt/archive5/ARCHIVE/data/{year}") workflow(link, target) for year in range(2025, 2027): link = Path("/mesonet/ARCHIVE/data") / str(year) target = Path(f"/mnt/archive33/ARCHIVE/data/{year}") workflow(link, target) for link, target in PAIRS: workflow(Path(link), Path(target)) if __name__ == "__main__": main() ================================================ FILE: docs/datasets/afos.md ================================================ ## NWS Text Product Archive ### Summary This archive consists of raw ASCII text products issued by the National Weather Service. Some places on the website will refer to this as "AFOS", which is an archaic old abbreviation associated with this dataset. The realtime source of this dataset is the processing of text products sent over the NOAAPort SBN, but archives have been backfilled based on what exists at NCEI and also some archives provided by the University of Wisconsin. * __Download Interface__: [IEM On-Demand](https://mesonet.agron.iastate.edu/wx/afos/) * __Spatial Domain__: Generally US NWS Offices that issue text products. * __Temporal Domain__: Some data back to 1996, but archive quality and completeness greatly improves for dates after 1998. ### Justification for processing While the Internet provides many places to view current NWS Text Products, archives of these are much more difficult to find. One of the primary goals of the IEM website is to maintain stable URLs, so when links are generates to NWS Text Products, they need to work into the future! Many of these text products have very useful information in them for researchers and others in the public. ### Other Sources of Information The [NCEI Site](https://www.ncei.noaa.gov) would be an authoritative source, but their archives of this data are very painful to work with. There are a number of other sites that have per-UTC day files with some text products included. For example [Oklahoma Mesonet](https:/mesonet.org/data/public/noaa/text/archive/). ### Processing and Quality Control Generally, some quality control is done to ensure that the data is ASCII format and not filled with control characters. There are also checks that product timestamps are sane and represent a timestamp that is close to reality. For example over the NOAAPort SBN feed, there is about one product per day that is a misfire or some other error that is not allowed to be inserted into the database. This database culls some of the more frequently issued text products. The reason being to save space and some of the text products are not very appropriate for long term archives. The most significant deletion are the SHEF products, which would overwhelm my storage system if I attempted to save the data! [The script](https://github.com/akrherz/iem/blob/main/scripts/dbutil/clean_afos.py) that does the database culling each day contains the exact AWIPS IDs used for this cleaning. ### Frequently Asked Questions 1. How can I bulk download the data? Sadly, this is not well done at the moment. The [WX AFOS](https://mesonet.agron.iastate.edu/wx/afos/) is about the best option as it has a "Download Text" button. 2. Please describe any one-offs within the archive? The `RRM` product is generally SHEF and thus was culled from the database to conserve space. An IEM user requested that this product be retained, so the culling of it stopped on 31 March 2023. So this product's archive only dates back till then. ================================================ FILE: docs/datasets/climodat.md ================================================ ## IEM Climodat Reports and Data ### Summary This document describes the once daily climate dataset that provides observations and estimates of high and low temperature, precipitation, snowfall, and snow depth. Parts of this dataset have been curated over the years by a number of Iowa State employees including Dr Shaw, Dr Carlson, and Dr Todey. * __Download Interface__: [IEM On-Demand](https://mesonet.agron.iastate.edu/request/coop/fe.phtml) * __Spatial Domain__: United States * __Temporal Domain__: varies by station * __Variables Provided__: Once daily high and low temperature, precipitation, snowfall, snow depth ### Justification for processing The most basic and important long term climate record are the once daily reports of high and low temperature along with precipitation and sometimes snow. The most commonly asked question of the IEM datasets are climate related, so curating a long-term dataset of daily observations is necessary. ### Other Sources of Information A great source of much of the same type of data is [Regional Climate Centers ACIS](http://www.rcc-acis.org/). The complication when comparing IEM Climodat data to other sources is the difference in station identifiers used. The history of station identifiers is long and complicated. The National Center for Environmental Information (NCEI) has made strides in attempting to straighten the identifiers out. This continues to be complicated as the upstream data source of information uses a completely different set of identifiers known as National Weather Service Location Identifiers (NWSLI), which are different than what NCEI or the IEM uses for our climate datasets. ### Processing and Quality Control There is nothing easy or trivial about processing or quality control of this dataset. After decades of work, plenty of issues remain. Having human observers be the primary driver of this dataset is both a blessing and a curse. The good aspects include the archive dating back to the late 1800s for some locations and relatively high data quality. The bad aspects include lots of metadata issues due to observation timing, station moves, and equipment siting. The primary data source for this dataset is the National Weather Service COOP observers. These reports come to the IEM over a variety of paths: * Realtime reports found in NWS SHEF Text Products, processed in realtime by the IEM * Via manually downloaded data archives provided by NCEI * Via web services provided by RCC ACIS The merging of these four datasets creates a bit of a nightmare to manage. Snowfall and snow depth data is always problematic. First, lets clarify what the terms mean. "Snowfall" is the amount of snow that fell from the sky over the previous 24 hour period. "Snow depth" is the amount of snow measured to be on the ground due to previous snowfalls. These numbers may sometimes contradict with snowfall amounts being larger than snow depth due to melting and/or compaction. Care should be used when analyzing the snowfall and snow depth reports. ### Frequently Asked Questions 1. This data contains 'Statewide' and 'Climate District' observations, where do they come from? The IEM produces gridded analyses of observed variables. A GIS-style weighted spatial sampling is done from this grid to derive averaged values over geographies like states and climate districts. Of course, when you average something like precipitation over a large area, you end up with rates that are lower than peak station rates and also with more precipitation events than individual stations within the region. 1. The download provides only a date, what time period does this represent? Almost always, this date *does not* represent a local calendar date's worth of data. This date represents the date on which the observation was taken. Typically, this observation is taken at about 7 AM and so represents a 24 hour period prior to 7 AM. Explicitly providing the time of observation is a future and necessary enhancement to this dataset, but just tricky to do. Some observation locations have switched times over the years and some even observe 24 hour precipitation totals at a different time than the temperature values. Nothing is ever easy with this dataset... 1. Where does the radiation data come from? The NWS COOP Network does not provide observations of daily solar radiation, but this variable is extremely important to many folks that use this information for modelling. As a convience, the IEM processes a number of reanalysis datasets and produces point sampling from the gridded information to provide "daily" radiation totals. A major complication is that the 'daily' COOP observations are typically at 7 AM and the gridded solar radiation data is extracted on close to a local calendar day basis. In general, the 7 AM value is for the previous day. 1. Where does the non-radiation data come from? This information is primarily driven by the realtime processing of NWS COOP observations done by the IEM. For data older than the past handful of years, it is taken from the NCEI databases and now the ACIS web services. Some manual work is done to meld the differences in site identifiers used between the various online resources. 1. How do I know which variables have been estimated and which are observations? The download interface for this dataset provides an option to include a flag for which observations have estimated temperatures or precipitation. Presently, a general flag is provided for both high and low temperature and no flag is provided for the snowfall and snow depth information. ================================================ FILE: docs/datasets/iemre.md ================================================ ## IEM Reanalysis ### Summary The IEM Reanalysis dataset is a daily gridded product combining a number of datasets into one product hopefully void of missing values. In some cases, data is interpolated and in other cases, the data is resampled from another grid. Keeping the workflow doing is a daily challenge due to changes in various input datasets and quirks with datasets over time. * __Download Interface__: [IEM On-Demand](https://mesonet.agron.iastate.edu/iemre/) * __Spatial Domain__: ... * __Temporal Domain__: ... ### Justification for processing A consistent and complete gridded analysis enables many downstream products and applications. Single point observations are of higher quality, but often have gaps and their representativity varies depending on many factors. There are many alternative sources available today with similiar data, but it is good to have a product under IEM workflow control that is not subject to outages and data service dropouts. For example, government shutdowns. ### Other Sources of Information There are many. NARR, ERA5, and the list goes on. ### Processing and Quality Control To be written... ### Frequently Asked Questions 1. When are daily fields updated? Well, there is a long story! 1. This is another question I have? Well, there is another story? ================================================ FILE: docs/datasets/metar.md ================================================ ## ASOS/AWOS Global METAR Archives ### Summary The primary format that worldwide airport weather station data is reported in is called METAR. This format is somewhat archaic, but well known and utilized in the community. The data is sourced from a number of places including: [Unidata IDD](https://www.unidata.ucar.edu/projects/#idd), [NCEI ISD](https://www.ncdc.noaa.gov/isd), [MADIS One Minute ASOS](https://madis.ncep.noaa.gov/madis_OMO.shtml), and [NCEI GHCHh](https://www.ncei.noaa.gov/products/global-historical-climatology-network-hourly). The weather stations included are typically called "Automated Surface Observation System (ASOS)". The term "Automated Weather Observation System (AWOS)" is often used inter-changably. * __Download Interface__: [IEM On-Demand](https://mesonet.agron.iastate.edu/request/download.phtml) * __Spatial Domain__: Worldwide * __Temporal Domain__: 1900-present ### Justification for processing The highest quality weather information comes from the ASOS sites. These stations get routine maintenance, considerable quality control, and is the baseline hourly interval dataset used by all kinds of folks. The data stream processed by the IEM contains global stations, so extending the ingest to the entire data stream was not significant effort. ### Other Sources of Information [NCEI Integrated Surface Database (ISD)](https://www.ncdc.noaa.gov/isd) is likely the most authoritative source of this information. ### Processing and Quality Control A Python based ingestor using the metar package processes this information into the IEM database. ### Frequently Asked Questions 1. Why is precipitation data all missing / zero for non-US locations? It is the IEM's understanding that precipitation is not included in the global data streams due to previous data distribution agreements. The precipitation data is considered of very high value as it can be used to model and predict the status of agricultural crops in the country. Such information could push commodity markets. For the present day, other satellite datasets likely negate some of these advantages, but alas. 2. How are "daily" precipitation totals calculated? In general, the ASOS stations operate in local standard time for the entire year round. This has some implications with computation of various daily totals as during daylight saving time, the calendar day total will represent a 1 AM to 1 AM local daylight time period. For the context of this METAR dataset, not all METAR reporting sites will generate a total that can be used for assignment of a calendar day's total. So the IEM uses a number of approaches to arrive at this total. * A script manually totals up the hourly precipitation reports and computes a true local calendar day total for the station, this total may later be overwritten by either of the below. * A real-time ingest process gleans the daily totals from the Daily Summary Message (DSM) issued by some ASOS sites. * A real-time ingest process gleans the daily totals from the Climate Report (CLI) that is issued for some ASOS sites by their respective local NWS Forecast Offfice. Not all stations have DSM and/or CLI products, so the manual totaling provides a minimum accounting. The complication is that this total does not cover the same period that a CLI/DSM product does. So complicated! 3. Please explain the temperature units, storage and processing. This is why we can not have nice things. The following discussion generally applies to the US observation sites. No matter what you see in various data feeds, the ASOS stations internally store their temperatures in **whole degree Fahrenheit**. The issues happen when the station transmits the data in whole degree Celsius and thus not have enough precision to covert back to Fahrenheit. For example, if the station observed a 78F temperature and then transmitted a 26C value, that 26C value converts back to 78.8F, which rounds to 79F. And down the rabbit-hole we go! The IEM's archive of ASOS/METAR data comes from 3 main sources and some minor auxillary ones. The main source is the NOAA satellite feed, called NOAAPort. This feed provides data in METAR format, so the transmitted units are always whole degree Celsius, but sometimes the METAR `T-group` is included, so there is enough added precision to reliabily convert back to whole degree Fahrenheit. The IEM's processing attempts to prioritize those METARs that include the `T-group`, so that reliable Fahrenheit storage can occur. The next main source is from the MADIS 5-minute ASOS dataset, previously called High Frequency METAR. This data feed has a significant issue whereby the transmitted data from the FAA to the NWS is only in whole degree Celsius. Such data can not be reliably converted back to whole degree Fahrenheit. For this reason, the IEM database stores these values as missing and they are not included in the data download. BUT, for those that really want this information, these values are included in the IEM-encoded raw METAR string that you can download with the data. You can find further discussion on this [IEM News Item](https://mesonet.agron.iastate.edu/onsite/news.phtml?id=1290). The third main source is from the [NCEI ISD](https://www.ncdc.noaa.gov/isd). At this time, there are no known issues with the temperature data in this feed being reliable for whole degree Fahrenheit. ================================================ FILE: docs/datasets/template.md ================================================ ## IEM Template [WIP] ### Summary This is... * __Download Interface__: [IEM On-Demand](https://mesonet.agron.iastate.edu) * __Spatial Domain__: ... * __Temporal Domain__: ... ### Justification for processing This is... ### Other Sources of Information This is... ### Processing and Quality Control | First Header | Second Header | | ------------- | ------------- | | Content Cell | Content Cell | | Content Cell | Content Cell | ### Frequently Asked Questions 1. Where does the radiation data come from? Well, there is a long story! 1. Where does the non-radiation data come from? Well, ther eis another story? ================================================ FILE: docs/datasets/vtec.md ================================================ # NWS Valid Time Extent Code (VTEC) Archives ## Summary The National Weather Service uses a rather complex and extensive suite of products and methodologies to issue watch, warnings, and advisories (WWA). Back in the 1990s and early 2000s, the automated processing of these products was extremely difficult and rife with errors. To help with automated parsing, the NWS implemented a system called Valid Time Extent Code (VTEC) which provides a more programatic description of what an individual WWA product is doing. The implementation of began in 2005 and was mostly wrapped up by 2008. The IEM attempts to do high fidelity processing of this data stream and has a number of Internet unique archives and applications to view this information. * __Download Interface__: [Shapefile/KML Download](https://mesonet.agron.iastate.edu/request/gis/watchwarn.phtml) * __Spatial Domain__: United States, including Guam, Puerto Rico and some other islands * __Temporal Domain__: Most WWA types back to 2008 or 2005, an archive of Flash Flood Warnings goes back to 2002 or so, and Tornado / Severe Thunderstorm Warnings goes back to 1986 ## Justification for processing NWS issued WWA alerts are an important environmental hazard dataset and has broad interest in the research and insurance industries. Even in 2017, there are very few places that you can find long term archives of this information in usable formats. ## Other Sources of Information The [National Center for Environmental Information](https://www.ncei.noaa.gov) has raw text product archives that do not contain processed atomic data of the actual WWA alerts. So the user is left to the adventure of text parsing the products. Otherwise, it is not clear if any other archive exists on the Internet of this information. ## Processing and Quality Control The [pyIEM](https://github.com/akrherz/pyIEM) python package is the primary code that does the text parsing and databasing of the WWA products. A large number of unit tests exist against the various variations and quirks found with processing the WWA data stream since the mid 2000s. New quirks and edge cases are still found today with minor corrections made to the archive when necessary. The IEM continuously alerts and annoys the NWS when various issues are found, hoping to get the NWS to correct their products. While it has been a long and frustrating process, things do eventually get fixed leading to more robust data archives. The pyIEM parsers send emails to the IEM developer when issues are found. The parser alerts when the following errors are encountered: * VTEC Event IDs (ETNs) being used that are out of sequential order. * Warning product segments are missing or have invalid Universal Geographic Code (UGC) encoding * Product segment has invalid VTEC encoding * Polygons included in the warning are invalid or counterclockwise * Timestamps are formatted incorrectly * The UGC / VTEC sequence of a particular product contains logical errors, for example a UGC zone silently drops out or into a warning. * Products are expired outside of the acceptable temporal bounds * Any other type of error and/or code bug that caused a processing fault ## Frequently Asked Questions 1. Please fully describe the schema used within the downloaded shapefiles. Grab some coffee and headache medicine as I am going to try to explain how the IEM processes these events into the database. The first concept to understand is that when the NWS issues a Watch, Warning, Advisory (WaWA) event, this event undergoes a lifecycle. The NWS can issue updates that modify the start and end times of the event and the spatial extent of the event. They can also do upgrades on the event, for example moving from a watch into a warning. The IEM database does not necessary fully document the event's lifecycle, but provides the metadata for the last known state of the event. For the context of IEM provided shapefiles, here is a discussion of what each DBF column represents. We will go into an example afterwards attempting to illustrate what each column means. But first, the timestamps. The presented timestamps are always in UTC timezone. The timestamp is represented by a 12 character string in the form of year, month, day, 24-hour,minute. To my knowledge, there is no timestamp data type in DBF, so this is the pain we have to live with. | DBF Column | Type | Description | | ------------- | ------------- | ----- | | WFO | 3 Char | This is the three character NWS Office/Center identifier. For CONUS locations, this is the 4 character ID dropping the first `K`. For non-CONUS sites, this is the identifier dropping the `P`. | | ISSUED | 12 Char | This timestamp represents the start time of the event. When an event's lifecycle begins, this issued value can be updated as the NWS issues updates. The value presented represents the last known state of the event start time.| | EXPIRED | 12 Char | Similiar to the ISSUED column above, this represents the products event end time. Again, this value is updated as the event lifecycle happens with updates made by the NWS. | | INIT_ISS | 12 Char | This is timestamp of the NWS Text Product that started the event. This timestamp is important for products like Winter Storm Watch, which have a begin time a number of days/hours into the future, but are typically considered to be in effect at the time of the text product issuace. Yeah, this is where the headaches start. This timestamp can also be used to form a canonical URL back to the IEM to fetch the raw NWS Text for this event. It is __not__ updated during the event's lifecycle. | | INIT_EXP | 12 Char | Similiar to `INIT_ISS` above, this is the expiration of the event denoted with the first issuance of the event. It is __not__ updated during the event's lifecycle. | | PHENOM or TYPE | 2 Char | This is the two character NWS identifier used to denote the VTEC event type. For example, `TO` for Tornado and `SV` for Severe Thunderstorm. A lookup table of these codes exists [within pyiem library](https://github.com/akrherz/pyIEM/blob/main/src/pyiem/nws/vtec.py). | | SIG | 1 Char | This is the one character NWS identifier used to denote the VTEC significance. The same link above for `PHENOM` has a lookup table for these. | | GTYPE | 1 Char | Either `P` for polygon or `C` for county/zone/parish. The shapefiles you download could contain both so-called storm-based (polygon) events and traditional county/zone based events. | | ETN | Int | The VTEC event identifier. A tracking number that should be unique for this event, but sometimes it is not. Yes, more headaches. Note that the uniqueness is not based on the combination of a UGC code, but the issuance center and a continuous spatial region for the event. | | STATUS | 3 Char | The VTEC status code denoting the state the event is during its life cycle. This is purely based on any updates the event got and not some logic on the IEM's end denoting if the event is in the past or not. | | NWS_UGC | 6 Char | For county,zone,parish warnings `GTYPE=C`, the Universal Geographic Code that the NWS uses. Sadly, this is not exactly FIPS. | | AREA_KM2 | Number | The IEM computed area of this event, this area computation is done in Albers (EPSG:9311). | | UPDATED | 12 Char | The timestamp when this event's lifecycle was last updated by the NWS. | | HV_NWSLI | 5 Char | For events that have H-VTEC (Hydro VTEC), this is the five character NWS Location Identifier. | | HV_SEV | 1 Char | For events that have H-VTEC (Hydro VTEC), this is the one character flood severity __at issuance__. | | HV_CAUSE | 2 Char | For events that have H-VTEC (Hydro VTEC), this is the two character cause of the flood. | | HV_REC | 2 Char | For events that have H-VTEC (Hydro VTEC), this is the code denoting if a record crest is expected __at issuance__. | | EMERGENC | Boolean | Based on unofficial IEM logic, is this event an "Emergency" at any point during its life cycle. | | POLY_BEG | 12 Char | In the case of polygons (GTYPE=P) the UTC timestamp that the polygon is initially valid for. | | POLY_END | 12 Char | In the case of polygons (GTYPE=P) the UTC timestamp that the polygon expires at. | | HAILTAG | Number | The IBW hail size tag (inches). This is only included with the (GTYPE=P) entries as there is a 1 to 1 association between the tags and the polygons. If you do not include SVS updates, it is just the issuance tag. | | WINDTAG | Number | The IBW wind gust tag (MPH). See HAILTAG. | | TORNTAG | 16 Char | The IBW tornado tag. See HAILTAG. | | DAMAGTAG | 16 Char | The IBW damage tag. See HAILTAG. | | PROD_ID | 36 Char | Issuance text. IEM identifier used to uniquely (99% of the time) identify NWS Text Products. The value can be passed to ``https://mesonet.agron.iastate.edu/p.php?pid=PROD_ID`` for a website viewer or against the IEM API service ``https://mesonet.agron.iastate.edu/api/1/nwstext/PROD_ID``. | | FCSTER | 24 Char | The product signature, which is often the forecaster who issued the event. | 1. I notice entires with an `expire` timestamp before the `issue` timestamp. How can this be? Oh my, buckle up for some confusion. The first point in this space is that our database represents the most recent snapshot of the given VTEC event during its life cycle. The life cycle includes the issuance to its death via a cancels, expiration, or upgrade to a different VTEC event. To illustrate the evolution of the database fields with a VTEC event lifecycle, please consider this example. At noon on 19 March 2019, NWS Des Moines `wfo=DMX` issues a Winter Storm Watch `phenom=WS` `sig=A` for Story County (`nws_ugc=IAZ048`). This watch goes into effect at 6 PM (tomorrow, 20 March) until 6 AM 21 March. The storm is a day away yet... The database entry looks like so: | STATUS | ISSUE | INIT_ISS | EXPIRE | INIT_EXP | | --- | --- | --- | --- | --- | | NEW | 201903202300 | 201903171700 | 201903211100 | 201903211100 | Now tomorrow comes and the NWS needs to decide what to do with the watch prior to 6 PM, since these type of watches can not reach their issuance time without either being cancelled or upgraded. So at 5 PM, the NWS decides to issue a Winter Storm Warning. Now the database entry for the __watch__ looks like so: | STATUS | ISSUE | INIT_ISS | EXPIRE | INIT_EXP | | --- | --- | --- | --- | --- | | UPG | 201903202300 | 201903171700 | __201903202200__ | 201903211100 | See how the `EXPIRE` column is now less than the `ISSUE` column, but the `INIT_ISS` and `INIT_EXP` columns are unchanged to hopefully help the end user deal with this situation. You have life choices to make on how to deal with this situation. In general, the watch _practically_ is in effect once the NWS issued it, regardless of when the actual bad weather is going to start. So the recommendation is to use the `INIT_ISS` column as the watch start time and the `EXPIRE` as the watch end time, but this logic is totally at your discretion. 1. How do Severe Thunderstorm, Flash Flood, or Tornado warnings have VTEC codes for dates prior to implementation? Good question! A number of years ago, a kind NWS manager provided a database dump of their curated WWA archive for dates between 1986 and 2005. While not perfect, this archive was the best/only source that was known at the time. The IEM did some logic processing and attempted to back-compute VTEC ETNs for this archive of warnings. The database was atomic to a local county/parish, so some logic was done to merge multiple counties when they spatially touched and had similiar issuance timestamps. Again from the above, automated machine parsing of the raw text is next to impossible. The ETNs were assigned as a convience so that various IEM apps and plots would present this data online. 1. The database has Weather Forecast Offices (WFOs) issuing WWA products for dates prior to the office even existing? How can this be!?!? Yeah, this is somewhat poor, but was done to again provide some continuity with current day operations. The archive database provided to the IEM did not contain the issuance forecast office, so without a means to properly attribute these, the present day WFOs were used. This issue is rarely raised by IEM users, but it is good to document. Maybe someday, a more authoritative archive will be made and these old warnings and be assigned to the various WSOs, etc that existed at the time. 1. What are the VTEC phenomena and significance codes? The phenomena code (two characters) and significance code (one character) denote the particular WWA hazzard at play with the product. The [NWS VTEC Site](https://www.weather.gov/vtec/) contains a one pager PDF that documents these codes. The NWS uses these codes to color encode their WAWA Map found on their homepage. You can find a lookup reference table of these codes and colors, [see pyiem library](https://github.com/akrherz/pyIEM/blob/main/src/pyiem/nws/vtec.py). 1. How do polygon warnings exist in the IEM archive prior to being official? The NWS offices started experimenting with polygons beginning in 2002. These polygons were included with the warnings, but sometimes were not geographically valid and/or leaked well outside of a local office's CWA bounds. On 1 October 2007, these polygons became the official warning for some VTEC types. In general, the IEM's data ingestor attempts to save these polygons whenever found. 1. What is the source of Alaska Marine VTEC events? For various convoluted reasons, Alaska WFOs do not issue full blown VTEC enabled products for their Marine Zones. Instead, somewhat cryptic headlines are generated within their `CWF` and `OFF` products that create faked VTEC events for their marine zones. On 4 January 2025, the IEM created a workflow that attempts to process these into somewhat spatial/temporally coherent VTEC events. At the time, an evaluation was done on a similar fake VTEC generation that was being done by the NWS within its CAP messages. This was found to be lacking due to very crude event identifier generation. So the IEM processing runs in real-time and these events were backfilled into years dating back to 2005. Further back processing was not straight forward due to complexities with the raw text. Some processing quirks include VTEC events are not permitted to cross year boundaries and there is no "in the future" logic that attempts to glean from the text if the given event is not yet active. Rewording, if the `CWF` or `OFF` text says "Gale Warning", the assumption is that the "Gale Warning" is now in effect. ================================================ FILE: docs/deployment/vendor-static-assets.md ================================================ # Vendor Static Assets Deployment This document explains how assets under /vendor/ are provided to the webroot. ## Why This Exists A number of IEM maintained repos use common javascript / CSS libraries. Since it is nice to do offline development and for patching, a simple github vendor repo is maintained and synced to /opt/vendor within the webfarm nodes. ## Source of Truth - Upstream repo: [akrherz/vendor](https://github.com/akrherz/vendor) - Version pinning policy: None - Integrity/verification policy: None ## Provisioning Model 1. The github repo is cloned to /opt/vendor 2. That same repo contains an apache configuration snippet that mounts the repo to answer /vendor/ URLs within other repos. ## Validation It is safe for code review to assume resources are properly references with /vendor/ mounted URI paths. ================================================ FILE: docs/meetings.md ================================================ Assorted Meeting Notes ===== 14 Jan 2020 Dr Takle, Iowa Climate Assessment ---- - Seeking to have first Iowa specific climate assessment report made - Just to note that Iowa is one of the few states without sig orography - maps can be of trends in the 9 climate regions - orography allows us to downscale more easily (?) - [ ] would like to see some sort of humidity variable in climodat - review Indiana's well done assessment - Seek a report on Iowa's current human health related to climate - would be nice to talk some about ozone, but am unsure of data sources 30 Aug 2018 Monsanto ----- - Taylor, Todey, Arritt, Dohleman, Colman - 10-11 Oct in St Louis is the ag-mesonets workshop - Dicamba label says to not spray in an inversion - when forecast probabilities are <10% or >90%, accuracy is 93% - code for spraying app is in the public domain - sprayer height is 24 inches above canopy - spraying in the morning goes with the probabilities, not so in afternoon - app has some spraying metadata management help - does logging of the pin drops exist, likely not - hours below temperature per year would be helpful - wind speed does not fully descriminate inversion presence 29 Mar 2016 Syngenta Discussion at Slater ---- - Joe Colleti, Bill Bevis, Kendall Lamkey, Joe Byrun, Greg Dudan, Homer Coden Josh Larson, Paul Travis, Danny Sign, Jim Reece, Tom Warner, Chad Geater Craig Davis, Mike Lorsan, Caroline Lawrence, James Coyle - Conference calls left off with need to resolve access and mechanics of how this would work out. - Syngenta has roughtly relationships with 15-20 Universities per 12-18 months - Never known transfer of actual data to another university though - Discussion about what the NSF funded midwest data hub looks like - It has 8 spokes, one of which is digitial Ag, another is Food, Energy, Water - The hub has the hope of collaborating with industry partners - the hub is a blanket umbrella for other projects to get funded with - The IP rights of such a collaboration are not necessarily defaulted to ISU - The hope is to get the lawyers to approve of this activity only once - There are two proposals in consideration as a part of the data hub spoke - USDA has a new foundation called FAR to fund stuff - NSF proposal cycle has deadlines for next Jan, Feb - the hope is that these proposals would be coming from blessing by the hub spokes and not rouge proposals - There are always proposal deadlines for things to consider - There are collaboration opportunities withe computer engineering and others to deal with computational tasks - Need to have a means to lure faculty into this that may have an interest - Syngenta has interest in persuing external funding - We'll be having breakouts today to further discuss things - I went to the Environmental Data Group with Joe Colleti and Chad Geater - Field weather stations have standard variables - They use gridded 0.25 degree data from the ECMWF (proprietary) - need to tease out drought tolerance variables from obs - unsure what exists for soybean data - a recent push was to push for GPS coordinates on all their data - Unsure what management data exists - they may have some soil temperature data - an issue is finding agreement between environmental datasets - Data needed depends on the science, or is it vice versa - A key aspect is where the compute runs that has to access the data - Likely can not determine logistics today - Not all of their data is relational, in fact, likely not even a majority - What is the quality of metadata? - They are working on data tagging. - Some of the datasets have changes in how the data was observed over the years - Would it be good to embed people within each institution to help with this collaboration? - A vast majority of the data would take effort to move and curate, not ready - Lots of data already exists, no energy to start processing it - Cateloging the data is a big time sync - talk about a project called G2F taking field measurements - G2F is having good luck with standardizing protocols - Need to make sure questions being posed are not redundant between groups - There is a some NSF data hub meeting on May 15-16 - Do ISU corn and soybean faculty currently collaborate? Sort of. - Not everybody at ISU is into team science :) - Talk about the structure of an ISU / Syngenta symposium ================================================ FILE: docs/nmp.md ================================================ National Mesonet Project Meetings ====== 25 Jan 2017 SGT Conference call at AMS Seattle ---- - Background of SGT, primarly with NASA (400 out of 550m $US) - South Alabama Mesonet presentation - Florida AWN presented some details on their network - reliability reports are not on calendar months, but period of days - adding new stations does not mean an immediate increase in funding ================================================ FILE: docs/soilmoisture.md ================================================ Soil Moisture Network Meeting Notes =================================== 17 May 2021 :: Planning with new hire Mark ----- - [x] "Hourly Radiation" on pulldown is vague. - [ ] Frozen Soil on Sortable Currents is in poor taste. - There's a new Fruit + Vegetable hire coming, which may take over vineyards. - Decided to barry the soilvue to 12 inches depth and place a reflectometer at the 4inch depth. - [ ] no close icon on the popup for homepage map - [x] daily precip plotting does not work 13 Jan 2020 :: Planning meeting with Dr Lamkey ---- - Continued desire to get time with the farm managers - Dr Honeyman appears to be onboard with 1/2 weather station replacement - "Redesign Iowa Agriculture" is a theme to hit on. - Small companies in Iowa are the ones that will use our data the most. - Look into what the Iowa Flood Center is doing for water table obs. - Discussion of proposing 6 positions for the Iowa Weather Network. - Should also target livestock industry in the state. - [ ] remove the county colors from the map. - [ ] only show stations on the map that have longer term data? - [ ] look into the 1977 drought in Story County. - Make plots of heat index and stress changes over the years. 17 Dec 2019 :: Meeting with Dr Lamkey ---- - Weather data is important to Ag Research and we need to emphasis that. - Dr Lamkey discussed replacing Ethan with Dr Honeyman, who is on board. - There is experiment station money that needs to be spent on research. - Dr Lamkey would like to see more monetization of the IEM. - Discussion of previous inversion station efforts. - [ ] create a one pager describing the usage of IEM/ISUSM - Will provide the one pager to the Dean, who may wish to promote it. - Dr Lamkey wishes we not concentrate on implementation, but request a budget. - People think NOAA stations are enough, one pager to show otherwise. - Discussion of new Dr Taylor position and how that may work out. - Tie in Climate Smart Ag. - Loop in USCRN? - Have a grad student do research / station maintenance next summer. 15 Aug 2019 :: Vineyard Station Meeting ---- - Jim Schraeder uses my stuff a bunch already - [ ] Send some example ISU Farm cooperator agreements for their review - Kenny McCabe installed the stations and still is on campus somewhere - [ ] product they use is hours of wetness per week - [ ] add comments to excel headers for easier usage 7 Feb 2018 ---- - Hornbuckle, Flory, Ethan, Vanloocke - Add vineyard station modems to the quote - Add cooperator stations modems to the quote - place the 6 Dr Todey sensors on 3 towers to replicate current towers - Maybe our inversion station is slightly over 5K - Campbell will announce pricing soon on the SoilVue10 23 Jan 2018 ---- - Hornbuckle, Flory, Ethan, Vanloocke - Discussion on budgeting the $36K we have to spend on the sensors 1 need to replace the modems 2 need to buy some parts for Ethan 3 Inversion stations with target of 5K per station - [ ] daryl writes up the proposal and gives options needed. 20 Mar 2017 ---- - Archontoulis, Goode, Ethan, KenP(Nashua), VanLoocke, Berns, Taylor, Flory - Mark Honeyman was unable to attend - The remaining vineyard station modems are in Flory's Office - Discussion on what to do with the precipitation sensors and if we should add a second at each site. Will be adding a second at AEA Farm to test - [ ] Need to make a correction to Sutherland's precip in 2013 - [ ] convert yieldfx reports back to a basis of COOP data - Dr Taylor noted that proximity to power lines help shield the stations from lightning - [ ] engage Dr Honeyman again about automating precip report for farm sites that don't do COOP reporting 25 Aug 2016 ----- - Tim Goode, Mark Honeyman, Ethan, Archontoulis, VanLoocke, Arritt, Cochran - Lots of discussion on the 5 vineyard stations, some highlights - currently in boxes, purchased in June - would like to spread them out over the state - have leaf wetness sensors - They will likely not be under bare soil - discussion on how these stations can be used for frost protection(?), this was not fully clear to me, but it would involve some aux forecast data - the soils are high pH, but not necessily sandy - they have the same cell modems - still have to resolve where they are putting soil moisture sensors - [x] email Flory and gauge interest on this matter - discussion of placing a new sensor on a farm of interest NE of Ames - is the new station at Marcus too close to others? 29 Jun 2016 ---- - Tim Goode, Mark Honeyman, Ethan, Flory, Archontoulis, Taylor, Vanloocke. Arritt - Dr Honeyman has a draft agreement that was worked on with legal council for the sites that are on private farms - Dr Taylor expressed a need to start replacing the wind sensors soon - He has a spot in mind (Marcus, IA) for the current unused station - Discussion about how soil moisture calibration is done in Oklahoma and how we could apply it here - They take two samples down to 5ft and then do gravimetric - Dr Taylor believes that we don't need per-site logger program calibrations of the sensors, errors are within 2% - Flory is going to look into multiplexors - [ ] daryl's TODO item on replacing winter precip was discussed again - Voted to have a bare soil requirement, Dr Honeyman will email out the group - We can likely increase the reporting interval as the timing and bandwidth exists - The door was opened again for having the Hort/Vineyard stations join the network - Discussion on what the Iowa Flood Center is deploying for soil moisture stations. 9 Jun 2016 ---- - Tim Goode, Mark Honeyman, Ethan, Flory, Archontoulis, Lamkey - Discussion of the new vineyard stations and likely not to include them at this time - I raised the need for 5-15 minute data and was wondering about the cell phone bill and if it could support more frequent data - Need to look into a legal aggreement with the private farm weather stations as the lat/lons are published for them - Need to decide if bare soil would be maintained for sites for the 4 inch sensor. - Need coefficients for each site so to get accurate soil moisture data - perhaps the soil moisture data should be removed from the website until this issue is resolved... - [ ] daryl should implement a replacement of the winter time precip so to allow for more complete data - [x] change the alerting threshold for offline alerts as they are being ignored by others - Ethan is funded 100% now from Experiment Station, so can work on this - [ ] create electronic version of the checklist for stations to do - Email Tim and Ethan on issues at the same time. ================================================ FILE: docs/yieldproject.md ================================================ FACTs Project Meeting Notes ======== 26 Jun 2019 Archontoulis Angelos ---- - For next year, the PSIMs domain may expand to MN and NE - [ ] akrherz/iem#199 can we move IEMRE to the database to make it faster - [ ] would like to launch a crop dry down app by Aug 3 - Instead of providing average RH, I could just provide daily min/max - [ ] produce a map showing RH computation bias by averaging method - [ ] NASA POWER has a RH field I could potentially use - [ ] daymet allow maybe has a RH field I could use 23 May 2019 Archontoulis Isaiah ---- - [ ] CFS may have some strange issues around cold days < 40F - [x] Send Angelo new map addresses - [x] Take location labels off the map - [x] Plot Magnitude of GDD departure 2 May 2019 Archontoulis ---- - [ ] he wants CFS radiation capped at 31 MJ/d - Plan is to push weekly update at 9 AM Wednesday, so runs made on Tuesday - [ ] Deliver a WPC Map of forecasted 7 day precipitation - [x] GDD departures over the coming week - [x] next week daily max temperature and daily min temperature - [ ] create a pyIEM zoom for the corn belt, IA->IN - [ ] PSIMs yearly scenario substitions, like what is done for station data 5 Apr 2019 Archontoulis, Angelos, Isaiah, one other ---- - The regional interest this year is IA, IL and IN - [ ] investigate NASA Power and see how difference its radiation is - There are three deliverables they want - [x] Static 1980-2018 PSIMs files, like I generated last year - [x] four routinely updated PSIMs files, one for each CFS 9 mon realization - [ ] scenarios with all years substituted. This may be too much data for them, so lower priority for now. 5 Jan 2017 Dr Archontoulis ---- - Review Nov 1 to today plot, to ensure it is doing the right thing - [ ] remove the march 15 to today plot, not used - [ ] Add NASS county yield somehow to the aridity x/y plot - A new folder was created at dropbox for the uploads to go to 11 Apr 2016 Dr Archontoulis ---- - He now has 6 files processed up until 23 March, wants data till 30 Nov - March 15th start date for the various GDD, etc plots - For the future scatter, just make it a cloud with some lines in it - Would like to use dropbox for sharing files - Add a GDD column and others for the APSIM met file - FIX: the units of the soil moisture shown in the download page - Will want me to produce a differently formatted file later in July - He wants 4 days of forecast data - Output file name is Ames_YYYYmmdd.met 24 Mar 2016 Dr Archontoulis ---- - Discuss my involvement with the yield forecasting project - Dr Helmers Cobbs site will be sending hourly precip data my way - They have a good web developer, so I am just wrangling data - I need to look at a dataset called agmerra - So the complicated routine about producing a 1980-2015 weather data file that has this year's data + forecast replacing that year's period. Then the rest of the year is simply taking that old year's data. I'll automate this - Look into usage of GFS + CFS for forecast data - There are six sites in play ================================================ FILE: environment.yml ================================================ dependencies: # NB: these are requirements of code in this repo # autoplot uses - affine # pytest - beautifulsoup4 # don't download stuff - cartopy_offlinedata # command line - click # pytest - coverage # mastodon iembot page - cryptocode # ?help support - docutils # one wxc script - ephem # various places - fiona # various places - gdal # TC backend - gunicorn - libgdal-pg # ISUSM ingest - inotify # For grib to TIFF conversion - libgdal-grib # important - geopandas # Get stuff - httpx # mrms script - imageio # Unknown problem with netCDF4 not loading - libnetcdf=4.9 # soft requirement for pandas - lxml - mastodon.py - matplotlib-base # various places - metar # duh - metpy>=1.0.0 # yup - netcdf4 # Transient dep for pyIEM, which is pip installed - nh3 # yup - numpy # racoon - odfpy # Complicated dep need for iemwebfarm mod_wsgi usage - openldap # excel output - openpyxl # due to config settings - pandas>=3 # mod-wsgi - paste # PIL - pillow # for downstream installs - pip # for psql and friends - postgresql # database - psycopg # for pyiem - pyarrow # validation - pydantic # lots of places - pygrib # caching - pymemcache # various places - pyproj # various shapefile dumpers - pyshp>=2 # for testing - pytest - pytest-cov - pytest-httpx - pytest-mpl - pytest-runner - pytest-xdist - qrcode # various places - rasterio # various places - rasterstats # Can't really avoid - requests # Testing - responses # important - scipy # autoplots - seaborn # needed for downstream pip install? - setuptools-markdown # cow et al - shapely # various dumpers - simplejson - sqlalchemy # for streaming back zip files - stream-zip # scripts - tqdm # AHPS scripts - twisted # testing - werkzeug # need to use more - verde # Excel - xlsxwriter # Excel - xlwt ================================================ FILE: eslint.config.js ================================================ // Legacy CommonJS ESLint configuration for pre-commit compatibility const js = require("@eslint/js"); const globals = require("globals"); module.exports = [ // Configuration for ESLint config file itself (Node.js environment) { files: ["eslint.config.js"], languageOptions: { ecmaVersion: 2020, sourceType: "script", globals: { ...globals.node } }, rules: { "no-console": "off" } }, // Ignore other problematic files { ignores: ["htdocs/vtec/assets/*.js", "htdocs/lsr/static.js"] }, js.configs.recommended, // Configuration for traditional script files (.js) { files: ["**/*.js"], ignores: ["src/iemjs/**/*.js"], languageOptions: { ecmaVersion: 2020, sourceType: "script", globals: { ...globals.browser, // Prohibited globals (set to false to trigger errors) "$": false, "jQuery": false, // Allowed globals for traditional scripts "ol": "readonly", "Ext": "readonly", "iemdata": "readonly", "moment": "readonly", "flowplayer": "readonly", "bootstrap": "readonly" } }, rules: { // jQuery prohibition rules "no-restricted-globals": [ "warn", { "name": "$", "message": "jQuery should not be used. Use vanilla JavaScript instead." }, { "name": "jQuery", "message": "jQuery should not be used. Use vanilla JavaScript instead." } ], // Modernization hints "no-restricted-syntax": [ "warn", { "selector": "CallExpression[callee.property.name='substr']", "message": "substr() is deprecated. Use substring() or slice() instead." }, { "selector": "CallExpression > Identifier[name='undefined']:last-child", "message": "Avoid explicitly passing 'undefined' as the last argument. Omit the argument instead - it defaults to undefined." }, // Encourage optional chaining over && in conditionals { "selector": "IfStatement[test.type='LogicalExpression'][test.operator='&&'][test.right.type='MemberExpression']", "message": "Use optional chaining (?.) instead of && for null checks before property access." }, { "selector": "IfStatement[test.type='LogicalExpression'][test.operator='&&'][test.right.type='CallExpression'][test.right.callee.type='MemberExpression']", "message": "Use optional chaining (?.) instead of && for null checks before method calls." }, { "selector": "ConditionalExpression[test.type='LogicalExpression'][test.operator='&&'][test.right.type='MemberExpression']", "message": "Use optional chaining (?.) instead of && in conditional expressions for property access." }, { "selector": "ConditionalExpression[test.type='LogicalExpression'][test.operator='&&'][test.right.type='CallExpression'][test.right.callee.type='MemberExpression']", "message": "Use optional chaining (?.) instead of && in conditional expressions for method calls." }, { "selector": "IfStatement[consequent.type='BlockStatement'][consequent.body.length=1][consequent.body.0.type='ReturnStatement'][consequent.body.0.argument.type='Literal'][consequent.body.0.argument.value=true][alternate.type='BlockStatement'][alternate.body.length=1][alternate.body.0.type='ReturnStatement'][alternate.body.0.argument.type='Literal'][alternate.body.0.argument.value=false]", "message": "Found complex boolean return - return the boolean expression directly instead of if/else with true/false." }, { "selector": "IfStatement[consequent.type='ReturnStatement'][consequent.argument.type='Literal'][consequent.argument.value=true][alternate.type='ReturnStatement'][alternate.argument.type='Literal'][alternate.argument.value=false]", "message": "Found complex boolean return - return the boolean expression directly instead of if/else with true/false." }, { "selector": "TemplateLiteral[expressions.length=0]", "message": "Template Literal Found - use single quotes instead of template literals when no interpolation is needed." } ], // Code quality "curly": ["error", "all"], "eqeqeq": "error", "no-console": "warn", "no-debugger": "error", "one-var": ["error", "never"], // Require one variable declaration per line "init-declarations": ["error", "always"], // Require variables to be initialized when declared "object-shorthand": "warn", // Use shorthand property syntax for object literals (e.g., {foo} instead of {foo: foo}) // Variable shadowing detection "no-shadow": ["error", { "builtinGlobals": false, "hoist": "functions", "allow": ["err", "error", "resolve", "reject", "cb", "callback", "done"] }], // Duplicate assignment detection "no-self-assign": "error", "no-sequences": "error", "no-unreachable": "error", // Block-scoped declarations "no-inner-declarations": ["error", "both"], // Function or var declarations in nested blocks is not preferred // Additional code quality rules to catch common issues "no-implicit-coercion": "warn", "no-return-assign": "error", "array-callback-return": "error", "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }], "prefer-arrow-callback": "warn", // Consider using arrow functions for callbacks "prefer-template": "warn", // Template Literal Found - use template literals instead of string concatenation "prefer-const": "warn", // Use const declarations for variables that are never reassigned "default-case": "warn", // No default cases in switch statements "complexity": ["warn", { "max": 15 }], // Function with cyclomatic complexity higher than threshold "no-unused-vars": ["warn", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false, "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrors": "all" }], // Found unused objects // Avoid usage of `this` in JavaScript code (IEM rule) "no-invalid-this": "error", "consistent-this": ["error", "self"], "class-methods-use-this": "warn", // Warn when class methods don't use `this` and could be static // Disable some rules that might be too strict for legacy code "no-redeclare": "off" } }, // Configuration for ES modules (.module.js and IEM utilities) { files: ["**/*.module.js", "src/iemjs/**/*.js"], languageOptions: { ecmaVersion: 2022, sourceType: "module", globals: { ...globals.browser } }, rules: { // Base ES5+ compliance rules "no-var": "error", // jQuery prohibition rules "no-restricted-globals": [ "error", { "name": "$", "message": "jQuery should not be used. Use vanilla JavaScript instead." }, { "name": "jQuery", "message": "jQuery should not be used. Use vanilla JavaScript instead." } ], // Modern JavaScript preferences (more strict for modules) "prefer-arrow-callback": "error", "prefer-template": "error", "prefer-const": "error", // Use const declarations for variables that are never reassigned "object-shorthand": "error", "no-return-await": "error", // Prevent unnecessary return await (performance issue) // Deprecated method warnings for modules too "no-restricted-syntax": [ "warn", { "selector": "CallExpression[callee.property.name='substr']", "message": "substr() is deprecated. Use substring() or slice() instead." }, { "selector": "ArrowFunctionExpression > AssignmentExpression", "message": "Avoid assignment operations in arrow function implicit returns. Use block statements with curly braces for side effects." }, { "selector": "CallExpression > Identifier[name='undefined']:last-child", "message": "Avoid explicitly passing 'undefined' as the last argument. Omit the argument instead - it defaults to undefined." }, { "selector": "IfStatement[test.type='LogicalExpression'][test.operator='&&'][test.left.type='Identifier'][test.right.type='MemberExpression']", "message": "Use optional chaining (?.) instead of && for null checks before property access." }, { "selector": "IfStatement[test.type='LogicalExpression'][test.operator='&&'][test.left.type='Identifier'][test.right.type='CallExpression'][test.right.callee.type='MemberExpression']", "message": "Use optional chaining (?.) instead of && for null checks before method calls." }, { "selector": "IfStatement[consequent.type='BlockStatement'][consequent.body.length=1][consequent.body.0.type='ReturnStatement'][consequent.body.0.argument.type='Literal'][consequent.body.0.argument.value=true][alternate.type='BlockStatement'][alternate.body.length=1][alternate.body.0.type='ReturnStatement'][alternate.body.0.argument.type='Literal'][alternate.body.0.argument.value=false]", "message": "Found complex boolean return - return the boolean expression directly instead of if/else with true/false." }, { "selector": "IfStatement[consequent.type='ReturnStatement'][consequent.argument.type='Literal'][consequent.argument.value=true][alternate.type='ReturnStatement'][alternate.argument.type='Literal'][alternate.argument.value=false]", "message": "Found complex boolean return - return the boolean expression directly instead of if/else with true/false." }, { "selector": "TemplateLiteral[expressions.length=0]", "message": "Template Literal Found - use single quotes instead of template literals when no interpolation is needed." } ], // Code quality "curly": ["error", "all"], "eqeqeq": "error", "no-console": "warn", "no-debugger": "error", "one-var": ["error", "never"], // Require one variable declaration per line "init-declarations": ["error", "always"], // Require variables to be initialized when declared // Variable shadowing detection "no-shadow": ["error", { "builtinGlobals": false, "hoist": "functions", "allow": ["err", "error", "resolve", "reject", "cb", "callback", "done"] }], // Duplicate assignment detection "no-self-assign": "error", "no-sequences": "error", "no-unreachable": "error", // Block-scoped declarations "no-inner-declarations": ["error", "both"], // Function or var declarations in nested blocks is not preferred // Additional code quality rules (stricter for modules) "no-implicit-coercion": "error", "no-return-assign": "error", "array-callback-return": "error", "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }], "require-await": "error", // Async functions must contain await expressions "default-case": "error", // No default cases in switch statements "complexity": ["error", { "max": 15 }], // Function with cyclomatic complexity higher than threshold "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false, "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrors": "all" }], // Found unused objects // Avoid usage of `this` in JavaScript code (IEM rule) "no-invalid-this": "error", "consistent-this": ["error", "self"], // Disable some rules that might be too strict "no-redeclare": "off" } }, // Configuration for test files - allow console usage for test output { files: ["**/tests/**/*.js", "**/*.test.js", "**/*.spec.js"], languageOptions: { globals: { ...globals.node // Add Node.js globals like process, Buffer, etc. } }, rules: { "no-console": "off", // Console output is essential for test feedback "require-await": "off", // Test runners often have async functions with await in loops "complexity": ["error", { "max": 30 }], // Function with cyclomatic complexity higher than threshold } } ]; ================================================ FILE: htdocs/.well-known/ai-plugin.json ================================================ { "schema_version": "v1", "name_for_human": "IEM Plugin", "name_for_model": "iem", "description_for_human": "Plugin for working with IEM data.", "description_for_model": "Plugin for working with IEM data.", "auth": { "type": "none" }, "api": { "type": "openapi", "url": "https://mesonet.agron.iastate.edu/api/1/openapi.json", "is_user_authenticated": false }, "logo_url": "https://mesonet.agron.iastate.edu/images/logo_small.png", "contact_email": "akrherz@iastate.edu", "legal_info_url": "https://mesonet.agron.iastate.edu/disclaimer.php" } ================================================ FILE: htdocs/.well-known/traffic-advice ================================================ [ {"user_agent": "prefetch-proxy", "disallow": true} ] ================================================ FILE: htdocs/ASOS/current.phtml ================================================ title = "{$network} Current Conditions"; $t->refresh = 1200; $t->iemselect2 = TRUE; $t->current_network = "ASOS"; $nt = new NetworkTable($network, FALSE, TRUE); $cities = $nt->table; $mesosite = iemdb('mesosite'); $stname = iem_pg_prepare($mesosite, "SELECT tzname from networks where id = $1"); $rs = pg_execute($mesosite, $stname, array($network)); if (pg_num_rows($rs) < 1) { $tzname = "America/Chicago"; } else { $row = pg_fetch_assoc($rs, 0); $tzname = $row["tzname"]; } $vals = array( "tmpf" => "Air Temperature [°F]", "dwpf" => "Dew Point Temp [°F]", "sknt" => "Wind Speed [knots]", "drct" => "Wind Direction [deg]", "alti" => "Altimeter [mb]", "peak" => "Today's Wind Gust [knots]", "peak_ts" => "Time of Peak Gust", "relh" => "Relative Humidity", "feel" => "Feels Like [°F]", "vsby" => "Visibility [miles]", "ts" => "Observation Time", "phour" => "Last Hour Rainfall [inch]", "min_tmpf" => "Today's Low Temperature", "name" => "Station Name", "max_tmpf" => "Today's High Temperature", "id" => "Station Identifier", "skyl1" => "Cloud Level 1", "skyl2" => "Cloud Level 2", "skyl3" => "Cloud Level 3", "skyl4" => "Cloud Level 4", "pday" => "Today Rainfall [inch]", ); if (!array_key_exists($sortcol, $vals)) { $sortcol = "name"; } $arr = array( "network" => $network, ); $jobj = iemws_json("currents.json", $arr); /* Final data array */ $mydata = array(); foreach ($jobj["data"] as $bogus => $iemob) { $key = $iemob["station"]; $mydata[$key] = $iemob; $mydata[$key]["ts"] = new DateTime($iemob["local_valid"]); $mydata[$key]["sped"] = $mydata[$key]["sknt"] * 1.15078; if ($mydata[$key]["max_gust"] > $mydata[$key]["max_sknt"]) { $mydata[$key]["peak"] = $mydata[$key]["max_gust"]; if (! is_null($mydata[$key]["local_max_gust_ts"])) { $mydata[$key]["peak_ts"] = new DateTime($mydata[$key]["local_max_gust_ts"]); } } else { $mydata[$key]["peak"] = $mydata[$key]["max_sknt"]; if (! is_null($mydata[$key]["local_max_sknt_ts"])) { $mydata[$key]["peak_ts"] = new DateTime($mydata[$key]["local_max_sknt_ts"]); } } } // End of while if ($format == 'csv') { $csv = "station,valid_gmt,tmpf,max_tmpf,min_tmpf,dwpf,sknt,drct,relh,vsby,phour_in,pday_in,metar\n"; foreach ($mydata as $key => $data) { $dt = new DateTime($data['utc_valid']); $csv .= sprintf( "%s,%s,%.0f,%.0f,%.0f,%.0f,%.0f,%.0f,%.1f,%s,%.2f,%.2f,%s\n", $key, $dt->format("Y-m-d H:i"), $data['tmpf'], $data["max_tmpf"], $data["min_tmpf"], $data['dwpf'], $data['sknt'], $data['drct'], $data['relh'], $data['vsby'], $data['phour'], $data['pday'], $data['raw'] ); } header("Content-type: text/plain"); echo $csv; die(); } $nselect = selectNetworkType("ASOS", $network); $ar = array( "no" => "No", "yes" => "Yes" ); $mselect = make_select("metar", $metar, $ar); $ar = array( "asc" => "Ascending", "desc" => "Descending" ); $sselect = make_select("sorder", $sorder, $ar); $ar = array( "html" => "Web Page", "csv" => "Comma Delimited", ); $fselect = make_select("format", $format, $ar); $uri = "current.phtml?sorder=$sorder&metar=$metar&network=$network&sortcol="; $year = date("Y"); $month = date("m"); $day = date("d"); $tkeys = array_keys($cities); $station = $tkeys[0]; $table = <<
Select Network: {$nselect} Include METARS:
{$mselect} Sort Order: {$sselect} Format: {$fselect}

Times shown are for timezone: {$tzname}. The local day summary is based on that timezone.
Table sorted by: ({$vals[$sortcol]})     Click on a column to sort it. Click on site ID for more information. You can download data from this network here and you can view daily summaries on this network.
EOM; $finalA = aSortBySecondIndex($mydata, $sortcol, $sorder); $now = time(); $i = 0; $old = ""; $domain = array_keys($nt->table); $online = array(); foreach ($finalA as $key => $parts) { // Keep track of online stations $online[] = $key; $i++; $row = ""; $tdiff = $now - strtotime($parts["utc_valid"]); $url = sprintf("/sites/site.php?station=%s&network=%s", $key, $network); $row .= sprintf("", $url, $key); $row .= ""; $row .= ""; $old .= $row; continue; } else if ($tdiff > 7200) { $fmt = "h:i A"; $row .= 'bgcolor="orange"'; } else if ($tdiff > 3600) { $fmt = "h:i A"; $row .= 'bgcolor="green"'; } else { $fmt = "h:i A"; } $phour = ($parts["phour"] != 0.0001) ? $parts["phour"] : 'T'; $pday = ($parts["pday"] != 0.0001) ? $parts["pday"] : 'T'; $ptmpf = ($parts["tmpf"] !== null) ? myround($parts["tmpf"], 0) : 'M'; $pdwpf = ($parts["dwpf"] !== null) ? myround($parts["dwpf"], 0) : 'M'; $pfeel = ($ptmpf === "M" || $pdwpf === "M" || $parts["feel"] === null) ? "M" : myround($parts["feel"], 0); $prelh = ($ptmpf === "M" || $pdwpf === "M" || $parts["relh"] === null) ? "M" : myround($parts["relh"], 0); $wxc = is_null($parts["wxcodes"]) ? "" : str_replace("{", " ", $parts["wxcodes"]); $row .= ">" . $parts["ts"]->format($fmt) . "" . "" . ""; $row .= "\n"; if ($metar == "yes") { $row .= " " . $parts["raw"] . " \n"; } $table .= $row; } $offline = array_diff($domain, $online); $offline_entries = ""; // loop over offline entries foreach ($offline as $sid) { $sname = $nt->table[$sid]["name"]; $url = sprintf("/sites/site.php?station=%s&network=%s", $sid, $network); $offline_entries .= sprintf( "" . "" . "\n", $sid, $url, $sid, $sname ); } $table .= $old; $table .= $offline_entries; $table .= <<
ADD: ID Station Ob Time Present Wx Temps °F   Wind [knots] Precip Clouds
Air Hi Lo Dewp Feels RH % Alti Vsby Speed Drct Gust @ Time 1 Hour Today Level 1 Level 2 Level 3 Level 4
%s" . $cities[$key]["name"] . " 10000) { $fmt = "d M h:i P"; $row .= "bgcolor=\"red\">" . $parts["ts"]->format($fmt) . "Site Offline
{$wxc}" . $ptmpf . "" . myround($parts["max_tmpf"], 0) . "" . myround($parts["min_tmpf"], 0) . " " . $pdwpf . " " . $pfeel . " " . $prelh . " " . $parts["alti"] . " " . $parts["vsby"] . " " . myround($parts["sknt"], 0); if (floatval($parts["gust"] > 0)) { $row .= "G" . myround($parts["gust"], 0); } $row .= "" . drct2txt($parts["drct"]) . ""; if (isset($parts["peak_ts"])) { $row .= sprintf( "%s @ %s", myround($parts["peak"],0), $parts["peak_ts"]->format("h:i A"), ); } $text_pday = $parts['pday'] == -99 ? 'M' : $parts['pday']; $row .= "{$phour} {$pday} " . $parts["skyc1"] . " " . $parts["skyl1"] . " " . $parts["skyc2"] . " " . $parts["skyl2"] . " " . $parts["skyc3"] . " " . $parts["skyl3"] . " " . $parts["skyc4"] . " " . $parts["skyl4"] . "
%s%sSite Offline
EOM; $t->content = $table; $t->render('sortables.phtml'); ================================================ FILE: htdocs/ASOS/index.phtml ================================================ title = "ASOS/AWOS Network"; $t->content = <<< EOM

ASOS / AWOS Network

The Automated Surface Observing System (ASOS) is considered to be the flagship automated observing network. Located at airports, the ASOS stations provide essential observations for the National Weather Service (NWS), the Federal Aviation Administration (FAA), and the Department of Defense (DOD). The primary function of the ASOS stations are to take minute-by-minute observations and generate basic weather reports.[1]

Please see this Important News Item regarding wagering on ASOS temperatures.

Current ASOS Data

1 Minute Archive

The National Center for Environmental Information (NCEI) provides an archive of one minute interval observations from many US ASOS sites back to the year 2000. This archive is available here, but in a difficult to use format. The IEM makes a best-guess effort at processing the mostly undocumented data format and updates the archive daily. This dataset is not realtime and is delayed by 18–36 hours or more due to NCEI availability.

Data is available for some sites back to 2000.

Quality Control

Historical Data

Comparisons

References

[1] Adapted from the ASOS User's Guide

EOM; $t->render('single.phtml'); ================================================ FILE: htdocs/ASOS/precipnote.phtml ================================================ title = "Note about ASOS Precipitation Data"; $t->content = <<Notes about ASOS Precipitation Reports

The Automated Surface Observation System ASOS is the primary automated weather observation system in the country. The network is maintained by the Federal Aviation Administration and National Weather Service. The IEM processes, archives, and makes available data from this network. The purpose of this page is to document some interesting aspects of the precipitation data from this network.

Please feel free to report any doubts or concerns you have with this documentation. This information has been pieced together based on conversations with the NWS and reviewing the ASOS Users Manual.

Regarding Time Zones...

Depending on the report type, observations from this network are reported in either local standard time or UTC (sometimes called GMT) time. The internal clocks on the ASOS sites are not updated for daylight time. This is an important consideration when using its local daily precipitation report found in the Daily Summary Message (DSM). This 24 hour period is between local midnight in standard time. So when the local calendar is in daylight time, the period will represent the time between 1 AM and 1 AM. This 24 hour is not exactly as stated and leads to the next section.

Regarding Exact Timing...

The ASOS reports time in hours and minutes without seconds. This creates some ambiguity when attempting to ascertain exactly when a reported precipitation rate occurred. When the time hits 00 seconds, the ASOS starts processing its memory of recently saved data. This processing ends promptly at 23 seconds after and various displays are updated, products disseminated, and one minute interval data archived.

The one minute precipitation data is thus some nearly 60 second period between when the previous minute's processing got to totaling the precipitation data to this minute's processing. So the one minute total is strictly not from a period between 00 seconds of the previous minute and 00 seconds of the current period. Instead this is approximately the 60 second period between 24 seconds after the previous minute to 23 seconds of the current minute. For example:

For one minute data, the 12:45 UTC observation approximately represents a period between 12:44:24 and 12:45:23 UTC.

For the hourly METAR precipitation, the 12:54 UTC observation represents a period between 11:54:24 and 12:54:23 UTC.

For the daily summary message, the observation represents a period between 11:59:24 PM LST of the previous day to 11:59:23 PM LST of the current day.

For the hourly data in the daily summary message, the observation is a period from (HH-1):59:24 to HH:59:23 LST.

Heavy Precipitation Rates...

The ASOS uses a tipping bucket method to measure precipitation. This means that a single tip of the bucket records 0.01 inch of precipitation with the data logger. Under intense rainfall rates, various physical things happen causing the tipping mechanism to not be able to keep up with the flowing water rate. The ASOS Users Manual notes in section 3.4.2 that a correction is applied to the measured accumulation on a minute by minute basis. The equation shown in the manual is as follows:

C = A(1 + 0.60A)

where C is the reported accumulation and A is the measured accumulation from the tipping bucket.

This means that when the ASOS reports a one minute accumulation of 0.25 inches, the actual measured value was around 0.22 inches. Please note that it is not our intention to claim this correction is wrong, but just to document that it is there. Extremely intense rainfall rates such as these are rare, so the typical adjustment is practically zero.

In Summary

The timing and precipitation totals are not exact, but they are conservative over time when summed. Please let us know of any corrections or clarifications you would like to see made. EOM; $t->render('single.phtml'); ================================================ FILE: htdocs/ASOS/recent.css ================================================ /* Ensure the region scrolls nicely if tall */ .table-responsive { max-height: 70vh; overflow: auto; } /* Sticky header hint (Bootstrap adds .sticky-top logic) */ thead.sticky-top th { background: #f8f9fa; } #recent-status { font-size: 0.875rem; } @media (max-width: 575.98px) { #recent-heading { font-size: 1.05rem; } #recent-desc { font-size: 0.9rem; } } ================================================ FILE: htdocs/ASOS/recent.module.js ================================================ let report = 'snowdepth'; const getAllowedReports = (select) => new Set([...select.options].map((opt) => opt.value)); const buildCell = (tagName, text) => { const cell = document.createElement(tagName); cell.textContent = text; return cell; }; /** * Fetch recent interesting METAR reports and populate the table. * Exposed for potential reuse (e.g. manual refresh button later). */ const buildRow = (feat) => { const row = document.createElement('tr'); row.appendChild(buildCell('td', feat.properties.station)); row.appendChild(buildCell('td', feat.properties.network)); row.appendChild(buildCell('td', feat.properties.valid)); row.appendChild(buildCell('td', `${feat.properties.value ?? ''}`)); row.appendChild(buildCell('td', feat.properties.metar)); return row; }; const buildMessageRow = (message) => { const row = document.createElement('tr'); const cell = buildCell('th', message); cell.colSpan = 5; row.appendChild(cell); return row; }; const updateStatus = (live, message) => { if (live) {live.textContent = message;} }; export const fetchData = async () => { const tableBody = document.querySelector('#datatable tbody'); if (!tableBody) {return;} const live = document.getElementById('recent-status'); tableBody.replaceChildren(buildMessageRow('Querying server, one moment')); updateStatus(live, 'Loading recent METAR reports…'); try { const resp = await fetch(`/geojson/recent_metar.py?q=${encodeURIComponent(report)}`); if (!resp.ok) {throw new Error(`${resp.status} ${resp.statusText}`);} const j = await resp.json(); tableBody.replaceChildren(); j.features.forEach((feat) => tableBody.appendChild(buildRow(feat))); if (j.features.length === 0) { tableBody.replaceChildren(buildMessageRow('No results were found, sorry!')); updateStatus(live, 'No recent METAR reports found for selected type.'); return; } updateStatus(live, `${j.features.length} recent METAR report${j.features.length === 1 ? '' : 's'} loaded.`); } catch (error) { tableBody.replaceChildren(buildMessageRow(`Error loading data ${error.message}`)); updateStatus(live, `Error loading data: ${error.message}`); } }; /** Get current report type. */ export const getReport = () => report; /** * Set current report type, update the URL hash, refresh data. * @param {string} value */ export const setReport = (value) => { report = value; // Update ?report= parameter without reloading page. const params = new URLSearchParams(window.location.search); params.set('report', value); const newUrl = `${window.location.pathname}?${params.toString()}`; window.history.replaceState({}, '', newUrl); fetchData(); }; /** Initialize report selection from current URL hash. */ const initReportFromURL = () => { const select = document.getElementById('report'); if (!(select instanceof HTMLSelectElement)) { return; } const allowedReports = getAllowedReports(select); const params = new URLSearchParams(window.location.search); const fromParam = params.get('report'); let candidate = fromParam; // Hash shim: allow legacy #value and migrate to ?report=value if (!candidate && window.location.hash.length > 1) { candidate = window.location.hash.substring(1); // Migrate hash -> query param (preserve other params if they appear later) const migrateParams = new URLSearchParams(window.location.search); migrateParams.set('report', candidate); const newUrl = `${window.location.pathname}?${migrateParams.toString()}`; window.history.replaceState({}, '', newUrl); } if (candidate?.length && allowedReports.has(candidate)) { report = candidate; select.value = report; } }; /** Wire up UI events and perform initial data load. */ export const init = () => { const select = document.getElementById('report'); if (select instanceof HTMLSelectElement) { select.addEventListener('change', (e) => { const target = e.currentTarget; if (target instanceof HTMLSelectElement) {setReport(target.value);} }); } initReportFromURL(); fetchData(); }; // Auto initialize when DOM is ready. document.addEventListener('DOMContentLoaded', init); ================================================ FILE: htdocs/ASOS/recent.phtml ================================================ title = "Recent Interesting METAR Reports"; $t->content = <<

Recent Interesting METAR Reports

The IEM processes and archives a feed of METAR formatted weather observations. These observations sometimes contain interesting things reported. This page lists such recent occurrences of a given phenomena for roughly the past 2–3 days.

Table of recent interesting METAR reports filtered by selected report type
ID Network UTC Valid Value METAR
EOM; $t->headextra = << EOM; $t->jsextra = << EOM; $t->render('single.phtml'); ================================================ FILE: htdocs/ASOS/reports/mon_prec.css ================================================ /* Bootstrap 5 enhancements for Monthly Precipitation Report */ /* Tabulator container styling */ #tabulator-container { margin-top: 1rem; } /* Table controls styling */ #table-controls { margin-bottom: 1rem; padding: 1rem; background-color: var(--bs-light); border-radius: 0.375rem; border: 1px solid var(--bs-border-color); } #table-controls .btn-group { margin-right: 0.5rem; margin-bottom: 0.5rem; } #table-controls .btn { margin-right: 0.25rem; } /* Enhance the "Interactive Grid" button */ #create-grid { background: linear-gradient(135deg, var(--bs-success), var(--bs-primary)); border: none; color: white; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.3s ease; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } #create-grid:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); } #create-grid:active { transform: translateY(0); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } /* Original table wrapper */ #original-table { margin-top: 1rem; } /* Tabulator table customizations */ .tabulator { border: 1px solid var(--bs-border-color); border-radius: 0.375rem; overflow: hidden; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } .tabulator .tabulator-header { background: var(--bs-primary); color: white; border-bottom: 2px solid var(--bs-primary); } .tabulator .tabulator-header .tabulator-col { border-right: 1px solid rgba(255, 255, 255, 0.2); background: var(--bs-primary); } .tabulator .tabulator-header .tabulator-col-title { font-weight: 600; color: white !important; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } .tabulator .tabulator-header .tabulator-col-sorter { color: white !important; } .tabulator .tabulator-header .tabulator-header-filter input { background-color: rgba(255, 255, 255, 0.9); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 0.25rem; color: var(--bs-dark); font-size: 0.875rem; } .tabulator .tabulator-header .tabulator-header-filter input:focus { background-color: white; border-color: var(--bs-primary); box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25); } /* Row styling */ .tabulator .tabulator-row:nth-child(even) { background-color: var(--bs-light); } .tabulator .tabulator-row:hover { background-color: rgba(var(--bs-primary-rgb), 0.1); } .tabulator .tabulator-row .tabulator-cell { border-right: 1px solid var(--bs-border-color); padding: 0.5rem 0.75rem; font-family: 'Courier New', monospace; text-align: center; } .tabulator .tabulator-row .tabulator-cell:first-child, .tabulator .tabulator-row .tabulator-cell:nth-child(2) { font-family: inherit; text-align: left; } /* Highlight special columns */ .tabulator .tabulator-row .tabulator-cell.highlight-summer { background-color: rgba(var(--bs-warning-rgb), 0.1); font-weight: 600; border-left: 3px solid var(--bs-warning); } .tabulator .tabulator-row .tabulator-cell.highlight-annual { background-color: rgba(var(--bs-success-rgb), 0.1); font-weight: 600; border-left: 3px solid var(--bs-success); } /* Link styling in table */ .tabulator .tabulator-row .tabulator-cell a { color: var(--bs-primary); text-decoration: none; font-weight: 500; } .tabulator .tabulator-row .tabulator-cell a:hover { color: var(--bs-primary); text-decoration: underline; } /* Pagination styling */ .tabulator .tabulator-footer { background-color: var(--bs-light); border-top: 1px solid var(--bs-border-color); } .tabulator .tabulator-footer .tabulator-page { background-color: white; border: 1px solid var(--bs-border-color); color: var(--bs-primary); border-radius: 0.25rem; margin: 0 0.125rem; } .tabulator .tabulator-footer .tabulator-page:hover { background-color: var(--bs-primary); color: white; } .tabulator .tabulator-footer .tabulator-page.active { background-color: var(--bs-primary); color: white; border-color: var(--bs-primary); } /* Form styling improvements */ form[name="change"] { background: linear-gradient(135deg, rgba(var(--bs-primary-rgb), 0.05), rgba(var(--bs-info-rgb), 0.05)); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 1.5rem; border: 1px solid var(--bs-border-color); } form[name="change"] p { margin-bottom: 1rem; } form[name="change"] select { margin-right: 1rem; margin-bottom: 0.5rem; } form[name="change"] input[type="submit"] { background: var(--bs-primary); color: white; border: none; padding: 0.5rem 1rem; border-radius: 0.375rem; font-weight: 500; transition: all 0.2s ease; } form[name="change"] input[type="submit"]:hover { background: var(--bs-primary); transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } /* Responsive adjustments */ @media (max-width: 768px) { #table-controls { padding: 0.75rem; } #table-controls .btn-group { display: flex; flex-direction: column; width: 100%; margin-bottom: 0.5rem; } #table-controls .btn { margin-right: 0; margin-bottom: 0.25rem; } .tabulator .tabulator-header .tabulator-col { min-width: 60px; } form[name="change"] { padding: 1rem; } form[name="change"] select { width: 100%; margin-right: 0; margin-bottom: 0.75rem; } form[name="change"] input[type="submit"] { width: 100%; } } /* Breadcrumb enhancement */ .breadcrumb { background: linear-gradient(135deg, var(--bs-light), rgba(var(--bs-primary-rgb), 0.05)); border-radius: 0.375rem; padding: 0.75rem 1rem; margin-bottom: 1.5rem; border: 1px solid var(--bs-border-color); } /* Missing data styling */ .tabulator .tabulator-row .tabulator-cell:has-text("M") { color: var(--bs-secondary); font-style: italic; } /* Loading spinner */ .tabulator-loader { border: 3px solid var(--bs-light); border-top: 3px solid var(--bs-primary); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 2rem auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Page header styling */ h1, h2, h3 { color: var(--bs-primary); font-weight: 600; } /* Info text styling */ p { line-height: 1.6; color: var(--bs-body-color); } /* Button group improvements */ .btn-group .btn { border-radius: 0.375rem; margin-right: 0.25rem; font-weight: 500; } .btn-group .btn:last-child { margin-right: 0; } /* Sticky header enhancement */ .sticky { position: sticky; top: 0; z-index: 10; background: var(--bs-primary); color: white; } /* Table wrapper for original table */ #original-table { overflow-x: auto; border-radius: 0.375rem; border: 1px solid var(--bs-border-color); } #original-table table { margin-bottom: 0; } #original-table th { background: var(--bs-primary); color: white; padding: 0.75rem 0.5rem; text-align: center; font-weight: 600; border-color: rgba(255, 255, 255, 0.2); } #original-table td { padding: 0.5rem; text-align: center; font-family: 'Courier New', monospace; border-color: var(--bs-border-color); } #original-table td:first-child, #original-table td:nth-child(2) { font-family: inherit; text-align: left; } ================================================ FILE: htdocs/ASOS/reports/mon_prec.module.js ================================================ // Tabulator-based Monthly Precipitation Report // Replaces ExtJS TableGrid implementation with modern Tabulator import {TabulatorFull as Tabulator} from 'https://unpkg.com/tabulator-tables@6.3.1/dist/js/tabulator_esm.min.mjs'; // Application state let precipTable = null; let originalData = []; let statusEl = null; // Common Tabulator configuration const commonConfig = { layout: "fitDataStretch", pagination: "local", paginationSize: 25, paginationSizeSelector: [10, 25, 50, 100, true], movableColumns: true, resizableColumns: true, sortMode: "local", filterMode: "local", responsiveLayout: false, // Use horizontal scroll instead of hiding columns tooltips: true, clipboard: true, clipboardCopyHeader: true, height: "70vh", placeholder: "No precipitation data available", initialSort: [ {column: "name", dir: "asc"} ] }; // Format precipitation values (handle missing data) function formatPrecipitation(cell) { const value = cell.getValue(); if (value === null || value === undefined || value === "M") {return "M";} if (typeof value === 'string' && value.trim() === '') {return "M";} const numValue = parseFloat(value); if (isNaN(numValue)) {return "M";} return numValue.toFixed(2); } // Column definitions for monthly precipitation data function getPrecipitationColumns() { return [ { title: "Station ID", field: "id", frozen: true, width: 100, sorter: "string", formatter: "link", formatterParams: { urlPrefix: '/sites/site.php?station=', urlSuffix: () => `&network=${getNetworkFromURL()}`, target: "_blank" }, headerFilter: "input", headerFilterPlaceholder: "Filter ID..." }, { title: "Station Name", field: "name", frozen: true, minWidth: 200, sorter: "string", headerFilter: "input", headerFilterPlaceholder: "Filter name..." }, {title: "Jan", field: "jan", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "January precipitation (inches)"}, {title: "Feb", field: "feb", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "February precipitation (inches)"}, {title: "Mar", field: "mar", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "March precipitation (inches)"}, {title: "Apr", field: "apr", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "April precipitation (inches)"}, {title: "May", field: "may", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "May precipitation (inches)"}, {title: "Jun", field: "jun", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "June precipitation (inches)"}, {title: "Jul", field: "jul", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "July precipitation (inches)"}, {title: "Aug", field: "aug", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "August precipitation (inches)"}, {title: "Sep", field: "sep", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "September precipitation (inches)"}, {title: "Oct", field: "oct", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "October precipitation (inches)"}, {title: "Nov", field: "nov", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "November precipitation (inches)"}, {title: "Dec", field: "dec", width: 80, sorter: "number", formatter: formatPrecipitation, headerTooltip: "December precipitation (inches)"}, { title: "MJJA", field: "mjja", width: 90, sorter: "number", formatter: formatPrecipitation, headerTooltip: "May-June-July-August total (growing season)", cssClass: "highlight-summer" }, { title: "Annual", field: "annual", width: 90, sorter: "number", formatter: formatPrecipitation, headerTooltip: "Annual total precipitation", cssClass: "highlight-annual" } ]; } // Get network parameter from URL function getNetworkFromURL() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('network') || 'IA_ASOS'; } // Load and process precipitation data from the existing table function loadTableData() { const tableElement = document.querySelector('#original-table'); if (!tableElement) { return []; } const rows = tableElement.querySelectorAll('tbody tr'); const data = []; rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length >= 16) { // Extract station ID from the link const linkElement = cells[0].querySelector('a'); const stationId = linkElement ? linkElement.textContent.trim() : cells[0].textContent.trim(); data.push({ id: stationId, name: cells[1].textContent.trim(), jan: cells[2].textContent.trim(), feb: cells[3].textContent.trim(), mar: cells[4].textContent.trim(), apr: cells[5].textContent.trim(), may: cells[6].textContent.trim(), jun: cells[7].textContent.trim(), jul: cells[8].textContent.trim(), aug: cells[9].textContent.trim(), sep: cells[10].textContent.trim(), oct: cells[11].textContent.trim(), nov: cells[12].textContent.trim(), dec: cells[13].textContent.trim(), mjja: cells[14].textContent.trim(), annual: cells[15].textContent.trim() }); } }); return data; } // Initialize the Tabulator table function initializeTable() { // Load data from existing table originalData = loadTableData(); if (originalData.length === 0) { return; } // Create Tabulator table precipTable = new Tabulator("#precipitation-tabulator-table", { ...commonConfig, columns: getPrecipitationColumns(), data: originalData }); // Show the new table and controls const container = document.getElementById('tabulator-container'); container.classList.remove('d-none'); const controls = document.getElementById('table-controls'); controls.classList.remove('d-none'); controls.removeAttribute('aria-hidden'); if (statusEl) {statusEl.textContent = 'Interactive table loaded. Use header inputs to filter and column headers to sort.';} // Hide the original table and button const original = document.getElementById('original-table'); original.style.display = 'none'; const btn = document.getElementById('create-grid'); btn.style.display = 'none'; btn.setAttribute('aria-expanded', 'true'); } // Setup table control event handlers function setupControls() { // Download CSV button document.getElementById('download-csv').addEventListener('click', () => { if (precipTable) { const year = new URLSearchParams(window.location.search).get('year') || new Date().getFullYear(); const network = getNetworkFromURL(); precipTable.download("csv", `${network}_${year}_precipitation.csv`); } }); // Download JSON button document.getElementById('download-json').addEventListener('click', () => { if (precipTable) { const year = new URLSearchParams(window.location.search).get('year') || new Date().getFullYear(); const network = getNetworkFromURL(); precipTable.download("json", `${network}_${year}_precipitation.json`); } }); // Copy to clipboard button document.getElementById('copy-clipboard').addEventListener('click', () => { if (precipTable) { precipTable.copyToClipboard("active"); } }); // Clear all filters button document.getElementById('clear-filters').addEventListener('click', () => { if (precipTable) { precipTable.clearHeaderFilter(); } }); } // Initialize the application document.addEventListener('DOMContentLoaded', () => { statusEl = document.getElementById('precip-status'); // Setup the "Interactive Grid" button const createGridButton = document.getElementById('create-grid'); if (createGridButton) { createGridButton.addEventListener('click', () => { if (statusEl) {statusEl.textContent = 'Initializing interactive table…';} initializeTable(); }); } // Setup table controls setupControls(); }); ================================================ FILE: htdocs/ASOS/reports/mon_prec.php ================================================ title = "{$year} {$network} Monthly Precipitation"; $pgconn = iemdb("iem"); $nt = new NetworkTable($network); $cities = $nt->table; $stname = iem_pg_prepare($pgconn, <<= 0 GROUP by id, month EOM ); $rs = pg_execute($pgconn, $stname, array($network)); $data = array(); for ($i = 0; $row = pg_fetch_assoc($rs); $i++) { if (!array_key_exists($row['id'], $data)) { $data[$row['id']] = array( null, null, null, null, null, null, null, null, null, null, null, null ); } $data[$row["id"]][intval($row["month"]) - 1] = $row["precip"]; } $t->headextra = << EOM; $t->jsextra = << EOM; reset($data); function friendly($val) { if (is_null($val)) return "M"; return sprintf("%.2f", $val); } $table = ""; foreach ($data as $key => $val) { $table .= sprintf( "%s%s %s%s%s %s%s%s %s%s%s %s%s%s %.2f%.2f ", $key, $network, $key, $cities[$key]["name"], friendly($val[0]), friendly($val[1]), friendly($val[2]), friendly($val[3]), friendly($val[4]), friendly($val[5]), friendly($val[6]), friendly($val[7]), friendly($val[8]), friendly($val[9]), friendly($val[10]), friendly($val[11]), array_sum(array_slice($val, 4, 4)), array_sum($val) ); } $d = date("d M Y h a"); $t->content = <<

{$year} {$network} Monthly Precipitation

This table was generated at {$d} and is based on available ASOS data. No attempt was made to estimate missing data.

{$netselect}
{$yselect}

Monthly Precipitation Data

{$table}
Monthly and seasonal (MJJA) precipitation totals (inches) for stations in the {$network} network, year {$year}. M denotes missing.
ID Name Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec MJJA Year
EOM; $t->render('single.phtml'); ================================================ FILE: htdocs/AWOS/current.phtml ================================================ title = "Iowa AWOS Network"; $t->content = <<Iowa AWOS Network

The term "AWOS" is typically applied to a subset of ASOS stations (airport weather stations) that are not federally owned. Over the years, the IEM has collected some datasets and products that are limited to AWOS sites in Iowa. This page details these Iowa-specific AWOS products. The main page for data from these sites resides with the ASOS Mainpage.

Docs:

Archived One Minute Data:

The Iowa DOT kept an one minute interval archive of their data between 1995 and 1 April 2011. Due to communication changes, this archival was discontinued. You can download data from this archive at the links below.

QC Info:

EOM; $t->render('single.phtml'); ================================================ FILE: htdocs/AWOS/reports/mon_prec.php ================================================ title = "AWOS Archive | Sky Coverages"; $t->content = <<

AWOS Sky Coverages

The Iowa DOT provides the IEM with an archive of 1 minute observations from their AWOS network. This archive contains numerically coded values for sky coverage. Unfortunately, there does not appear to be a definitive cross reference to match these values up to something humans can understand (ex. overcast). Here is our (Harry Hillaker and Daryl Herzmann) educated guess at a cross reference to what the sky coverage codes mean that are found in the archive.

Code Translated Code: Meaning
0NOREPORT No report, since lower cloud layers are obstructing.
1SCATTERED Scattered (10% to 50% coverage)
2BROKEN Broken (60% to 90% coverage)
4OVERCAST Overcast (More than 90% coverage)
8OBSCURATION Full obscuration (no ceiling or cloud amount available)
17OBSCURATION Partial obscuration, lowest cloud layer is scattered.
18OBSCURATION Partial obscuration, lowest cloud layer is broken.
20OBSCURATION Partial obscuration, lowest cloud layer is overcast.
32INDEFINITE Indefinite ceiling (no cloud cover amount available?)
64CLEAR No clouds below 12000 feet
128FEW Few (less than 10% coverage)
255MISSING Missing (sky condition not measured).
*UNKNOWNCODE All other values that may appear in the database.
EOM; $t->render('single.phtml'); ================================================ FILE: htdocs/COOP/7am-app.js ================================================ /* global ol */ let renderattr = 'pday'; let map = null; let coopLayer = null; let azosLayer = null; let mrmsLayer = null; let cocorahsLayer = null; /** * Replace HTML special characters with their entity equivalents * @param string val * @returns string converted string */ function escapeHTML(val) { return val .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Format date as YYMMDD string * @param {Date} date * @returns {string} */ function formatDateYYMMDD(date) { const year = date.getFullYear().toString().substring(2); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); return year + month + day; } /** * Format date as YYYY-MM-DD string * @param {Date} date * @returns {string} */ function formatDateISO(date) { const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); return `${year}-${month}-${day}`; } /** * Parse YYMMDD string to Date object * @param {string} dateStr * @returns {Date} */ function parseDateYYMMDD(dateStr) { const year = parseInt(`20${dateStr.substring(0, 2)}`, 10); const month = parseInt(dateStr.substring(2, 4), 10) - 1; const day = parseInt(dateStr.substring(4, 6), 10); return new Date(year, month, day); } /** * Parse YYYY-MM-DD string to Date object without timezone issues * @param {string} dateStr * @returns {Date} */ function parseDateISO(dateStr) { const parts = dateStr.split('-'); const year = parseInt(parts[0]); const month = parseInt(parts[1]) - 1; // Month is 0-based const day = parseInt(parts[2]); return new Date(year, month, day); } /** * Add days to a date string without timezone issues * @param {string} dateStr - YYYY-MM-DD format * @param {number} days - Number of days to add (can be negative) * @returns {string} - New date in YYYY-MM-DD format */ function addDaysToDateString(dateStr, days) { const date = parseDateISO(dateStr); date.setDate(date.getDate() + days); return formatDateISO(date); } /** * Migrate legacy hash URLs to URLSearchParams * This maintains backward compatibility with old bookmarked URLs * Legacy format: #240610/pday * New format: ?date=240610&attr=pday */ function migrateLegacyHashURLs() { const hash = window.location.hash; if (hash && hash.length > 1) { const hashContent = hash.substring(1); // Remove the # character const subtokens = hashContent.split('/'); if (subtokens.length >= 1) { const url = new URL(window.location); // Clear the hash and set as URL parameters url.hash = ''; url.searchParams.set('date', subtokens[0]); if (subtokens.length > 1) { url.searchParams.set('attr', subtokens[1]); } // Replace the URL without adding to history window.history.replaceState({}, '', url); return { date: subtokens[0], attr: subtokens.length > 1 ? subtokens[1] : null }; } } return null; } /** * Parse URL parameters to set initial state * Handles both URLSearchParams and legacy hash URLs (with migration) */ function parseURLParams() { // First, check for legacy hash URLs and migrate them const migrated = migrateLegacyHashURLs(); // Get current URL parameters (either from migration or existing params) const params = new URLSearchParams(window.location.search); const dateParam = params.get('date') || (migrated?.date); const attrParam = params.get('attr') || (migrated?.attr); if (attrParam) { renderattr = escapeHTML(attrParam); const renderAttrElement = document.getElementById('renderattr'); if (renderAttrElement instanceof HTMLSelectElement) { renderAttrElement.value = renderattr; } } if (dateParam) { const dt = parseDateYYMMDD(escapeHTML(dateParam)); const datepickerElement = document.getElementById('datepicker'); if (datepickerElement instanceof HTMLInputElement) { datepickerElement.value = formatDateISO(dt); } } } /** * Update URL with current parameters using URLSearchParams */ function updateURL() { const datepickerElement = document.getElementById('datepicker'); if (!(datepickerElement instanceof HTMLInputElement)) {return;} // Use parseDateISO to avoid timezone issues instead of new Date() const selectedDate = parseDateISO(datepickerElement.value); const tt = formatDateYYMMDD(selectedDate); const url = new URL(window.location); url.searchParams.set('date', tt); url.searchParams.set('attr', renderattr); // Clear any legacy hash url.hash = ''; // Update URL without page reload window.history.replaceState({}, '', url); } function updateMap() { const renderattrElement = document.getElementById('renderattr'); if (renderattrElement instanceof HTMLSelectElement) { renderattr = escapeHTML(renderattrElement.value); } coopLayer.setStyle(coopLayer.getStyle()); azosLayer.setStyle(azosLayer.getStyle()); updateURL(); // Announce change to assistive tech const status = document.getElementById('status'); if (status) { const label = renderattrElement?.selectedOptions?.[0]?.text || renderattr; status.textContent = `Parameter updated: ${label}`; } } function updateDate() { // We have a changed date, hello! const datepickerElement = document.getElementById('datepicker'); if (!(datepickerElement instanceof HTMLInputElement)) {return;} // Use parseDateISO to avoid timezone issues instead of new Date() const selectedDate = parseDateISO(datepickerElement.value); const fullDate = formatDateISO(selectedDate); // Create new sources with updated URLs to force refresh // This ensures OpenLayers actually fetches new data cocorahsLayer.setSource(new ol.source.Vector({ format: new ol.format.GeoJSON(), projection: ol.proj.get('EPSG:3857'), url: `/geojson/7am.py?group=cocorahs&dt=${fullDate}`, })); coopLayer.setSource(new ol.source.Vector({ format: new ol.format.GeoJSON(), projection: ol.proj.get('EPSG:3857'), url: `/geojson/7am.py?group=coop&dt=${fullDate}`, })); azosLayer.setSource(new ol.source.Vector({ format: new ol.format.GeoJSON(), projection: ol.proj.get('EPSG:3857'), url: `/geojson/7am.py?group=azos&dt=${fullDate}`, })); mrmsLayer.setSource( new ol.source.XYZ({ url: get_tms_url(), }) ); updateURL(); const status = document.getElementById('status'); if (status) { status.textContent = `Date updated: ${fullDate}`; } } function pretty(val) { if (val === null || val === undefined) {return 'M';} if (val === 0.0001) {return 'T';} return val; } function makeVectorLayer(dt, title, group) { return new ol.layer.Vector({ title, source: new ol.source.Vector({ format: new ol.format.GeoJSON(), projection: ol.proj.get('EPSG:3857'), url: `/geojson/7am.py?group=${group}&dt=${dt}`, }), style(feature) { let txt = feature.get(renderattr) === 0.0001 ? 'T' : feature.get(renderattr); txt = txt === null || txt === undefined ? '.' : txt; return [ new ol.style.Style({ text: new ol.style.Text({ font: '14px Calibri,sans-serif', text: txt.toString(), stroke: new ol.style.Stroke({ color: '#fff', width: 3, }), fill: new ol.style.Fill({ color: 'black', }), }), }), ]; }, }); } function get_tms_url() { // Generate the TMS URL given the current settings const datepickerElement = document.getElementById('datepicker'); if (!(datepickerElement instanceof HTMLInputElement)) {return '';} // Use parseDateISO to avoid timezone issues instead of new Date() const selectedDate = parseDateISO(datepickerElement.value); const dateStr = formatDateISO(selectedDate); return `https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/idep0::mrms-12z24h::${dateStr}/{z}/{x}/{y}.png`; } function buildUI() { const renderAttrElement = document.getElementById('renderattr'); if (renderAttrElement) { renderAttrElement.addEventListener('change', () => { updateMap(); }); } // Set up date input with HTML5 date type const datepickerElement = document.getElementById('datepicker'); if (datepickerElement instanceof HTMLInputElement) { datepickerElement.type = 'date'; datepickerElement.min = '2009-02-01'; datepickerElement.max = formatDateISO(new Date()); datepickerElement.value = formatDateISO(new Date()); datepickerElement.addEventListener('change', () => { updateDate(); }); } const minusDayElement = document.getElementById('minusday'); if (minusDayElement) { minusDayElement.addEventListener('click', (event) => { event.preventDefault(); // Prevent form submission const datepicker = document.getElementById('datepicker'); if (datepicker instanceof HTMLInputElement) { if (datepicker.value) { datepicker.value = addDaysToDateString(datepicker.value, -1); updateDate(); } } }); } const plusDayElement = document.getElementById('plusday'); if (plusDayElement) { plusDayElement.addEventListener('click', (event) => { event.preventDefault(); // Prevent form submission const datepicker = document.getElementById('datepicker'); if (datepicker instanceof HTMLInputElement) { if (datepicker.value) { datepicker.value = addDaysToDateString(datepicker.value, 1); updateDate(); } } }); } } document.addEventListener('DOMContentLoaded', () => { buildUI(); parseURLParams(); // Get the date from the datepicker after parseURLParams() has run // This will be either the URL date or today's date (set by buildUI) const datepickerElement = document.getElementById('datepicker'); let currentDate = formatDateISO(new Date()); if (datepickerElement instanceof HTMLInputElement) { if (datepickerElement.value) { currentDate = datepickerElement.value; } } cocorahsLayer = makeVectorLayer(currentDate, 'IA CoCoRaHS Reports', 'cocorahs'); coopLayer = makeVectorLayer(currentDate, 'NWS COOP Reports', 'coop'); azosLayer = makeVectorLayer(currentDate, 'ASOS/AWOS Reports', 'azos'); mrmsLayer = new ol.layer.Tile({ title: 'MRMS 12z 24 Hour', source: new ol.source.XYZ({ url: get_tms_url(), }), }); map = new ol.Map({ target: 'map', layers: [ mrmsLayer, new ol.layer.Tile({ title: 'County Boundaries', source: new ol.source.XYZ({ url: '/c/tile.py/1.0.0/uscounties/{z}/{x}/{y}.png', }), }), new ol.layer.Tile({ title: 'State Boundaries', source: new ol.source.XYZ({ url: '/c/tile.py/1.0.0/usstates/{z}/{x}/{y}.png', }), }), new ol.layer.Tile({ title: 'NWS CWA Boundaries', source: new ol.source.XYZ({ url: '/c/tile.py/1.0.0/wfo/{z}/{x}/{y}.png', }), }), cocorahsLayer, coopLayer, azosLayer, ], view: new ol.View({ projection: 'EPSG:3857', center: [-10505351, 5160979], zoom: 7, }), }); const layerSwitcher = new ol.control.LayerSwitcher(); map.addControl(layerSwitcher); const element = document.getElementById('popup'); const popup = new ol.Overlay({ element, positioning: 'bottom-center', stopEvent: false, }); map.addOverlay(popup); // Simple popover implementation to replace Bootstrap popover let popoverVisible = false; function showPopover(content) { if (element) { // Put content directly in the popup element element.innerHTML = content; // Use CSS classes instead of inline styles element.classList.add('popup-visible'); popoverVisible = true; } } function hidePopover() { if (element && popoverVisible) { element.classList.remove('popup-visible'); element.innerHTML = ''; popoverVisible = false; } } // display popup on click map.on('click', evt => { const feature = map.forEachFeatureAtPixel(evt.pixel, feature2 => { return feature2; }); if (feature) { const geometry = feature.getGeometry(); const coord = geometry.getCoordinates(); popup.setPosition(coord); const link = `/sites/site.php?station=${feature.getId()}&network=${feature.get('network')}`; const dt = new Date(feature.get('valid')); const content = [ `

${feature.getId()} ${feature.get('name')}`, `
Hour of Ob: ${feature.get('hour')}`, `
High: ${pretty(feature.get('high'))}`, `
Low: ${pretty(feature.get('low'))}`, `
Temp at Ob: ${pretty(feature.get('coop_tmpf'))}`, `
Precip: ${pretty(feature.get('pday'))}`, `
Snow: ${pretty(feature.get('snow'))}`, `
Snow Depth: ${pretty(feature.get('snowd'))}`, `
Valid: ${dt}`, '

', ]; showPopover(content.join('')); } else { hidePopover(); } }); }); ================================================ FILE: htdocs/COOP/7am.css ================================================ .map { height: 400px; width: 100%; background-color: #FFFFFF; } .popover { width: 300px; } /* Custom popup styles for map feature popover */ #popup { position: relative; background: white; border: 1px solid #ccc; border-radius: 5px; padding: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); font-size: 12px; line-height: 1.4; min-width: 200px; max-width: 300px; display: none; /* Hidden by default */ } /* Visible state for popup */ #popup.popup-visible { display: block; } /* Style for links within popup */ #popup a { color: #0066cc; text-decoration: none; } #popup a:hover { text-decoration: underline; } /* Strong text styling within popup */ #popup strong { font-weight: bold; } ================================================ FILE: htdocs/COOP/7am.php ================================================ title = "Iowa Daily COOP Reports and Comparisons"; $t->headextra = << EOM; $t->jsextra = << EOM; $t->content = <<

Iowa Daily COOP Reports & Comparisons

Visual comparison of multiple data sources for the once-daily COOP reports. Choose a parameter and date to update the map. Data is valid for the 24-hour period ending near 7 AM local time.

Map Controls

Select the variable to render on the map.
MRMS Legend:
Legend for MRMS 24 hour precipitation
View Date
Select a date between Feb 1 2009 and today.

Interactive Map

Use the layer switcher to toggle station networks or MRMS background. Click a station label for details.

EOM; $t->render("full.phtml"); ================================================ FILE: htdocs/COOP/cat.module.js ================================================ let currentmode = true; /** * Toggle visibility of table rows without data */ function hiderows() { const noDataRows = document.querySelectorAll('tr.nodata'); const rowShower = document.getElementById('rowshower'); if (currentmode) { noDataRows.forEach(row => { row.style.display = 'none'; }); if (rowShower) { rowShower.value = 'Show rows without data'; } } else { noDataRows.forEach(row => { row.style.display = ''; }); if (rowShower) { rowShower.value = 'Hide rows without data'; } } currentmode = !currentmode; } document.addEventListener('DOMContentLoaded', () => { const rowShower = document.getElementById('rowshower'); if (rowShower) { rowShower.addEventListener('click', hiderows); } }); ================================================ FILE: htdocs/COOP/cat.phtml ================================================ title = "NWS COOP Observations by Date by Site"; $content = <<

This page presents the raw observations of daily high and low temperature along with precipitation and snow when available. The dates shown are the date when the observation was reported. For observations made at 7 AM, this would be a for a 24 hour period up until that time and not for that local date. The timestamps shown are valid for the local timezone of that reporting station.

Hint: You should be able to highlight the values in the table and then copy + paste these values into a spreadsheet program like Excel. A download interface does exist for these observations.
EOM; // SCENARIO 1: Print out data for year for station if (strlen($station) > 0) { $link = "cat.phtml?year=$year&station=$station&network=$network&sortvar="; if ($sortvar == "") $sortvar = "day"; $stationselect = networkSelect($network, $station); $content .= <<

COOP obs listed by station

Click on a date to get all obs for a particular date.
{$stationselect} EOM; $content .= yearSelect(2004, $year, "year"); $content .= "
"; } // SCENARIO 2: Print out all stations for network and date if (strlen($date) > 0) { $year = substr($date, 0, 4); $link = "cat.phtml?date=$date&network=$network&sortvar="; if ($sortvar == "") $sortvar = "station"; $content .= <<

COOP obs listed by date

Click on the NWSLI to view all obs from one station.
EOM; } $netselect = selectNetworkType("COOP", $network); $content .= << {$netselect}
EOM; $nt = new NetworkTable($network); $cities = $nt->table; $iemdb = iemdb("iem"); $sortdir = "ASC"; if ( $sortvar == "pday" || $sortvar == "snow" || $sortvar == "snowd" || $sortvar == "max_tmpf" || $sortvar == "snoww" ) $sortdir = "DESC"; if (strlen($station) > 0) { $stname = iem_pg_prepare($iemdb, "SELECT c.*, s.id as station, s.name as sname, c.coop_valid at time zone s.tzname as cts, c.report from summary_$year c, stations s WHERE s.id = $1 and s.network = $2 and c.iemid = s.iemid and c.day <= 'TODAY' ORDER by $sortvar $sortdir"); $rs = pg_execute($iemdb, $stname, array($station, $network)); } else { $stname = iem_pg_prepare($iemdb, "SELECT c2.*, s.id as station, s.name as sname, c2.coop_valid at time zone s.tzname as cts, c2.report from summary_$year c2, stations s WHERE c2.day = $1 and s.network = $2 and c2.iemid = s.iemid ORDER by $sortvar $sortdir"); $rs = pg_execute($iemdb, $stname, array($date, $network)); } while ($row = pg_fetch_assoc($rs)) { $nwsli = $row["station"]; $high = $row["max_tmpf"]; if ($high == "-99" || $high == "") $high = "M"; if ($high > 130 || $high < -90) $high = "M"; $tmpf = $row["coop_tmpf"]; if ($tmpf == "-99" || $tmpf == "") $tmpf = "M"; if ($tmpf > 130 || $tmpf < -90) $tmpf = "M"; $low = $row["min_tmpf"]; if ($low == "99" || $low == "" || $low == -99) $low = "M"; if ($low > 130 || $low < -90) $low = "M"; $rain = $row["pday"]; if ($rain == 0.0001) $rain = "T"; else if ($rain == -99 || $rain == "") $rain = "M"; else if ($rain > 30 || $rain < 0) $rain = "M"; $snow = $row["snow"]; if ($snow == 0.0001) $snow = "T"; else if ($snow == -99 || $snow == "") $snow = "M"; else if ($snow > 100 || $snow < 0) $snow = "M"; $snowd = $row["snowd"]; if ($snowd == 0.0001) $snowd = "T"; else if ($snowd == -99 || $snowd == "") $snowd = "M"; else if ($snowd > 1000 || $snowd < 0) $snowd = "M"; $snoww = $row["snoww"]; if ($snoww == 0.0001) $snoww = "T"; else if ($snoww == -99 || $snoww == "") $snoww = "M"; else if ($snoww > 1000 || $snoww < 0) $snoww = "M"; $rowcontent = ""; $dstring = $row["day"]; if ( $tmpf == "M" && $high == "M" && $low == "M" && $rain == "M" && $snow == "M" && $snowd == "M" ) { $dddd = "" . $dstring . ""; $rowcontent = str_replace("", "", $rowcontent) . ""; } else { $rowcontent .= "\n"; if (!is_null($row["report"])) { $rowcontent .= sprintf( '', $row["report"], ); } } $content .= $rowcontent; } $content .= <<

Errors exist in this dataset and you should evaluate the observations before using...

EOM; $t->content = $content; $t->jsextra = ''; $t->render('full.phtml'); ================================================ FILE: htdocs/COOP/current.phtml ================================================ "; $sitesurl = sprintf( "/sites/site.php?station=%s&network=%s", $dict["sid"], $dict["network"] ); $s .= <<
EOM; $s .= ""; $s .= ""; $s .= ""; $fmt = (date("Ymd") != date("Ymd", $dict["ts"])) ? 'd M Y h:i A' : 'h:i A'; $s .= ""; $s .= sprintf( "", $dict["tmpf"] != "" ? $dict["tmpf"] : "M", $dict["max_tmpf"] != "" ? $dict["max_tmpf"] : "M", $dict["min_tmpf"] != "" ? $dict["min_tmpf"] : "M" ); $s .= sprintf( "", precip_formatter($dict["pday"]), precip_formatter($dict["snow"]), precip_formatter($dict["ratio"]), precip_formatter($dict["snowd"]), precip_formatter($dict["snoww"]) ); $s .= "\n"; return $s; } if (is_null($wfo)) { $arr = array( "network" => $network, ); $baseurl = "current.phtml?network=$network"; } else { $arr = array( "networkclass" => "COOP", "wfo" => $wfo, ); $baseurl = "current.phtml?wfo=$wfo"; } $jobj = iemws_json("currents.json", $arr); $db = array(); foreach ($jobj["data"] as $bogus => $iemob) { $site = $iemob["station"]; $db[$site] = array( 'snow' => "", 'snowd' => "", 'ratio' => "", 'pday' => "", 'min_tmpf' => "", 'max_tmpf' => "", 'tmpf' => "", 'snoww' => "" ); $db[$site]['ts'] = strtotime($iemob["local_valid"]); $db[$site]['lts'] = strtotime($iemob["local_valid"]); $db[$site]['sid'] = $site; $db[$site]['name'] = $iemob["name"]; $db[$site]['raw'] = $iemob["raw"]; $db[$site]['state'] = $iemob["state"]; $db[$site]['network'] = $iemob["network"]; $db[$site]['county'] = $iemob["county"]; if ($iemob["tmpf"] > -100) { $db[$site]['tmpf'] = $iemob["tmpf"]; } if ($iemob["max_tmpf"] > -100) { $db[$site]['max_tmpf'] = $iemob["max_tmpf"]; } if ($iemob["min_tmpf"] < 99) { $db[$site]['min_tmpf'] = $iemob["min_tmpf"]; } $db[$site]['pday'] = $iemob["pday"]; $db[$site]['snoww'] = $iemob["snoww"]; $db[$site]['snow'] = ($iemob["snow"] >= 0) ? $iemob["snow"] : ""; $db[$site]['snowd'] = ($iemob["snowd"] >= 0) ? $iemob["snowd"] : ""; $db[$site]["ratio"] = -1; if ($db[$site]["snow"] > 0.0001 && $db[$site]["pday"] > 0.0001) { $db[$site]["ratio"] = intval($db[$site]["snow"] / $db[$site]["pday"]); } } $db = aSortBySecondIndex($db, $sortcol, $sortdir); $oddrow = true; $firstsection = ""; $lastsection = ""; foreach ($db as $site => $value) { $oddrow = !$oddrow; if (date("Ymd", $value["ts"]) == date("Ymd")) { $firstsection .= make_row($value, $oddrow); } else { $value["tmpf"] = ""; $lastsection .= make_row($value, $oddrow); } } function get_sortdir($baseurl, $column, $sortCol, $sortDir) { $newSort = ($sortDir == "asc") ? "desc" : "asc"; if ($column == $sortCol) return "{$baseurl}&sortcol=$column&sortdir=$newSort"; return "{$baseurl}&sortcol=$column&sortdir=$sortDir"; } $t->title = "NWS COOP Current Sortables"; $cols = array( "ts" => "Valid", "county" => "County", "sid" => "Site ID", "name" => "Station Name", "ratio" => "Snow to Water Ratio", "tmpf" => "Ob Temperature", "max_tmpf" => "24 hour High", "min_tmpf" => "24 hour Low", "snow" => "24 hour Snowfall", "snowd" => "Snowfall Depth", "pday" => "24 hour rainfall", "phour" => "Rainfall One Hour", "snoww" => "Snow Water Equivalent" ); if (!array_key_exists($sortcol, $cols)) { xssafe(""); } $t->current_network = "COOP"; $nselect = '"; $wselect = ""; $sorts = array("asc" => "Ascending", "desc" => "Descending"); $one = get_sortdir($baseurl, "sid", $sortcol, $sortdir); $two = get_sortdir($baseurl, "name", $sortcol, $sortdir); $three = get_sortdir($baseurl, "county", $sortcol, $sortdir); $four = get_sortdir($baseurl, "ts", $sortcol, $sortdir); $five = get_sortdir($baseurl, "tmpf", $sortcol, $sortdir); $six = get_sortdir($baseurl, "max_tmpf", $sortcol, $sortdir); $seven = get_sortdir($baseurl, "min_tmpf", $sortcol, $sortdir); $eight = get_sortdir($baseurl, "pday", $sortcol, $sortdir); $nine = get_sortdir($baseurl, "snow", $sortcol, $sortdir); $ten = get_sortdir($baseurl, "ratio", $sortcol, $sortdir); $eleven = get_sortdir($baseurl, "snowd", $sortcol, $sortdir); $twelve = get_sortdir($baseurl, "snoww", $sortcol, $sortdir); $content = << {$nselect}
{$wselect}

Sorted by: {$cols[$sortcol]} {$sorts[$sortdir]}. Times are presented in the local time of the site. Click on the identifier to get all daily observations for the site. Click on the site name to get more information on the site. Click on the column heading to sort the column, clicking again will reverse the sort. A new interactive map app exists that plots this data on a map.

NWSLI: Site Name: Date: Temp at Ob Max Air Temp Min Air Temp Precip Snowfall Snow Depth Snow Water Equiv
"; if (strlen($station) > 0) { $rowcontent .= $nwsli; } else { $rowcontent .= "" . $nwsli . ""; } $rowcontent .= "" . $cities[$nwsli]["name"] . "
No Observation Reported for $dddd
"; if ($row["cts"]) { $dstring = date("j M Y gA", strtotime(substr($row["coop_valid"], 0, 16))); } if (strlen($date) > 0) { $rowcontent .= $dstring; } else { $rowcontent .= "" . $dstring . ""; } $rowcontent .= "" . $tmpf . "" . $high . "" . $low . "" . $rain . "" . $snow . "" . $snowd . "" . $snoww . "
%s
Source Text " . $dict['sid'] . "" . $dict["name"] . ", " . $dict["state"] . "" . $dict["county"] . "" . date($fmt, $dict["lts"]) . "%s%s %s%s%s%s%s%s
EOM; $content .= <<
Add: Source Text: SiteID: Station Name: County: Valid: Temperatures [F] Hydro
At Ob 24h High 24h Low 24hour Rain Snowfall Ratio Snow Depth SWE
EOM; $t->content = $content; $t->render('sortables.phtml'); ================================================ FILE: htdocs/COOP/dl/normals.phtml ================================================ title = "NWS COOP Daily Normals"; $year = date("Y"); $network = isset($_REQUEST['network']) ? xssafe($_REQUEST['network']) : 'IACLIMATE'; $nselect = networkSelect($network, ""); $mselect = monthSelect("month"); $dselect = daySelect("day"); $sselect = selectNetworkType("CLIMATE", $network); $t->content = <<With this interface, you can download daily climate normals for NWS COOP sites. Please fill out the form below:

Data is available from the following states:

{$sselect}

1. Climatology Source:

The IEM maintains a set of station idenitifers that does not exactly match what NCEI uses. When you select the NCEI climatology and an IEM station identifier, you are getting the cross reference between the two idenitifer sets. Hopefully, this is generally one to one, but it could be "nearest station"

Select Data Source

2. Download Type:

2a. Select Station:
Single Station, All Days
{$nselect}

2b. Select Month & Date:
All Stations, One Day
Select Month: {$mselect}
Select Day: {$dselect}

3. Download Options:

4. Submit Form:


Data includes Lat/Lon information. EOM; $t->render('single.phtml'); ================================================ FILE: htdocs/COOP/extremes.css ================================================ /* COOP Extremes App Styles */ /* Loading indicator */ .spinner-border { width: 3rem; height: 3rem; } /* Sticky table header */ .sticky { position: sticky; top: 0; background-color: var(--bs-light); z-index: 10; } /* Table styling improvements */ .table-responsive { margin-top: 1rem; overflow-x: auto; } /* Tabulator horizontal scrolling */ .tabulator { overflow-x: auto !important; width: 100% !important; } .tabulator .tabulator-tableHolder { overflow-x: auto !important; } #data-table { margin-bottom: 0; min-width: 1300px; /* Ensure minimum width for all columns */ } #data-table th { background-color: var(--bs-light); font-weight: 600; border-bottom: 2px solid var(--bs-border-color); } #data-table th a { color: var(--bs-primary); text-decoration: none; } #data-table th a:hover { color: var(--bs-primary); text-decoration: underline; } /* Mobile responsive improvements */ @media (max-width: 768px) { .table-responsive { font-size: 0.875rem; } #data-table th, #data-table td { padding: 0.5rem 0.25rem; white-space: nowrap; } /* Hide less critical columns on small screens */ .hide-mobile { display: none; } } /* API info styling */ #api-info { margin-top: 1rem; margin-bottom: 1rem; } #api-info code { word-break: break-all; background-color: var(--bs-gray-100); padding: 0.25rem 0.5rem; border-radius: 0.25rem; } /* Form styling */ #controls-form { margin-bottom: 1rem; } #controls-form .table { margin-bottom: 0; } /* Form controls styling */ #controls-form .card { border: 1px solid #dee2e6; margin-bottom: 1.5rem; } #controls-form .card-header { background-color: #f8f9fa; border-bottom: 1px solid #dee2e6; padding: 0.75rem 1rem; } #controls-form .card-header h6 { color: #0d6efd; font-weight: 600; } #controls-form .form-label { font-weight: 500; margin-bottom: 0.25rem; } #controls-form .form-select, #controls-form .btn { font-size: 0.875rem; } /* Header section */ #header-section h3 { color: #0d6efd; font-size: 1.5rem; } #header-section h4 { font-size: 1.25rem; font-weight: 600; } #header-section .text-muted { font-size: 0.9rem; } /* Error state styling */ .error-message { color: var(--bs-danger); padding: 1rem; background-color: var(--bs-danger-bg-subtle); border: 1px solid var(--bs-danger-border-subtle); border-radius: 0.375rem; margin: 1rem 0; } /* Mode explanation alert */ .alert-info h5 { color: #0c5460; font-size: 1rem; margin-bottom: 0.75rem; } .alert-info .row > div { padding: 0.5rem; } .alert-info strong { color: #0c5460; font-size: 0.95rem; } .alert-info .small { line-height: 1.4; margin-top: 0.25rem; } /* Back button styling */ .btn-outline-primary.btn-sm { font-size: 0.8rem; padding: 0.375rem 0.75rem; } /* Column grouping visual separation */ .temp-group { border-left: 3px solid var(--bs-warning); } .precip-group { border-left: 3px solid var(--bs-info); } /* Sortable column indicators */ .sortable { cursor: pointer; user-select: none; } .sortable:hover { background-color: var(--bs-gray-100); } .sort-indicator { margin-left: 0.25rem; opacity: 0.5; font-size: 0.75rem; } .sort-indicator.active { opacity: 1; } /* Map container and view toggle */ #view-toggle { margin-bottom: 1rem; } #view-toggle .btn-group { box-shadow: 0 2px 4px rgba(0,0,0,0.1); } #map-container { height: 500px; width: 100%; border: 2px solid var(--bs-border-color); border-radius: 0.375rem; margin-bottom: 1rem; position: relative; } #map-container.hidden { display: none; } /* Map popup styling */ .ol-popup { position: absolute; background-color: white; box-shadow: 0 1px 4px rgba(0,0,0,0.2); padding: 15px; border-radius: 10px; border: 1px solid #cccccc; bottom: 12px; left: -50px; min-width: 280px; max-width: 400px; } .ol-popup:after, .ol-popup:before { top: 100%; border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; } .ol-popup:after { border-top-color: white; border-width: 10px; left: 48px; margin-left: -10px; } .ol-popup:before { border-top-color: #cccccc; border-width: 11px; left: 48px; margin-left: -11px; } .ol-popup-closer { text-decoration: none; position: absolute; top: 2px; right: 8px; font-size: 16px; color: #999; } .ol-popup-closer:after { content: "✖"; } .ol-popup-content { font-size: 14px; } .ol-popup-content h5 { margin: 0 0 5px 0; color: var(--bs-primary); } .popup-data-table { width: 100%; font-size: 12px; margin-top: 8px; } .popup-data-table td { padding: 2px 6px; border-bottom: 1px solid #eee; } .popup-data-table .label { font-weight: bold; color: #666; } /* Mobile responsive map */ @media (max-width: 768px) { #map-container { height: 400px; } .ol-popup { min-width: 200px; max-width: 280px; font-size: 12px; } .map-controls { position: relative; top: auto; left: auto; margin-bottom: 10px; width: 100%; display: flex; align-items: center; gap: 8px; } .map-controls label { white-space: nowrap; } } /* Layer switcher customization */ .layer-switcher { position: absolute; top: 10px; right: 10px; background: rgba(255, 255, 255, 0.95); border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } .layer-switcher button { background: none; border: none; padding: 8px; cursor: pointer; } .layer-switcher button:hover { background: rgba(0,0,0,0.1); } /* Legend for temperature colors */ .legend-title { font-weight: bold; margin-bottom: 3px; color: var(--bs-primary); font-size: 12px; } .legend-subtitle { font-size: 10px; color: #666; margin-bottom: 3px; font-style: italic; } .legend-item { display: flex; align-items: center; margin-bottom: 2px; } .legend-color { width: 12px; height: 12px; border-radius: 2px; margin-right: 5px; border: 1px solid #fff; } /* Map controls */ .map-controls { position: absolute; top: 10px; left: 10px; background: rgba(255, 255, 255, 0.95); padding: 12px; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.15); z-index: 100; font-size: 13px; min-width: 200px; } .control-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } .control-row:last-of-type { margin-bottom: 0; } .control-row label { font-weight: 500; margin: 0; white-space: nowrap; } /* Year filter specific styling */ .control-row:has(#year-filter) { border-top: 1px solid #e9ecef; padding-top: 8px; margin-top: 8px; } .control-row:has(#year-filter) label { color: #6c757d; font-size: 12px; } .legend-row { display: flex; align-items: flex-start; gap: 8px; } .legend-row label { font-weight: 500; margin: 0; white-space: nowrap; margin-top: 2px; } .map-legend { flex: 1; font-size: 11px; } .map-controls select { width: auto; display: inline-block; } /* Tabulator table styling overrides */ .tabulator { border: 1px solid var(--bs-border-color) !important; border-radius: 0.375rem !important; font-size: 0.875rem !important; background-color: var(--bs-body-bg) !important; } .tabulator .tabulator-header { background-color: var(--bs-light) !important; border-bottom: 2px solid var(--bs-border-color) !important; font-weight: 600 !important; } .tabulator .tabulator-header .tabulator-col { background-color: var(--bs-light) !important; border-right: 1px solid var(--bs-border-color) !important; color: var(--bs-body-color) !important; } .tabulator .tabulator-header .tabulator-col .tabulator-col-content { padding: 8px 6px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } .tabulator .tabulator-tableHolder .tabulator-table .tabulator-row { border-bottom: 1px solid var(--bs-border-color) !important; background-color: transparent !important; } .tabulator .tabulator-tableHolder .tabulator-table .tabulator-row:hover { background-color: var(--bs-light) !important; } .tabulator .tabulator-tableHolder .tabulator-table .tabulator-row .tabulator-cell { border-right: 1px solid var(--bs-border-color) !important; padding: 6px 8px !important; vertical-align: middle !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } /* Tabulator row striping */ .tabulator .tabulator-tableHolder .tabulator-table .tabulator-row:nth-child(even) { background-color: rgba(0, 0, 0, 0.05) !important; } .tabulator .tabulator-tableHolder .tabulator-table .tabulator-row:nth-child(even):hover { background-color: var(--bs-light) !important; } /* Station/Date column special styling */ .tabulator .tabulator-cell.station-col { font-weight: 600 !important; background-color: rgba(13, 110, 253, 0.1) !important; } .tabulator .tabulator-cell.years-col { text-align: center !important; font-weight: 500 !important; } /* Numeric cell alignment */ .tabulator .tabulator-cell.temp-group, .tabulator .tabulator-cell.precip-group { text-align: right !important; } /* Column header improvements */ .tabulator .tabulator-header .tabulator-col.temp-group .tabulator-col-content { background-color: rgba(255, 193, 7, 0.1) !important; } .tabulator .tabulator-header .tabulator-col.precip-group .tabulator-col-content { background-color: rgba(13, 202, 240, 0.1) !important; } /* Frozen column styling */ .tabulator .tabulator-cell.station-col { position: sticky !important; left: 0 !important; z-index: 5 !important; border-right: 2px solid var(--bs-border-color) !important; } .tabulator .tabulator-header .tabulator-col:first-child { position: sticky !important; left: 0 !important; z-index: 10 !important; border-right: 2px solid var(--bs-border-color) !important; } /* Enhanced Tabulator styling for frozen columns and height */ .tabulator .tabulator-freeze-left { border-right: 2px solid var(--bs-primary) !important; background-color: var(--bs-light) !important; z-index: 10 !important; } .tabulator .tabulator-freeze-left .tabulator-cell { background-color: var(--bs-light) !important; font-weight: 500 !important; } .tabulator .tabulator-freeze-left:hover .tabulator-cell { background-color: var(--bs-info) !important; } /* Ensure proper table height and scrolling */ .tabulator .tabulator-tableHolder { max-height: calc(70vh - 120px) !important; /* Account for header and pagination */ overflow-y: auto !important; } /* Pagination styling for better visibility */ .tabulator .tabulator-footer { border-top: 2px solid var(--bs-border-color) !important; background-color: var(--bs-light) !important; padding: 8px !important; } .tabulator .tabulator-paginator { color: var(--bs-body-color) !important; } .tabulator .tabulator-page { color: var(--bs-primary) !important; border: 1px solid var(--bs-border-color) !important; background-color: var(--bs-body-bg) !important; } .tabulator .tabulator-page.active { background-color: var(--bs-primary) !important; color: white !important; } /* Column grouping visual indicators for Tabulator */ .tabulator .tabulator-header .temp-group { border-left: 3px solid var(--bs-warning) !important; } .tabulator .tabulator-header .precip-group { border-left: 3px solid var(--bs-info) !important; } .tabulator .tabulator-tableHolder .tabulator-table .tabulator-row .temp-group { border-left: 3px solid var(--bs-warning) !important; } .tabulator .tabulator-tableHolder .tabulator-table .tabulator-row .precip-group { border-left: 3px solid var(--bs-info) !important; } /* Links in Tabulator cells */ .tabulator .tabulator-cell a { color: var(--bs-primary) !important; text-decoration: none !important; } .tabulator .tabulator-cell a:hover { color: var(--bs-primary) !important; text-decoration: underline !important; } /* Prevent text wrapping in table cells */ .tabulator .tabulator-cell { white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } .tabulator .tabulator-header .tabulator-col .tabulator-col-content { white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } /* Sort arrow styling */ .tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter { right: 4px !important; } .tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-content:hover { background-color: rgba(0, 0, 0, 0.1) !important; } /* Improve frozen column appearance */ .tabulator .tabulator-frozen.tabulator-frozen-left { border-right: 2px solid var(--bs-primary) !important; } /* Better responsive handling */ @media (max-width: 1200px) { .tabulator { font-size: 0.75rem !important; } .tabulator .tabulator-header .tabulator-col .tabulator-col-content { padding: 6px 4px !important; } .tabulator .tabulator-tableHolder .tabulator-table .tabulator-row .tabulator-cell { padding: 4px 6px !important; } } ================================================ FILE: htdocs/COOP/extremes.js ================================================ /** * COOP Extremes App - Simple function-based implementation * Handles data fetching, table rendering, and sorting for climatology data */ /* global ol, Tabulator */ // Global app state const appState = { config: null, data: null, sortColumn: null, sortDirection: null, isStationView: false, map: null, vectorSource: null, popup: null, currentApiUrl: null, labelAttribute: 'avg_high', // Default label attribute - use numeric for legend colorRanges: null, // Will store calculated color ranges for current attribute table: null, // Tabulator instance yearFilter: null, // Selected year for filtering allFeatures: null // Store all features for filtering }; /** * Initialize the application */ function initializeApp() { appState.config = getConfig(); appState.sortColumn = appState.config.sortcol; appState.sortDirection = appState.config.sortdir; appState.isStationView = Boolean(appState.config.station); appState.labelAttribute = appState.config.labelAttribute; // Set from URL parameter appState.yearFilter = appState.config.yearFilter; // Set from URL parameter // Hide/show components based on view mode if (appState.isStationView) { // Station view - hide map and form controls const mapContainer = document.getElementById('map-container'); const formContainer = document.getElementById('controls-form'); if (mapContainer) { mapContainer.style.display = 'none'; } if (formContainer) { formContainer.style.display = 'none'; } } else { // Date view - show form controls, hide form in station view const formContainer = document.getElementById('controls-form'); if (formContainer) { formContainer.style.display = 'block'; } } showLoading(true); fetchData() .then(() => { renderHeader(); updateTable(); showApiInfo(); if (!appState.isStationView) { initializeMap(); } attachEventListeners(); }) .catch((error) => { showError(`Failed to load climatology data: ${error.message}`); }) .finally(() => { showLoading(false); }); } /** * Extract configuration from DOM elements (form inputs, URL parameters) */ function getConfig() { // Get URL parameters const urlParams = new URLSearchParams(window.location.search); // Helper to get value from URL or fallback function getParamOrInput(param, selector, fallback) { const urlVal = urlParams.get(param); if (urlVal !== null && urlVal !== undefined && urlVal !== '') { return urlVal; } const input = document.querySelector(selector); if (input && input.value !== undefined && input.value !== '') { return input.value; } return fallback; } return { tbl: getParamOrInput('tbl', 'select[name="tbl"]', 'climate'), month: parseInt(getParamOrInput('month', 'select[name="month"]', new Date().getMonth() + 1), 10), day: parseInt(getParamOrInput('day', 'select[name="day"]', new Date().getDate()), 10), sortcol: getParamOrInput('sortcol', 'unused', 'station'), network: getParamOrInput('network', 'select[name="network"]', 'IACLIMATE'), station: urlParams.get('station') || null, sortdir: getParamOrInput('sortdir', 'unused', 'ASC'), labelAttribute: getParamOrInput('label', 'unused', 'avg_high'), yearFilter: getParamOrInput('year', 'unused', null) }; } /** * Fetch climatology data from appropriate API endpoint */ function fetchData() { const vars = appState.config; const syear = getStartYear(vars.tbl); const eyear = getEndYear(vars.tbl); const apiUrl = vars.station ? `/json/climodat_stclimo.py?station=${vars.station}&syear=${syear}&eyear=${eyear}` : `/geojson/climodat_dayclimo.py?network=${vars.network}&month=${vars.month}&day=${vars.day}&syear=${syear}&eyear=${eyear}`; appState.currentApiUrl = apiUrl; return fetch(apiUrl) .then((response) => { if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); }) .then((jsonData) => { if (vars.station) { // Process station data appState.data = jsonData.climatology.map((item) => ({ ...item, valid: new Date(2000, item.month - 1, item.day) // Create date for sorting })); } else { // Process day data from GeoJSON appState.data = jsonData.features.map((feature) => feature.properties); } }); }; /** * Get start year based on table selection */ function getStartYear(tbl) { switch (tbl) { case 'climate51': return 1951; case 'climate71': return 1971; case 'climate81': return 1981; default: return 1800; } }; /** * Get end year based on table selection */ function getEndYear(tbl) { switch (tbl) { case 'climate71': return 2001; case 'climate81': return 2011; default: return new Date().getFullYear() + 1; } }; /** * Render header section with clear mode indication */ function renderHeader() { const headerSection = document.getElementById('header-section'); const vars = appState.config; const headerHtml = vars.station ? (() => { // Station mode - Daily Climatology for Single Station const backLink = `extremes.php?network=${vars.network}&tbl=${vars.tbl}&month=${vars.month}&day=${vars.day}&label=${appState.labelAttribute}${appState.yearFilter ? `&year=${appState.yearFilter}` : ''}`; return `

🏛️ Daily Climatology for Single Station

Station: ${vars.station}

Showing records for all days of the year at this station

`; })() : (() => { // Date mode - Single Date Climatology for State const date = new Date(2000, vars.month - 1, vars.day); const dateStr = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric' }); const networkName = getNetworkDisplayName(vars.network); return `

🌡️ Single Date Climatology for State

${dateStr} - ${networkName}

Showing records for all stations on this date. Click any station ID to see its full year of data.

`; })(); headerSection.innerHTML = headerHtml; }; /** * Get display name for network */ function getNetworkDisplayName(network) { const networkNames = { 'IACLIMATE': 'Iowa', 'ILCLIMATE': 'Illinois', 'INCLIMATE': 'Indiana', 'KSCLIMATE': 'Kansas', 'KYCLIMATE': 'Kentucky', 'MICLIMATE': 'Michigan', 'MNCLIMATE': 'Minnesota', 'MOCLIMATE': 'Missouri', 'NECLIMATE': 'Nebraska', 'NDCLIMATE': 'North Dakota', 'OHCLIMATE': 'Ohio', 'SDCLIMATE': 'South Dakota', 'WICLIMATE': 'Wisconsin' }; return networkNames[network] || network; }; /** * Initialize Tabulator table */ function initializeTable() { const columns = getTableColumns(); appState.table = new Tabulator("#data-table", { data: prepareTableData(), // Use prepared data instead of raw data layout: "fitDataTable", responsiveLayout: "hide", pagination: "local", paginationSize: 10, // Show 10 rows by default paginationSizeSelector: [10, 25, 50, 100], height: "70vh", // Set table height to 70% of viewport height movableColumns: true, resizableColumns: true, tooltips: true, columnHeaderSortMulti: false, scrollHorizontal: true, // Enable horizontal scrolling freezeFirstColumn: true, // Freeze the first column headerSort: true, // Make headers sticky columns, placeholder: "No climatology data available for the selected criteria", initialSort: [ {column: appState.isStationView ? "date_link" : "station_link", dir: "asc"} ] }); // Add export buttons after table is created addExportButtons(); } /** * Get column definitions for Tabulator based on view type */ function getTableColumns() { const firstColumnTitle = appState.isStationView ? 'Date' : 'Station'; const firstColumnField = appState.isStationView ? 'date_link' : 'station_link'; return [ { title: firstColumnTitle, field: firstColumnField, formatter: "html", width: appState.isStationView ? 120 : 200, // Wider for station names headerSort: true, cssClass: "station-col", sorter(a, b, aRow, bRow) { // Use proper sorting for dates in station view if (appState.isStationView) { const aSort = aRow.getData().date_sort || 0; const bSort = bRow.getData().date_sort || 0; return aSort - bSort; } // For station names, use the station id const aText = aRow.getData().station || ''; const bText = bRow.getData().station || ''; return aText.localeCompare(bText); } }, { title: "Years", field: "years", width: 60, headerSort: true, formatter(cell) { return cell.getValue() || ''; } }, // High Temperature column group { title: "Avg High °F", field: "avg_high", width: 90, formatter(cell) { return formatNumber(cell.getValue(), 1); }, cssClass: "temp-group", sorter: "number" }, { title: "Max High °F", field: "max_high", width: 90, formatter(cell) { return cell.getValue() || ''; }, cssClass: "temp-group", sorter: "number" }, { title: "Max High Year", field: "max_high_years", width: 100, formatter(cell) { return formatYears(cell.getValue()); }, cssClass: "temp-group", sorter(a, b) { // Custom sorter for year arrays const aStr = Array.isArray(a) ? a.join('') : String(a || ''); const bStr = Array.isArray(b) ? b.join('') : String(b || ''); return aStr.localeCompare(bStr); } }, { title: "Min High °F", field: "min_high", width: 90, formatter(cell) { return cell.getValue() || ''; }, cssClass: "temp-group", sorter: "number" }, { title: "Min High Year", field: "min_high_years", width: 100, formatter(cell) { return formatYears(cell.getValue()); }, cssClass: "temp-group", sorter(a, b) { const aStr = Array.isArray(a) ? a.join('') : String(a || ''); const bStr = Array.isArray(b) ? b.join('') : String(b || ''); return aStr.localeCompare(bStr); } }, // Low Temperature column group { title: "Avg Low °F", field: "avg_low", width: 90, formatter(cell) { return formatNumber(cell.getValue(), 1); }, cssClass: "temp-group", sorter: "number" }, { title: "Max Low °F", field: "max_low", width: 90, formatter(cell) { return cell.getValue() || ''; }, cssClass: "temp-group", sorter: "number" }, { title: "Max Low Year", field: "max_low_years", width: 100, formatter(cell) { return formatYears(cell.getValue()); }, cssClass: "temp-group", sorter(a, b) { const aStr = Array.isArray(a) ? a.join('') : String(a || ''); const bStr = Array.isArray(b) ? b.join('') : String(b || ''); return aStr.localeCompare(bStr); } }, { title: "Min Low °F", field: "min_low", width: 90, formatter(cell) { return cell.getValue() || ''; }, cssClass: "temp-group", sorter: "number" }, { title: "Min Low Year", field: "min_low_years", width: 100, formatter(cell) { return formatYears(cell.getValue()); }, cssClass: "temp-group", sorter(a, b) { const aStr = Array.isArray(a) ? a.join('') : String(a || ''); const bStr = Array.isArray(b) ? b.join('') : String(b || ''); return aStr.localeCompare(bStr); } }, // Precipitation column group { title: "Avg Precip in", field: "avg_precip", width: 100, formatter(cell) { return formatNumber(cell.getValue(), 2); }, cssClass: "precip-group", sorter: "number" }, { title: "Max Precip in", field: "max_precip", width: 100, formatter(cell) { return formatNumber(cell.getValue(), 2); }, cssClass: "precip-group", sorter: "number" }, { title: "Max Precip Year", field: "max_precip_years", width: 110, formatter(cell) { return formatYears(cell.getValue()); }, cssClass: "precip-group", sorter(a, b) { const aStr = Array.isArray(a) ? a.join('') : String(a || ''); const bStr = Array.isArray(b) ? b.join('') : String(b || ''); return aStr.localeCompare(bStr); } } ]; } /** * Update table with new data */ function updateTable() { if (!appState.table) { initializeTable(); return; } // Prepare data for Tabulator const tableData = prepareTableData(); appState.table.setData(tableData); // Ensure export buttons are present addExportButtons(); } /** * Prepare data for Tabulator display */ function prepareTableData() { if (!appState.data || appState.data.length === 0) { return []; } const vars = appState.config; return appState.data.map((row) => { const linkData = appState.isStationView ? (() => { // Station view - link to date const date = new Date(2000, row.month - 1, row.day); const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); let link = `extremes.php?day=${row.day}&month=${row.month}&network=${vars.network}&tbl=${vars.tbl}`; if (appState.labelAttribute) { link += `&label=${appState.labelAttribute}`; } if (appState.yearFilter) { link += `&year=${appState.yearFilter}`; } return { linkCell: `${dateStr}`, linkField: 'date_link' }; })() : (() => { // Day view - link to station with name and ID let link = `extremes.php?station=${row.station}&network=${vars.network}&tbl=${vars.tbl}`; if (appState.labelAttribute) { link += `&label=${appState.labelAttribute}`; } if (appState.yearFilter) { link += `&year=${appState.yearFilter}`; } // Use station name if available, otherwise fall back to station ID const displayText = row.name ? `${row.name} (${row.station})` : row.station; return { linkCell: `${displayText}`, linkField: 'station_link' }; })(); // Create a new object with the link field const tableRow = { ...row }; tableRow[linkData.linkField] = linkData.linkCell; // Add sortable date field for station view (month*100 + day gives proper numeric sort) if (appState.isStationView) { tableRow.date_sort = row.month * 100 + row.day; } return tableRow; }); } /** * Format numeric values with specified decimal places */ function formatNumber(value, decimals) { if (value === null || value === undefined || value === '') {return '';} return parseFloat(value).toFixed(decimals); }; /** * Format year arrays as comma-separated strings */ function formatYears(years) { if (!years || !Array.isArray(years)) {return '';} return years.join(', '); }; /** * Show/hide loading indicator */ function showLoading(show) { const loadingIndicator = document.getElementById('loading-indicator'); const contentArea = document.getElementById('content-area'); if (loadingIndicator) { loadingIndicator.style.display = show ? 'block' : 'none'; } if (contentArea) { contentArea.style.display = show ? 'none' : 'block'; } }; /** * Show API information */ function showApiInfo() { const apiInfo = document.getElementById('api-info'); const apiUrl = document.getElementById('api-url'); if (apiInfo && apiUrl) { if (appState.currentApiUrl) { apiUrl.textContent = appState.currentApiUrl; apiInfo.style.display = 'block'; } } }; /** * Show error message */ function showError(message) { const contentArea = document.getElementById('content-area'); const errorDiv = document.createElement('div'); errorDiv.className = 'error-message'; errorDiv.innerHTML = `
Error Loading Data

${message}

Please try again or contact support if the problem persists.

`; if (contentArea) { contentArea.insertBefore(errorDiv, contentArea.firstChild); } }; /** * Attach event listeners for enhanced interactions */ function attachEventListeners() { // Helper to add change listeners to form elements function addFormChangeListeners(form) { const selectors = ['select[name="network"]', 'select[name="month"]', 'select[name="day"]', 'select[name="tbl"]']; selectors.forEach((selector) => { const el = form.querySelector(selector); if (el) { el.addEventListener('change', handleFormChange); } }); } // Handle form changes dynamically (don't submit, just update data) const form = document.getElementById('controls-form'); const submitBtn = document.getElementById('form-submit-btn'); const dynamicIndicator = document.getElementById('dynamic-indicator'); if (form && !appState.isStationView) { // Update submit button text for dynamic mode if (submitBtn) { submitBtn.value = 'Update View'; submitBtn.style.backgroundColor = '#0d6efd'; submitBtn.style.color = 'white'; submitBtn.style.border = '1px solid #0d6efd'; } // Show dynamic indicator if (dynamicIndicator) { dynamicIndicator.style.display = 'block'; } // Prevent form submission form.addEventListener('submit', (e) => { e.preventDefault(); handleFormChange(); return false; }); // Add change listeners to all form elements addFormChangeListeners(form); } // Handle browser back/forward navigation window.addEventListener('popstate', () => { // Reload the page to handle URL parameter changes window.location.reload(); }); // Handle label attribute changes for map const labelSelect = document.getElementById('label-attribute'); if (labelSelect && !appState.isStationView) { // Set initial value labelSelect.value = appState.labelAttribute; labelSelect.addEventListener('change', function() { appState.labelAttribute = this.value; appState.config.labelAttribute = this.value; // Update year filter visibility based on new attribute updateYearFilterVisibility(); // Repopulate year filter with years relevant to the new attribute if (appState.allFeatures && !['avg_high', 'avg_low', 'avg_precip', 'station', 'years'].includes(this.value)) { populateYearFilter(appState.allFeatures); } // Update URL to persist the selection updateUrl(); // Update map updateMapLabels(); // Re-apply year filter since filtering logic depends on selected attribute if (appState.yearFilter) { applyYearFilter(); } }); } // Handle year filter changes for map const yearFilter = document.getElementById('year-filter'); if (yearFilter && !appState.isStationView) { // Set initial value from URL parameter if (appState.yearFilter) { yearFilter.value = appState.yearFilter; } // Set initial visibility based on current attribute updateYearFilterVisibility(); yearFilter.addEventListener('change', function() { appState.yearFilter = this.value || null; appState.config.yearFilter = this.value || null; // Update URL to persist the selection updateUrl(); // Apply filter applyYearFilter(); }); } }; /** * Initialize OpenLayers map */ function initializeMap() { // Create vector source for station data appState.vectorSource = new ol.source.Vector(); // Create popup overlay const container = document.getElementById('popup'); const closer = document.getElementById('popup-closer'); appState.popup = new ol.Overlay({ element: container, autoPan: { animation: { duration: 250 } } }); // Close popup when X is clicked closer.onclick = function() { appState.popup.setPosition(); closer.blur(); return false; }; // Initialize map appState.map = new ol.Map({ target: 'map-container', layers: [ // Base layers group new ol.layer.Group({ title: 'Base Maps', layers: [ new ol.layer.Tile({ title: 'OpenStreetMap', type: 'base', visible: true, source: new ol.source.OSM() }), new ol.layer.Tile({ title: 'Satellite', type: 'base', visible: false, source: new ol.source.XYZ({ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attributions: 'Tiles © Esri' }) }) ] }), // Vector layer for stations new ol.layer.Vector({ title: 'Climate Stations', source: appState.vectorSource, style: getStationStyle }) ], overlays: [appState.popup], view: new ol.View({ center: ol.proj.fromLonLat([-93.5, 42.0]), // Default to Iowa zoom: 7 }) }); // Add layer switcher control const layerSwitcher = new ol.control.LayerSwitcher({ tipLabel: 'Toggle layer visibility', groupSelectStyle: 'children' }); appState.map.addControl(layerSwitcher); // Add click handler for station popups appState.map.on('singleclick', (evt) => { const clickedFeature = appState.map.forEachFeatureAtPixel(evt.pixel, (feature) => feature); if (clickedFeature) { showStationPopup(clickedFeature, evt.coordinate); } else { appState.popup.setPosition(); } }); // Change cursor when hovering over stations appState.map.on('pointermove', (evt) => { if (evt.dragging) {return;} const pixel = appState.map.getEventPixel(evt.originalEvent); const hit = appState.map.hasFeatureAtPixel(pixel); const target = appState.map.getTarget(); if (target?.style) { target.style.cursor = hit ? 'pointer' : ''; } }); // Add stations to map if data is available if (appState.data) { addStationsToMap(); } }; /** * Add station data to map as features */ function addStationsToMap() { if (!appState.vectorSource || appState.isStationView) {return;} appState.vectorSource.clear(); // Get the original GeoJSON features (we need to fetch again to get coordinates) // Since we already processed the data, we need the original GeoJSON fetchGeoJSONForMap(); } /** * Fetch GeoJSON data specifically for map display */ function fetchGeoJSONForMap() { const vars = appState.config; const syear = getStartYear(vars.tbl); const eyear = getEndYear(vars.tbl); const apiUrl = `/geojson/climodat_dayclimo.py?network=${vars.network}&month=${vars.month}&day=${vars.day}&syear=${syear}&eyear=${eyear}`; fetch(apiUrl) .then((response) => { return response.json(); }) .then((geoJsonData) => { addGeoJSONToMap(geoJsonData); }) .catch(() => { // Silently handle error - map will remain empty }); } /** * Add GeoJSON features to the map */ function addGeoJSONToMap(geoJsonData) { const format = new ol.format.GeoJSON(); const features = format.readFeatures(geoJsonData, { featureProjection: 'EPSG:3857' // Web Mercator for display }); // Store all features for filtering appState.allFeatures = features; // Apply current filter const filteredFeatures = filterFeaturesByYear(features); appState.vectorSource.addFeatures(filteredFeatures); // Populate year filter dropdown populateYearFilter(features); // Calculate initial color ranges and update legend appState.colorRanges = calculateColorRanges(); updateLegend(); // Fit map to show all stations if (filteredFeatures.length > 0) { const extent = appState.vectorSource.getExtent(); appState.map.getView().fit(extent, { padding: [20, 20, 20, 20], maxZoom: 10 }); } } /** * Get style for station features based on selected attribute */ function getStationStyle(feature) { const props = feature.getProperties(); const value = props[appState.labelAttribute]; const backgroundColor = getColorForValue(value); // Refactored for complexity: split getLabelText into smaller helpers (Rule: complexity, refactoring) function isNullOrUndefined(val) { return val === null || val === undefined; } function formatStationOrYears(val) { return String(val); } function formatPrecip(val) { return typeof val === 'number' ? `${val.toFixed(2)}"` : String(val); } function formatHighLow(val) { return typeof val === 'number' ? `${Math.round(val)}°` : String(val); } function formatArray(val) { return val.join(','); } function formatNumber1(val) { return val.toFixed(1); } function getLabelText(val, attr) { if (isNullOrUndefined(val)) {return '';} if (attr === 'station' || attr === 'years') { return formatStationOrYears(val); } if (attr.includes('precip')) { return formatPrecip(val); } if (attr.includes('high') || attr.includes('low')) { return formatHighLow(val); } if (Array.isArray(val)) { return formatArray(val); } if (typeof val === 'number') { return formatNumber1(val); } return String(val); } const labelText = getLabelText(value, appState.labelAttribute); return new ol.style.Style({ text: new ol.style.Text({ text: labelText, font: 'bold 12px Arial', fill: new ol.style.Fill({ color: '#333333' }), stroke: new ol.style.Stroke({ color: '#ffffff', width: 2 }), backgroundFill: new ol.style.Fill({ color: backgroundColor }), backgroundStroke: new ol.style.Stroke({ color: '#ffffff', width: 1 }), padding: [3, 6, 3, 6] }) }); }; /** * Show popup with station information /** * Helper to generate a table row with optional highlight if year matches * Exported for use in other functions. */ window.popupRow = function popupRow(label, value, yearsArr, yearFilter, valueSuffix = '', formatYearsFn = null) { let highlight = ''; if (yearFilter) { if (yearsArr?.includes?.(parseInt(yearFilter))) { highlight = ' style="background: #fff3cd; font-weight: bold;"'; } } let yearsStr = ''; if (yearsArr) { if (formatYearsFn) { yearsStr = formatYearsFn(yearsArr); } else { yearsStr = yearsArr; } } let suffixYears = ''; if (yearsArr) { suffixYears = ` (${yearsStr})`; } return `${label}:${value}${valueSuffix}${suffixYears}`; }; function showStationPopup(feature, coordinate) { const props = feature.getProperties(); const content = document.getElementById('popup-content'); // Use station name if available, otherwise fall back to station ID const stationDisplayName = props.name ? `${props.name} (${props.station})` : props.station; let popupHtml = `
${stationDisplayName}
`; // If year filter is active, highlight records from that year if (appState.yearFilter) { const filterYear = parseInt(appState.yearFilter); popupHtml += `
Showing records from ${filterYear}
`; } popupHtml += ''; popupHtml += ``; popupHtml += ``; // Use helper for rows with year highlighting popupHtml += window.popupRow('Max High', props.max_high || 'N/A', props.max_high_years, appState.yearFilter, '°F', formatYears); popupHtml += window.popupRow('Min High', props.min_high || 'N/A', props.min_high_years, appState.yearFilter, '°F', formatYears); popupHtml += ``; popupHtml += window.popupRow('Max Low', props.max_low || 'N/A', props.max_low_years, appState.yearFilter, '°F', formatYears); popupHtml += window.popupRow('Min Low', props.min_low || 'N/A', props.min_low_years, appState.yearFilter, '°F', formatYears); popupHtml += ``; popupHtml += window.popupRow('Max Precip', formatNumber(props.max_precip, 2), props.max_precip_years, appState.yearFilter, '"', formatYears); popupHtml += `

View station details →

`; content.innerHTML = popupHtml; appState.popup.setPosition(coordinate); } /** * Update map labels when attribute selection changes */ function updateMapLabels() { if (appState.vectorSource) { // Recalculate color ranges for new attribute appState.colorRanges = calculateColorRanges(); // Update legend updateLegend(); // Force redraw of all features by changing the style const features = appState.vectorSource.getFeatures(); features.forEach((feature) => { feature.changed(); }); } } /** * Update the legend based on current attribute and data */ function updateLegend() { const legendElement = document.querySelector('.map-legend'); if (!legendElement) { return; } let legendHtml = ''; if (!appState.colorRanges) { // No color ranges (e.g., station names) - show info message instead legendElement.style.display = 'block'; legendElement.innerHTML = `
${getAttributeLabel(appState.labelAttribute)} (no color coding)
`; return; } // Show legend with current attribute info legendElement.style.display = 'block'; const attributeLabel = getAttributeLabel(appState.labelAttribute); legendHtml = `
${attributeLabel} ranges:
`; appState.colorRanges.ranges.forEach((range) => { legendHtml += `
${range.label}${appState.colorRanges.units}
`; }); legendElement.innerHTML = legendHtml; } /** * Get human-readable label for attribute */ function getAttributeLabel(attribute) { const labels = { 'station': 'Station ID', 'avg_high': 'Average High Temperature', 'max_high': 'Maximum High Temperature', 'min_high': 'Minimum High Temperature', 'avg_low': 'Average Low Temperature', 'max_low': 'Maximum Low Temperature', 'min_low': 'Minimum Low Temperature', 'avg_precip': 'Average Precipitation', 'max_precip': 'Maximum Precipitation', 'years': 'Years of Data' }; return labels[attribute] || attribute; } /** * Calculate color ranges for the currently selected attribute */ function calculateColorRanges() { if (!appState.data || appState.data.length === 0) {return null;} // Skip color calculation for non-numeric attributes if (appState.labelAttribute === 'station') {return null;} // Get all numeric values for the selected attribute const values = appState.data .map((item) => item[appState.labelAttribute]) .filter((val) => typeof val === 'number' && !isNaN(val)) .sort((a, b) => a - b); if (values.length === 0) {return null;} const min = values[0]; const max = values[values.length - 1]; const range = max - min; if (range === 0) {return null;} // For temperature data, use appropriate decimal places const isTemp = appState.labelAttribute.includes('high') || appState.labelAttribute.includes('low'); const isPrecip = appState.labelAttribute.includes('precip'); const decimals = isPrecip ? 2 : (isTemp ? 0 : 1); // Create 5 equal ranges const step = range / 5; return { min, max, ranges: [ { min, max: min + step, color: '#3388ff', label: `${min.toFixed(decimals)} - ${(min + step).toFixed(decimals)}` }, { min: min + step, max: min + 2 * step, color: '#44bb44', label: `${(min + step).toFixed(decimals)} - ${(min + 2 * step).toFixed(decimals)}` }, { min: min + 2 * step, max: min + 3 * step, color: '#ffbb44', label: `${(min + 2 * step).toFixed(decimals)} - ${(min + 3 * step).toFixed(decimals)}` }, { min: min + 3 * step, max: min + 4 * step, color: '#ff8844', label: `${(min + 3 * step).toFixed(decimals)} - ${(min + 4 * step).toFixed(decimals)}` }, { min: min + 4 * step, max, color: '#ff4444', label: `${(min + 4 * step).toFixed(decimals)} - ${max.toFixed(decimals)}` } ], units: getAttributeUnits(appState.labelAttribute) }; } /** * Get appropriate units for display based on attribute type */ function getAttributeUnits(attribute) { if (attribute.includes('precip')) {return '"';} if (attribute.includes('high') || attribute.includes('low')) {return '°F';} return ''; } /** * Get color for a value based on current color ranges */ function getColorForValue(value) { if (!appState.colorRanges || typeof value !== 'number') { return '#cccccc'; // Default gray for non-numeric or missing data } for (let i = 0; i < appState.colorRanges.ranges.length; i++) { const range = appState.colorRanges.ranges[i]; if (i === appState.colorRanges.ranges.length - 1) { // Last range includes max value if (value >= range.min && value <= range.max) { return range.color; } } else { if (value >= range.min && value < range.max) { return range.color; } } } return '#cccccc'; // Default } /** * Handle form changes - update URL and reload data dynamically */ function handleFormChange() { // Get current form values const form = document.getElementById('controls-form'); const networkSelect = form.querySelector('select[name="network"]'); const monthSelect = form.querySelector('select[name="month"]'); const daySelect = form.querySelector('select[name="day"]'); const tblSelect = form.querySelector('select[name="tbl"]'); // Build new URL parameters const params = new URLSearchParams(window.location.search); if (networkSelect) {params.set('network', networkSelect.value);} if (monthSelect) {params.set('month', monthSelect.value);} if (daySelect) {params.set('day', daySelect.value);} if (tblSelect) {params.set('tbl', tblSelect.value);} // Keep existing sort parameters if (appState.config.sortcol) {params.set('sortcol', appState.config.sortcol);} if (appState.config.sortdir) {params.set('sortdir', appState.config.sortdir);} // Keep current label attribute setting if (appState.labelAttribute) {params.set('label', appState.labelAttribute);} // Clear year filter when switching to different dataset (network/date change) // The year filter is specific to a particular dataset combination appState.yearFilter = null; appState.config.yearFilter = null; // Make sure year parameter is not included in the new URL params.delete('year'); // Update URL without page reload const newUrl = `${window.location.pathname}?${params.toString()}`; window.history.pushState({}, '', newUrl); // Update app config appState.config = getConfig(); // Show loading and reload data showLoading(true); // Clear any existing error messages const existingErrors = document.querySelectorAll('.error-message'); existingErrors.forEach((error) => { error.remove(); }); fetchData() .then(() => { renderHeader(); updateTable(); showApiInfo(); if (!appState.isStationView) { // Clear and reload map data if (appState.vectorSource) { appState.vectorSource.clear(); } addStationsToMap(); // Reset year filter UI since we have a new dataset const yearSelect = document.getElementById('year-filter'); if (yearSelect) { yearSelect.value = ''; } // Update year filter visibility for current attribute updateYearFilterVisibility(); } }) .catch((error) => { showError(`Failed to load climatology data: ${error.message}`); }) .finally(() => { showLoading(false); }); } /** * Update URL with current application state */ function updateUrl() { const params = new URLSearchParams(); // Add all current config values to ensure they're preserved params.set('network', appState.config.network); params.set('month', appState.config.month); params.set('day', appState.config.day); params.set('tbl', appState.config.tbl); // Only add non-default values to keep URLs clean if (appState.config.sortcol && appState.config.sortcol !== 'station') { params.set('sortcol', appState.config.sortcol); } if (appState.config.sortdir && appState.config.sortdir !== 'ASC') { params.set('sortdir', appState.config.sortdir); } if (appState.labelAttribute && appState.labelAttribute !== 'avg_high') { params.set('label', appState.labelAttribute); } if (appState.yearFilter) { params.set('year', appState.yearFilter); } const newUrl = `${window.location.pathname}?${params.toString()}`; window.history.pushState({}, '', newUrl); } /** * Populate the year filter dropdown with available years */ function populateYearFilter(features) { const yearSelect = document.getElementById('year-filter'); if (!yearSelect) {return;} // Collect years based on the currently selected attribute const allYears = new Set(); // Determine which year field to use based on current label attribute let yearField = null; switch (appState.labelAttribute) { case 'max_high': yearField = 'max_high_years'; break; case 'min_high': yearField = 'min_high_years'; break; case 'max_low': yearField = 'max_low_years'; break; case 'min_low': yearField = 'min_low_years'; break; case 'max_precip': yearField = 'max_precip_years'; break; default: // For non-record attributes, collect from all year arrays yearField = null; } features.forEach((feature) => { const props = feature.getProperties(); if (yearField) { // Collect years from specific field only const years = props[yearField]; if (Array.isArray(years)) { years.forEach((year) => { allYears.add(year); }); } } else { // Collect from all year arrays (for avg attributes) const yearArrays = [ 'max_high_years', 'min_high_years', 'max_low_years', 'min_low_years', 'max_precip_years' ]; yearArrays.forEach((yearArrayField) => { const years = props[yearArrayField]; if (Array.isArray(years)) { years.forEach((year) => { allYears.add(year); }); } }); } }); // Sort years in descending order const sortedYears = Array.from(allYears).sort((a, b) => b - a); // Clear existing options except "All Years" yearSelect.innerHTML = ''; // Add year options sortedYears.forEach((year) => { const option = document.createElement('option'); option.value = year; option.textContent = year; if (appState.yearFilter && parseInt(appState.yearFilter) === year) { option.selected = true; } yearSelect.appendChild(option); }); } /** * Filter features by selected year */ function filterFeaturesByYear(features) { if (!appState.yearFilter) { return features; // No filter applied } // Determine which year field to check based on the currently selected attribute let yearField = null; switch (appState.labelAttribute) { case 'max_high': yearField = 'max_high_years'; break; case 'min_high': yearField = 'min_high_years'; break; case 'max_low': yearField = 'max_low_years'; break; case 'min_low': yearField = 'min_low_years'; break; case 'max_precip': yearField = 'max_precip_years'; break; default: // For non-record attributes (avg_high, avg_low, avg_precip, station, years), // check if ANY record was set in the selected year return features.filter((feature) => { const props = feature.getProperties(); const yearArrays = [ 'max_high_years', 'min_high_years', 'max_low_years', 'min_low_years', 'max_precip_years' ]; for (let i = 0; i < yearArrays.length; i++) { const years = props[yearArrays[i]]; if (years?.includes?.(parseInt(appState.yearFilter))) { return true; } } return false; }); } // Filter by specific record type return features.filter((feature) => { const props = feature.getProperties(); const years = props[yearField]; return Boolean(years?.includes?.(parseInt(appState.yearFilter))); }); } /** * Apply year filter to the map */ function applyYearFilter() { if (!appState.allFeatures || !appState.vectorSource) {return;} // Clear current features first appState.vectorSource.clear(); // Filter features based on selected year const filteredFeatures = filterFeaturesByYear(appState.allFeatures); // Add filtered features to the map (even if it's an empty array) if (filteredFeatures.length > 0) { appState.vectorSource.addFeatures(filteredFeatures); } // Note: We don't zoom/fit the map view when filtering - this preserves // the user's current view and spatial context // Note: Color ranges and legend are NOT updated here - they should remain // consistent based on the full dataset, not the filtered subset } /** * Update year filter visibility based on selected attribute */ function updateYearFilterVisibility() { const yearFilter = document.querySelector('#year-filter'); if (!yearFilter) { return; } const yearFilterRow = yearFilter.closest('.control-row'); if (!yearFilterRow) { return; } // Hide year filter for average attributes since they don't have specific record years const isAverageAttribute = ['avg_high', 'avg_low', 'avg_precip', 'station', 'years'].includes(appState.labelAttribute); if (isAverageAttribute) { yearFilterRow.style.display = 'none'; // Clear any active year filter when hiding if (appState.yearFilter) { appState.yearFilter = null; appState.config.yearFilter = null; const yearSelect = document.getElementById('year-filter'); if (yearSelect) { yearSelect.value = ''; } // Update URL to remove year parameter updateUrl(); // Apply the cleared filter (show all features) if (appState.allFeatures) { applyYearFilter(); } } } else { yearFilterRow.style.display = 'flex'; } } /** * Add export buttons for CSV and Excel download */ function addExportButtons() { // Create export buttons container const tableContainer = document.getElementById('data-table'); let exportContainer = document.getElementById('table-export-buttons'); if (!exportContainer) { exportContainer = document.createElement('div'); exportContainer.id = 'table-export-buttons'; exportContainer.className = 'mb-2 d-flex gap-2'; exportContainer.innerHTML = `${''} `; // Insert before the table tableContainer.parentNode.insertBefore(exportContainer, tableContainer); } // Add event listeners for export buttons document.getElementById('download-csv').addEventListener('click', () => { const filename = generateExportFilename('csv'); appState.table.download("csv", filename); }); document.getElementById('download-xlsx').addEventListener('click', () => { const filename = generateExportFilename('xlsx'); appState.table.download("xlsx", filename, {sheetName: "Climate Data"}); }); } /** * Generate appropriate filename for exports */ function generateExportFilename(extension) { const vars = appState.config; const filename = appState.isStationView ? `climate_station_${vars.station}` : (() => { const monthStr = vars.month.toString().padStart(2, '0'); const dayStr = vars.day.toString().padStart(2, '0'); const networkStr = vars.network.toLowerCase(); return `climate_${networkStr}_${monthStr}-${dayStr}`; })(); return `${filename}.${extension}`; } // Initialize the app when DOM is ready document.addEventListener('DOMContentLoaded', initializeApp); ================================================ FILE: htdocs/COOP/extremes.php ================================================ title = "NWS COOP Daily Climatology"; // Get URL parameters with defaults $tbl = substr(get_str404("tbl", "climate"), 0, 10); $month = get_int404("month", date("m")); $day = get_int404("day", date("d")); $sortcol = get_str404("sortcol", "station"); $network = substr(get_str404("network", "IACLIMATE"), 0, 9); $station = get_str404("station", null); $sortdir = get_str404("sortdir", "ASC"); // Build render variables array $render_vars = array( 'tbl' => $tbl, 'month' => $month, 'day' => $day, 'sortcol' => $sortcol, 'network' => $network, 'station' => $station, 'sortdir' => $sortdir ); // Create form selects $netselect = selectNetworkType("CLIMATE", $network); $mselect = monthSelect($month, "month"); $dselect = daySelect($day, "day"); $ar = array( "climate" => "All Available", "climate51" => "Since 1951", "climate71" => "1971-2000", "climate81" => "1981-2010" ); $tblselect = make_select("tbl", $tbl, $ar); $t->content = <<
Two App Modes Available:
🌡️ Single Date Climatology for State

Select a specific date to see records for all stations on that day. Use the form below to choose date and state.

🏛️ Daily Climatology for Single Station

Click any station ID in the table to see all daily records for that station throughout the year.

This table gives a listing of unofficial daily records for NWS COOP stations. You may click on a column to sort it. You can click on the station name to get all daily records for that station or click on the date to get all records for that date.

Download Daily Climatology

🌡️ Single Date Mode Controls
Change state or date to view different climatology data
{$netselect}
{$mselect} {$dselect}
{$tblselect}

EOM; $t->headextra = << EOM; $t->jsextra = << EOM; $t->render('full.phtml'); ================================================ FILE: htdocs/COOP/freezing.php ================================================ title = "Freezing Dates"; $nt = new NetworkTable($network); $cities = $nt->table; $nselect = selectNetworkType("CLIMATE", $network); $conn = iemdb("coop"); $query = "select station, valid, min_low, min_low_yr from climate WHERE valid > '2000-08-01' and min_low <= $2 and substr(station,0,3) = $1 ORDER by valid"; $stname = iem_pg_prepare($conn, $query); $rs = pg_execute($conn, $stname, array(substr($network, 0, 2), 32)); $query = "select station, valid, low from climate WHERE valid > '2000-08-01' and low <= $2 and substr(station,0,3) = $1 ORDER by valid"; $stname = iem_pg_prepare($conn, $query); $rs2 = pg_execute($conn, $stname, array(substr($network, 0, 2), 40)); $data = array(); while ($row = pg_fetch_assoc($rs)) { $st = $row["station"]; if (!isset($data[$st])) { $data[$st] = array( "min_low" => 100, "avglow32day" => null, "avglow28day" => null, "station" => $st); $data[$st]["low"] = $row["min_low"]; $data[$st]["lowyr"] = $row["min_low_yr"] . "-" . substr($row["valid"], 5, 6); } if (!isset($data[$st]["low28"])) { if (intval($row["min_low"]) < 29) { $data[$st]["low28"] = $row["min_low"]; $data[$st]["low28yr"] = $row["min_low_yr"] . "-" . substr($row["valid"], 5, 6); } } } while ($row = pg_fetch_assoc($rs2)) { $st = $row["station"]; if (!isset($data[$st]["avelow40day"])) { if (intval($row["low"]) < 41) { $data[$st]["avelow40day"] = substr($row["valid"], 5, 6); } } if (!isset($data[$st]["avelow32day"])) { if (intval($row["low"]) < 33) { $data[$st]["avelow32day"] = substr($row["valid"], 5, 6); } } if (!isset($data[$st]["avelow28day"])) { if (intval($row["low"]) < 28) { $data[$st]["avelow28day"] = substr($row["valid"], 5, 6); } } } $finalA = array(); $finalA = aSortBySecondIndex($data, $sortcol); $table = ""; foreach ($finalA as $key => $value) { if (!array_key_exists($key, $cities)) continue; $table .= "" . $cities[strtoupper($key)]["name"] . " " . $data[$key]["low"] . " " . $data[$key]["lowyr"] . " " . $data[$key]["low28"] . " " . $data[$key]["low28yr"] . " " . $data[$key]["avelow40day"] . " " . $data[$key]["avelow32day"] . " " . $data[$key]["avelow28day"] . " \n"; } $t->content = <<Freezing Dates

Using the NWS COOP data archive, significant dates relating to fall are extracted and presented on this page. The specific dates are the first occurance of that temperature and may have occured again in subsequent years.
The "Record Lows" columns show the first fall occurance of a low temperature. The "Average Lows" column shows when certain climatological thresholds are surpassed in the fall.

Select Network: {$nselect}
{$table}
COOP Site: Record Lows: Average Lows:
Temp <= 32°F Temp <= 28°F Below 40°F Below 32°F Below 28°F
Temp: Date: Temp: Date:
EOM; $t->render('single.phtml'); ================================================ FILE: htdocs/COOP/hpd.php ================================================ Please select a station and date.

"; if ($station) { $dbconn = iemdb('other'); $stname = iem_pg_prepare( $dbconn, "select * from hpd_alldata WHERE station = $1 and valid >= $2 " . "and valid < $3 ORDER by valid ASC" ); $valid = mktime(0, 0, 0, $month, $day, $year); $sts = date("Y-m-d 00:00", $valid); $ets = date("Y-m-d 23:59", $valid); $rs = pg_execute($dbconn, $stname, array($station, $sts, $ets)); $table = ''; while ($row = pg_fetch_assoc($rs)) { $table .= sprintf( "", $row["valid"], $row["precip"] ); } $table .= "
ValidPrecip
%s%s
"; } $t = new MyView(); $t->title = "COOP HPD FisherPorter Precip"; $sselect = networkSelect("IA_HPD", $station); $t->content = <<

The IEM maintains an archive of processed rain gauge data from the "Fisher Porter" equipment that is run at some NWS COOP locations in Iowa. There is considerable delay to the availability of this data from NCEI. Currently, a process runs on the 15th each month and downloads data for the previous 3rd, 6th, and 12th month to the current date.

Updated 3 Feb 2023: As it stands currently, I can not find this datasource available from NCEI. So there's no data in this archive since ~Feb 2021.

{$sselect}
{$yselect} {$mselect} {$dselect}
{$table}
EOM; $t->render('single.phtml'); ================================================ FILE: htdocs/COOP/index.css ================================================ div.tease { margin: 5px; padding: 5px; width: 400px; background: #e8cc84; float: left; } div.tease img { margin-right: 5px; float: left; } div.tease a { font-weight: bold; padding: 5px; } ================================================ FILE: htdocs/COOP/index.phtml ================================================ title = "NWS COOP Data"; $t->headextra = << EOM; $yr = date("Y"); $dict = array( array( "img" => "chart_line_f_t.png", "url" => "/plotting/coop/threshold_histogram_fe.phtml", "title" => "Winter Min Low Temp Frequencies", "desc" => "Histogram showing the number of years that a certain low temperature threshold is exceeded." ), array( "img" => "chart_line_h_t.png", "url" => "/plotting/coop/spread_fe.phtml", "title" => "Daily Temperature Spread", "desc" => "Histogram showing daily high/low temperatures." ), array( "img" => "data_table.png", "url" => "/COOP/periods.phtml", "title" => "Yearly Average Temperatures", "desc" => "For a date interval of your choice, get the yearly statewide average temperatures." ), array( "img" => "data_table.png", "url" => "/sites/hist.phtml?station=AMSI4&network=IA_COOP", "title" => "Observations by Month", "desc" => "View observations per station and per month basis." ), array( "img" => "data_table.png", "url" => "/COOP/freezing.php", "title" => "Fall Freezing Dates", "desc" => "Statistics of dates for significant first fall freezes." ), array( "img" => "data_table.png", "url" => "/COOP/snowd_duration.phtml", "title" => "Snow Depth Duration", "desc" => "For a given date, how long will the snow stick around?" ), array( "img" => "data_table.png", "url" => "/COOP/extremes.php", "title" => "Daily Climate in Tables", "desc" => "Tables of daily temperature and precipitation climatology." ), array( "img" => "thumb_map.png", "url" => "/GIS/apps/coop/index.php", "title" => "Daily Climate in Maps", "desc" => "Plots of daily extremes and averages with a GIS Ready! download of the data presented." ), array( "img" => "chart_line_t_d.png", "url" => "/plotting/coop/climate_fe.php", "title" => "Daily Average Temperatures", "desc" => "Dynamically produced chart of average daily temperatures." ), array( "img" => "chart_line_t_y.png", "url" => "/plotting/auto/?q=100", "title" => "Yearly Average Temperatures", "desc" => "Plot average daily temperatures for a year of your choice." ), array( "img" => "chart_line_d_a.png", "url" => "/plotting/auto/?q=107", "title" => "Accumulated Precipitation Probabilities", "desc" => "Chart of precip probabilities for a time period of your choice." ), array( "img" => "chart_line_t_d.png", "url" => "/plotting/auto/?q=99", "title" => "Yearly Departures from Average", "desc" => "Plot average temperatures versus what actually occured during one year." ), array( "img" => "chart_line_ac_d.png", "url" => "/plotting/auto/?q=108", "title" => "Accumulated Departures from Average", "desc" => "Plot an accumulated departure from average for rainfall and growing degree days for a time period of your choice!" ), array( "img" => "thumb_map.png", "url" => "/COOP/map/", "title" => "Map Daily Observations", "desc" => "Interactive map of daily observations." ), ); $content = ""; foreach ($dict as $k => $v) { // Card-like teaser; image is decorative (title + text already conveys info) $content .= <<

{$v["title"]}

{$v["desc"]}

EOM; } $t->content = <<

National Weather Service Cooperative Observer Program (COOP)

The COOP network is comprised of volunteer observers reporting once-daily high and low air temperature, liquid precipitation, snowfall and snow depth. Explore popular applications below.

IEM Value-Added Climodat Dataset

The IEM processes preliminary COOP reports, applies limited quality control, and estimates some missing data to produce the coherent Climodat dataset. Popular Climodat applications:

Data Applications

{$content}
EOM; $t->render('full.phtml'); ================================================ FILE: htdocs/COOP/map/index.css ================================================ /* Climap Application Styles */ .map { height: 70vh; width: 100%; border: 1px solid #dee2e6; border-radius: 0.375rem; position: relative; } .popover { width: 320px; max-width: 90vw; } .popover-body { font-size: 0.875rem; line-height: 1.5; } /* Font size controls styling */ .btn-group .btn.disabled { pointer-events: none; background-color: #e9ecef; border-color: #dee2e6; } /* Loading states */ .loading { opacity: 0.6; pointer-events: none; } .loading-spinner { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1000; } /* Responsive adjustments */ @media (max-width: 768px) { .map { height: 50vh; } .popover { width: 280px; } .btn-group .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; } } ================================================ FILE: htdocs/COOP/map/index.js ================================================ /* global ol */ let renderattr = 'high'; let vectorLayer = null; let map = null; let popup = null; let fontSize = 14; /** * Replace HTML special characters with their entity equivalents */ function escapeHTML(val) { return val .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Get current date string from date picker (YYYY-MM-DD format) * Avoids timezone issues by working directly with the string value */ function getCurrentDateString() { const datePicker = document.getElementById('datepicker'); return escapeHTML(datePicker.value); // Already in YYYY-MM-DD format } /** * Update URL parameters with current state */ function updateURL() { const dateStr = getCurrentDateString(); const url = new URL(window.location); url.searchParams.set('date', dateStr); url.searchParams.set('var', renderattr); // Add map position if (map) { const view = map.getView(); const center = ol.proj.toLonLat(view.getCenter()); url.searchParams.set('lon', center[0].toFixed(4)); url.searchParams.set('lat', center[1].toFixed(4)); url.searchParams.set('zoom', view.getZoom().toFixed(0)); } window.history.pushState({}, '', url); } /** * Update map with new render attribute */ function updateMap() { const selectElement = document.getElementById('renderattr'); renderattr = escapeHTML(selectElement.value); vectorLayer.setStyle(vectorLayer.getStyle()); updateURL(); } /** * Update map with new date */ function updateDate() { const dateStr = getCurrentDateString(); // Show loading state const mapElement = document.getElementById('map'); mapElement.classList.add('loading'); map.removeLayer(vectorLayer); vectorLayer = makeVectorLayer(dateStr); map.addLayer(vectorLayer); // Remove loading state after a brief delay setTimeout(() => { mapElement.classList.remove('loading'); }, 500); updateURL(); } /** * Style function for vector features */ const vectorStyleFunction = feature => { const value = feature.get(renderattr); const outlinecolor = '#000000'; if (value !== null && !isNaN(value)) { return [ new ol.style.Style({ fill: new ol.style.Fill({ color: 'rgba(255, 255, 255, 0.6)', }), text: new ol.style.Text({ font: `${fontSize}px Calibri,sans-serif`, text: value.toString(), fill: new ol.style.Fill({ color: '#FFFFFF', width: 1, }), stroke: new ol.style.Stroke({ color: outlinecolor, width: 3, }), }), }), ]; } return [ new ol.style.Style({ image: new ol.style.Circle({ fill: new ol.style.Fill({ color: 'rgba(255,255,255,0.4)', }), stroke: new ol.style.Stroke({ color: '#3399CC', width: 1.25, }), radius: 5, }), fill: new ol.style.Fill({ color: 'rgba(255,255,255,0.4)', }), stroke: new ol.style.Stroke({ color: '#3399CC', width: 1.25, }), }), ]; }; /** * Create vector layer for given date */ function makeVectorLayer(dt) { return new ol.layer.Vector({ source: new ol.source.Vector({ format: new ol.format.GeoJSON(), projection: ol.proj.get('EPSG:3857'), url: `/geojson/coopobs.py?valid=${dt}`, }), style: vectorStyleFunction, }); } /** * Extract feature data with defaults */ function getFeatureData(feature) { const get = (key, defaultValue = 'N/A') => feature.get(key) ?? defaultValue; return { station: feature.get('station'), network: feature.get('network'), name: get('name'), utcValid: get('utc_valid'), coopTmpf: get('coop_tmpf'), hour: get('hour'), high: get('high', 'M'), low: get('low', 'M'), precip: get('precip', 'M'), snow: get('snow', 'M'), snowd: get('snowd', 'M'), report: get('report') }; } /** * Create and show popover with feature information */ function showPopover(feature, coordinate) { const data = getFeatureData(feature); const content = `
NWSLI: ${data.station} Name: ${data.name} Reported At: ${data.utcValid} Report Local Hour: ${data.hour} Air Temp: ${data.coopTmpf} High: ${data.high} Low: ${data.low} Precip: ${data.precip} Snow: ${data.snow} Snow Depth: ${data.snowd} SHEF Report: ${data.report}
`; // Use the OpenLayers popup element directly const popupElement = document.getElementById('popup'); popupElement.innerHTML = `
${content}
`; // Position the popup using OpenLayers popup.setPosition(coordinate); } /** * Hide popover */ function hidePopover() { const popupElement = document.getElementById('popup'); popupElement.innerHTML = ''; popup.setPosition(null); } /** * Fetch and display CLI report */ async function fetchCLIReport(url) { const reportDiv = document.getElementById('clireport'); reportDiv.innerHTML = '
Loading CLI report...
'; try { const response = await fetch(url); if (!response.ok) {throw new Error('Network response was not ok');} const data = await response.text(); reportDiv.innerHTML = `
${data}
`; } catch (error) { reportDiv.innerHTML = `
Failed to fetch CLI report. ${error} Please try again.
`; } } /** * Set date picker to specific date string (YYYY-MM-DD format) * Avoids timezone issues by working directly with string values */ function setDatePickerValue(dateStr) { const datePicker = document.getElementById('datepicker'); datePicker.value = dateStr; } /** * Parse URL parameters and update interface * Handles migration from legacy hash-based URLs to modern URL parameters * Legacy format: #YYMMDD/variable -> Modern format: ?date=YYYY-MM-DD&var=variable */ function parseURLParameters() { const urlParams = new URLSearchParams(window.location.search); // Check for legacy hash parameters first and migrate them const tokens = window.location.href.split('#'); if (tokens.length === 2) { const hashTokens = tokens[1].split('/'); if (hashTokens.length === 2) { const tpart = escapeHTML(hashTokens[0]); const hashRenderattr = escapeHTML(hashTokens[1]); // Parse date from hash (YYMMDD format) and convert to YYYY-MM-DD if (tpart.length === 6) { const year = 2000 + parseInt(tpart.substring(0, 2)); const month = String(parseInt(tpart.substring(2, 4))).padStart(2, '0'); const day = String(parseInt(tpart.substring(4, 6))).padStart(2, '0'); const dateStr = `${year}-${month}-${day}`; // Redirect to new URL format const newUrl = new URL(window.location); newUrl.hash = ''; newUrl.searchParams.set('date', dateStr); newUrl.searchParams.set('var', hashRenderattr); window.location.replace(newUrl.toString()); return; } } } // Handle modern URL parameters const dateParam = urlParams.get('date'); const varParam = urlParams.get('var'); if (varParam) { renderattr = escapeHTML(varParam); const selectElement = document.getElementById('renderattr'); selectElement.value = renderattr; } if (dateParam) { // Validate date format (YYYY-MM-DD) and set directly to avoid timezone issues if (/^\d{4}-\d{2}-\d{2}$/.test(dateParam)) { setDatePickerValue(dateParam); updateDate(); } } } /** * Initialize the application */ document.addEventListener('DOMContentLoaded', () => { // Initialize renderattr from the form's selected value const selectElement = document.getElementById('renderattr'); renderattr = selectElement.value; // Set up date picker (PHP already sets the initial value) const datePicker = document.getElementById('datepicker'); // Set up date picker change handler datePicker.addEventListener('change', () => { updateDate(); }); // Initialize vector layer with current date picker value const currentDateStr = getCurrentDateString(); vectorLayer = makeVectorLayer(currentDateStr); // Get map position from URL or use defaults const urlParams = new URLSearchParams(window.location.search); const lon = parseFloat(urlParams.get('lon')) || -95.0; const lat = parseFloat(urlParams.get('lat')) || 42.0; const zoom = parseInt(urlParams.get('zoom')) || 3; const center = ol.proj.fromLonLat([lon, lat]); // Set up map map = new ol.Map({ target: 'map', layers: [ new ol.layer.Tile({ title: 'Global Imagery', source: new ol.source.XYZ({ attributions: 'Tiles © ArcGIS', url: 'https://server.arcgisonline.com/ArcGIS/rest/services/' + 'World_Imagery/MapServer/tile/{z}/{y}/{x}', }), }), new ol.layer.Tile({ title: 'State Boundaries', source: new ol.source.XYZ({ url: '/c/tile.py/1.0.0/usstates/{z}/{x}/{y}.png', }), }), vectorLayer, ], view: new ol.View({ projection: 'EPSG:3857', center, zoom, }), }); map.addControl(new ol.control.LayerSwitcher()); // Update URL when map is moved or zoomed map.on('moveend', () => { updateURL(); }); // Set up popup overlay const element = document.getElementById('popup'); popup = new ol.Overlay({ element, positioning: 'bottom-center', stopEvent: true, // Allow clicks on links inside the popup }); map.addOverlay(popup); // Handle map clicks map.on('click', evt => { const feature = map.forEachFeatureAtPixel(evt.pixel, feature2 => { return feature2; }); if (feature) { const geometry = feature.getGeometry(); const coord = geometry.getCoordinates(); showPopover(feature, coord); // Fetch CLI report const link = feature.get('link'); if (link) { fetchCLIReport(link); } } else { hidePopover(); } }); // Parse URL parameters if present parseURLParameters(); // Font size buttons document.getElementById('fplus').addEventListener('click', () => { fontSize += 2; vectorLayer.setStyle(vectorStyleFunction); }); document.getElementById('fminus').addEventListener('click', () => { fontSize -= 2; vectorLayer.setStyle(vectorStyleFunction); }); // Render attribute change handler document.getElementById('renderattr').addEventListener('change', () => { updateMap(); }); }); ================================================ FILE: htdocs/COOP/map/index.php ================================================ "High Temperature (°F)", "low" => "Low Temperature (°F)", "coop_tmpf" => "Observation Temperature (°F)", "precip" => "Precipitation (inch)", "snow" => "Snowfall (inch)", "snowd" => "Snow Depth (inch)", ]; // Validate variable parameter $valid_var = array_key_exists($var_param, $render_vars) ? $var_param : "high"; // Generate select element using helper function $render_select = make_select("renderattr", $valid_var, $render_vars, "", "form-select", FALSE, FALSE, TRUE, ["id" => "renderattr"]); $t = new MyView(); $t->title = "Map of Daily NWS COOP Reports"; $OL = '10.7.0'; $t->headextra = << EOM; $t->jsextra = << EOM; $t->content = <<