diff --git a/Makefile b/Makefile index bd33036..44f303d 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,10 @@ deploy: build rsync --archive --verbose --delete dist/ root@bert:/srv/www/maps.chandlerswift.com/ +.PHONY: deploy-remote +deploy-remote: build + rsync --archive --verbose --delete dist/ root@bert-jump:/srv/www/maps.chandlerswift.com/ + .PHONY: clean clean: rm -r dist diff --git a/layers/fcc/towers/README.md b/layers/fcc/towers/README.md new file mode 100644 index 0000000..6e292cb --- /dev/null +++ b/layers/fcc/towers/README.md @@ -0,0 +1 @@ +https://data.fcc.gov/download/pub/uls/complete/r_tower.zip diff --git a/layers/fcc/towers/layer.js b/layers/fcc/towers/layer.js new file mode 100644 index 0000000..04855c6 --- /dev/null +++ b/layers/fcc/towers/layer.js @@ -0,0 +1,24 @@ +import VectorLayer from 'ol/layer/Vector'; +import {Vector as VectorSource} from 'ol/source.js'; +import GeoJSON from 'ol/format/GeoJSON.js'; + +import {Style} from 'ol/style.js'; +import Icon from 'ol/style/Icon.js'; + +import data from './data.geojson?url'; // TODO: remove `?url`? +import pinURL from './pin.svg?url'; // TODO: remove `?url`? + +const vectorLayer = new VectorLayer({ + source: new VectorSource({ + url: data, + format: new GeoJSON, + }), + style: new Style({ + image: new Icon({ + anchor: [0.5, 1], + src: pinURL, + }), + }), +}); + +export default vectorLayer; diff --git a/layers/fcc/towers/pin.svg b/layers/fcc/towers/pin.svg new file mode 100644 index 0000000..ad2a79a --- /dev/null +++ b/layers/fcc/towers/pin.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/layers/fcc/towers/process.py b/layers/fcc/towers/process.py new file mode 100755 index 0000000..a91ae57 --- /dev/null +++ b/layers/fcc/towers/process.py @@ -0,0 +1,90 @@ +#!/usr/bin/env nix-shell +#! nix-shell -i python3 -p python3 + +import csv +import io +import json +import zipfile +import urllib.request + +fieldnames = { + "CO": ("Record Type", "Content Indicator", "File Number", "Registration Number", "Unique System Identifier", + "Coordinate Type", "Latitude Degrees", "Latitude Minutes", "Latitude Seconds", "Latitude Direction", + "Latitude_Total_Seconds", "Longitude Degrees", "Longitude Minutes", "Longitude Seconds", + "Longitude Direction", "Longitude Total Seconds", "Array Tower Position", "Array Total Tower"), + +} +filenames = ["CO", "EN", "HS", "RA", "RE", "SC"] +files = {} +resp = urllib.request.urlopen("https://data.fcc.gov/download/pub/uls/complete/r_tower.zip") +with zipfile.ZipFile(io.BytesIO(resp.read())) as z: + for filename in filenames: + with z.open(f"{filename}.dat", "r") as f, io.TextIOWrapper( # Avoids `_csv.Error: iterator should return strings, not bytes (the file should be opened in text mode)` + f, encoding="utf-8", newline="", errors="replace" + ) as text_f: + contents = [] + # csv expects text rows; TextIOWrapper decodes bytes from the zip member + reader = csv.DictReader(text_f, delimiter="|", fieldnames=fieldnames.get(filename)) + for row in reader: + contents.append(row) + files[filename] = contents + +# CO data example: +#{ +# 'Record Type': 'CO', +# 'Content Indicator': 'REG', +# 'File Number': 'A0016772', +# 'Registration Number': '1014003', +# 'Unique System Identifier': '99845', +# 'Coordinate Type': 'T', +# 'Latitude Degrees': '18', +# 'Latitude Minutes': '26', +# 'Latitude Seconds': '18.0', +# 'Latitude Direction': 'N', +# 'Latitude_Total_Seconds': '66378.0', +# 'Longitude Degrees': '66', +# 'Longitude Minutes': '29', +# 'Longitude Seconds': '53.0', +# 'Longitude Direction': 'W', +# 'Longitude Total Seconds': '239393.0', +# 'Array Tower Position': '', +# 'Array Total Tower': '' +# } + +towers = [] + +for w in files["CO"]: + if w['Longitude Degrees'] == '' or w['Latitude Degrees'] == '': + print(f"Skipping tower with missing coordinates: {w}") + continue + towers.append({ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + (float(w['Longitude Degrees']) + + float(w['Longitude Minutes']) / 60 + + float(w['Longitude Seconds']) / 3600) * + (-1 if w['Longitude Direction'] == 'W' else 1), + (float(w['Latitude Degrees']) + + float(w['Latitude Minutes']) / 60 + + float(w['Latitude Seconds']) / 3600) * + (1 if w['Latitude Direction'] == 'N' else -1), + ], # yes, [lon, lat] since it's [x, y] + }, + "properties": { + "File Number": w['File Number'], + "Registration Number": w['Registration Number'], + "Unique System Identifier": w['Unique System Identifier'], + }, + }) + +print(f"""{len(towers)} towers found""") + +geojson = { + "type": "FeatureCollection", + "features": towers, +} + +with open("data.geojson", "w") as f: + f.write(json.dumps(geojson)) diff --git a/layers/index.js b/layers/index.js index 4f4a422..e0c9727 100644 --- a/layers/index.js +++ b/layers/index.js @@ -23,6 +23,7 @@ import minnesotaAdventureTrails from './minnesota-adventure-trails/index.js'; import cropHistory from './crop-history/index.js'; import mnAmbulanceServiceAreas from './mn-ambulance-service-areas/layer.js'; import upsServiceAreas from './ups/index.js'; +import fccTowersLayer from './fcc/towers/layer.js'; const layerCategories = [ { // Base maps @@ -93,6 +94,10 @@ const layerCategories = [ layer: bikepackingLayer, }, mnAmbulanceServiceAreas, + { + name: "FCC Towers", + layer: fccTowersLayer, + } ] }, upsServiceAreas,