Skip to content

Commit 0b85d9f

Browse files
committed
new binary format, don't limit to peninsula
1 parent bb6b6b6 commit 0b85d9f

3 files changed

Lines changed: 198 additions & 88 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
features.bin
2+
priorities.json

index.html

Lines changed: 108 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<!DOCTYPE html>
22
<html lang="en">
3+
34
<head>
45
<meta charset="UTF-8">
56
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -61,73 +62,136 @@
6162
</style>
6263
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
6364
</head>
65+
6466
<body>
6567
<div class="header" id="priorities-header"></div>
6668
<div id="map"></div>
6769

6870
<script>
69-
// Initialize the map
7071
var map = L.map('map');
7172

7273
// Add a basemap (OpenStreetMap)
7374
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
7475
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
7576
}).addTo(map);
7677

77-
// Define a function to style the GeoJSON features
78-
function getStyle(feature) {
79-
const colors = {
80-
'1': '#017A74', // High priority: Dark Green
81-
'2': '#E66101', // Medium priority: Orange
82-
'3': '#6B3FA0' // Low priority: Purple
83-
};
84-
85-
return {
86-
color: colors[feature.properties.priority] || '#CCCCCC', // Default to grey
87-
weight: 6,
88-
opacity: 1
89-
};
90-
}
91-
92-
// Format the deadline to show weekday and time (e.g., Wed 3:30 PM)
9378
function formatDeadline(deadline) {
9479
const deadlineDate = new Date(deadline);
80+
const now = new Date();
81+
if (deadlineDate > now - (3 * 86400 * 1000)) {
82+
return deadlineDate.toLocaleString('en-US', {
83+
weekday: 'short',
84+
hour: 'numeric',
85+
minute: '2-digit'
86+
});
87+
}
88+
9589
return deadlineDate.toLocaleString('en-US', {
96-
weekday: 'short',
90+
month: 'short',
91+
day: 'numeric',
9792
hour: 'numeric',
9893
minute: '2-digit'
9994
});
10095
}
10196

102-
// Load GeoJSON data
103-
fetch('data.geojson')
104-
.then(response => response.json())
105-
.then(data => {
106-
// Add GeoJSON to the map
107-
var geojsonLayer = L.geoJSON(data, {
108-
style: getStyle,
109-
onEachFeature: function(feature, layer) {
110-
layer.bindPopup(`
111-
${feature.properties.title || 'Unknown'}<br>
112-
<strong>Priority:</strong> ${feature.properties.priority || 'Unknown'}<br>
113-
<strong>Deadline:</strong> ${feature.properties.deadline || 'Unknown'}<br>
114-
<strong>Timeline:</strong> ${feature.properties.timeline || 'Unknown'}
115-
`);
116-
}
117-
}).addTo(map);
118-
119-
// Auto-center map to the GeoJSON layer
120-
map.fitBounds(geojsonLayer.getBounds());
121-
})
122-
.catch(error => console.error('Error loading GeoJSON:', error));
97+
async function decodeFeatures(arrayBuffer) {
98+
const dataView = new DataView(arrayBuffer);
99+
let offset = 0;
100+
101+
const featureCount = dataView.getUint32(offset, true);
102+
offset += 4;
103+
104+
const baseLon = dataView.getFloat64(offset, true);
105+
offset += 8;
106+
const baseLat = dataView.getFloat64(offset, true);
107+
offset += 8;
108+
109+
const features = [];
110+
for (let i = 0; i < featureCount; i++) {
111+
const titleLength = dataView.getUint8(offset);
112+
offset += 1;
113+
const title = new TextDecoder().decode(new Uint8Array(arrayBuffer, offset, titleLength));
114+
offset += titleLength;
115+
116+
const priority = dataView.getUint8(offset);
117+
offset += 1;
118+
119+
const coordCount = dataView.getUint16(offset, true);
120+
offset += 2;
121+
122+
const coords = [];
123+
for (let j = 0; j < coordCount; j++) {
124+
// Read delta lat and lon
125+
const deltaLon = dataView.getInt32(offset, true) / 1000000;
126+
offset += 4;
127+
const deltaLat = dataView.getInt32(offset, true) / 1000000;
128+
offset += 4;
129+
130+
// Leaflet expects [lat, lon]
131+
coords.push([baseLat + deltaLat, baseLon + deltaLon]);
132+
}
133+
134+
features.push({ title, priority, coords });
135+
}
136+
137+
return features;
138+
}
139+
140+
async function renderFeatures(map, url, priorities) {
141+
try {
142+
const response = await fetch(url);
143+
const arrayBuffer = await response.arrayBuffer();
144+
const features = await decodeFeatures(arrayBuffer);
145+
146+
if (features.length === 0) {
147+
console.error('No features to render.');
148+
return;
149+
}
150+
151+
const bounds = L.latLngBounds();
152+
153+
features.forEach(feature => {
154+
const color = getPriorityColor(feature.priority);
155+
156+
const priorityDetails = priorities[feature.priority];
157+
const timelineInHours = priorityDetails ? priorityDetails.Timeline / 3600000000000 : 0; // ns to hours
158+
159+
const polyline = L.polyline(feature.coords, { color, weight: 6 })
160+
.bindPopup(`
161+
${feature.title || 'Unknown'}<br>
162+
<strong>Priority:</strong> ${feature.priority}<br>
163+
<strong>Deadline:</strong> ${formatDeadline(priorityDetails.Deadline)}<br>
164+
<strong>Timeline:</strong> ${timelineInHours} hours
165+
`).addTo(map);
166+
167+
bounds.extend(polyline.getBounds());
168+
});
169+
170+
if (bounds.isValid()) {
171+
map.fitBounds(bounds);
172+
} else {
173+
console.error('Calculated bounds are not valid.');
174+
}
175+
176+
} catch (error) {
177+
console.error('Error rendering features:', error);
178+
}
179+
}
180+
181+
function getPriorityColor(priority) {
182+
switch (priority) {
183+
case 1: return '#017A74'; // High priority
184+
case 2: return '#E66101'; // Medium priority
185+
case 3: return '#6B3FA0'; // Low priority
186+
default: return '#CCCCCC'; // Default color
187+
}
188+
}
123189

124-
// Load priorities.json
125190
fetch('priorities.json')
126191
.then(response => response.json())
127192
.then(priorities => {
128193
const prioritiesHeader = document.getElementById('priorities-header');
129194

130-
// Generate horizontal list of priorities
131195
prioritiesHeader.innerHTML = Object.entries(priorities)
132196
.map(([priority, details]) => `
133197
<div class="priority-item priority-${priority}">
@@ -136,8 +200,11 @@
136200
</div>
137201
`)
138202
.join('');
203+
204+
renderFeatures(map, 'features.bin', priorities);
139205
})
140206
.catch(error => console.error('Error loading priorities:', error));
141207
</script>
142208
</body>
209+
143210
</html>

main.go

Lines changed: 88 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package main
22

33
import (
4+
"bytes"
5+
"encoding/binary"
46
"encoding/json"
57
"fmt"
68
"io"
79
"log"
10+
"math"
811
"net/http"
912
"os"
1013
"slices"
@@ -13,7 +16,6 @@ import (
1316

1417
"github.com/paulmach/orb"
1518
"github.com/paulmach/orb/geojson"
16-
"github.com/paulmach/orb/planar"
1719
)
1820

1921
const activeTravelwaysDownloadURL = `https://hub.arcgis.com/api/download/v1/items/a3631c7664ef4ecb93afb1ea4c12022b/geojson?redirect=false&layers=0&spatialRefId=4326`
@@ -38,13 +40,14 @@ func main() {
3840
}
3941

4042
type priority struct {
43+
Number int
4144
Timeline time.Duration
4245
Deadline time.Time
4346
}
4447
priorities := map[string]priority{
45-
"1": {12 * time.Hour, endTime.Add(12 * time.Hour)},
46-
"2": {18 * time.Hour, endTime.Add(18 * time.Hour)},
47-
"3": {36 * time.Hour, endTime.Add(36 * time.Hour)},
48+
"1": {1, 12 * time.Hour, endTime.Add(12 * time.Hour)},
49+
"2": {2, 18 * time.Hour, endTime.Add(18 * time.Hour)},
50+
"3": {3, 36 * time.Hour, endTime.Add(36 * time.Hour)},
4851
}
4952

5053
resp, err := http.Get(activeTravelwaysDownloadURL)
@@ -82,13 +85,7 @@ func main() {
8285
}
8386

8487
fc.Features = slices.DeleteFunc(fc.Features, func(f *geojson.Feature) bool {
85-
if f.Properties["WINT_LOS"] == nil {
86-
return true
87-
}
88-
if !isPolygonInsidePolygon(peninsulaPolygon.Geometry().(orb.Polygon), f.Geometry) {
89-
return true
90-
}
91-
return false
88+
return f.Properties["WINT_LOS"] == nil
9289
})
9390
for _, f := range fc.Features {
9491
props := f.Properties
@@ -101,17 +98,16 @@ func main() {
10198
}
10299

103100
f.Properties["title"] = props["LOCATION"]
104-
f.Properties["priority"] = priorityNum
101+
f.Properties["priority"] = p.Number
105102
f.Properties["timeline"] = fmt.Sprintf("%d hours", int(p.Timeline.Hours()))
106103
f.Properties["deadline"] = p.Deadline.Format("Mon 3:04 PM")
107104
}
108105

109-
b, err = json.Marshal(fc)
110-
if err != nil {
106+
var out bytes.Buffer
107+
if err := encodeFeatures(fc.Features, &out); err != nil {
111108
log.Fatal(err)
112109
}
113-
114-
if err := os.WriteFile("data.geojson", b, 0644); err != nil {
110+
if err := os.WriteFile("features.bin", out.Bytes(), 0644); err != nil {
115111
log.Fatal(err)
116112
}
117113

@@ -125,42 +121,87 @@ func main() {
125121
}
126122
}
127123

128-
func isPolygonInsidePolygon(outer orb.Polygon, inner orb.Geometry) bool {
129-
switch inner := inner.(type) {
130-
case orb.Polygon:
131-
// Check if all points of the inner polygon are inside the outer polygon
132-
for _, ring := range inner {
133-
for _, point := range ring {
134-
if !planar.PolygonContains(outer, point) {
135-
return false // If any point is outside, it's not wholly contained
136-
}
137-
}
124+
func encodeFeatures(features []*geojson.Feature, writer io.Writer) error {
125+
if len(features) == 0 {
126+
return fmt.Errorf("no features")
127+
}
128+
if len(features) > math.MaxUint32 {
129+
return fmt.Errorf("too many features: %d exceeds uint32 capacity", len(features))
130+
}
131+
132+
// Write the number of features
133+
if err := binary.Write(writer, binary.LittleEndian, uint32(len(features))); err != nil {
134+
return err
135+
}
136+
137+
var baseLon, baseLat float64
138+
first := features[0]
139+
switch g := first.Geometry.(type) {
140+
case orb.LineString:
141+
baseLon, baseLat = g[0][0], g[0][1]
142+
case orb.MultiLineString:
143+
baseLon, baseLat = g[0][0][0], g[0][0][1]
144+
default:
145+
return fmt.Errorf("unknown geometry type: %T", g)
146+
}
147+
148+
if err := binary.Write(writer, binary.LittleEndian, float64(baseLon)); err != nil {
149+
return err
150+
}
151+
if err := binary.Write(writer, binary.LittleEndian, float64(baseLat)); err != nil {
152+
return err
153+
}
154+
155+
for _, feature := range features {
156+
// Write title length and title
157+
titleBytes := []byte(feature.Properties.MustString("title", ""))
158+
if len(titleBytes) > 255 {
159+
return fmt.Errorf("title too long: %s exceeds 255 bytes", titleBytes)
138160
}
139-
case orb.MultiPolygon:
140-
// Check if all points of the inner multipolygon are inside the outer polygon
141-
for _, p := range inner {
142-
if !isPolygonInsidePolygon(outer, p) {
143-
return false
144-
}
161+
if err := binary.Write(writer, binary.LittleEndian, uint8(len(titleBytes))); err != nil {
162+
return err
145163
}
146-
case orb.LineString:
147-
// Check if all points of the inner line string are inside the outer polygon
148-
for _, point := range inner {
149-
if !planar.PolygonContains(outer, point) {
150-
return false
164+
if _, err := writer.Write(titleBytes); err != nil {
165+
return err
166+
}
167+
168+
// Write priority
169+
if err := binary.Write(writer, binary.LittleEndian, uint8(feature.Properties.MustInt("priority", 0))); err != nil {
170+
return err
171+
}
172+
173+
var ls orb.LineString
174+
switch g := feature.Geometry.(type) {
175+
case orb.LineString:
176+
ls = g
177+
case orb.MultiLineString:
178+
for _, gLS := range g {
179+
ls = append(ls, gLS...)
151180
}
181+
default:
182+
return fmt.Errorf("unknown geometry type: %T", g)
152183
}
153-
case orb.MultiLineString:
154-
// Check if all points of the inner multi line string are inside the outer polygon
155-
for _, ls := range inner {
156-
for _, point := range ls {
157-
if !planar.PolygonContains(outer, point) {
158-
return false
159-
}
184+
185+
if len(ls) > math.MaxUint16 {
186+
return fmt.Errorf("too many coordinates: %d exceeds uint16 capacity", len(ls))
187+
}
188+
189+
// Write number of coordinates
190+
if err := binary.Write(writer, binary.LittleEndian, uint16(len(ls))); err != nil {
191+
return err
192+
}
193+
194+
for _, coord := range ls {
195+
deltaLon := int32(math.Round((coord[0] - baseLon) * 1000000))
196+
if err := binary.Write(writer, binary.LittleEndian, deltaLon); err != nil {
197+
return err
198+
}
199+
deltaLat := int32(math.Round((coord[1] - baseLat) * 1000000))
200+
if err := binary.Write(writer, binary.LittleEndian, deltaLat); err != nil {
201+
return err
160202
}
161203
}
162-
default:
163-
log.Fatalf("unknown geometry type: %T", inner)
164204
}
165-
return true
205+
206+
return nil
166207
}

0 commit comments

Comments
 (0)