Skip to content

Commit d4dfb78

Browse files
committed
feat: add highlight schools layer to visualize school-covered buildings and enhance shelter access app functionality
Made-with: Cursor
1 parent 8e2c23e commit d4dfb78

3 files changed

Lines changed: 152 additions & 34 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ npm-debug.log*
4444
# Local development
4545
.env
4646
.env.local
47+
js/config.js
4748

4849
# Backup files
4950
*.bak

index.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,21 @@ <h2>Data Layers</h2>
373373
>
374374
</div>
375375
</div>
376+
<div class="layer-item">
377+
<div class="layer-icon" style="color: #22c55e; font-size: 18px; display: flex; align-items: center; justify-content: center;">
378+
🏫
379+
</div>
380+
<label class="toggle-switch">
381+
<input type="checkbox" id="highlightSchoolsLayer" />
382+
<span class="toggle-slider"></span>
383+
</label>
384+
<div class="layer-info">
385+
<span class="layer-label">Highlight Schools</span>
386+
<span class="layer-description"
387+
>Green highlight • School shelters &amp; covered buildings</span
388+
>
389+
</div>
390+
</div>
376391
</div>
377392
</div>
378393
</div>
@@ -1461,6 +1476,7 @@ <h2>מדריך שימוש</h2>
14611476
</div>
14621477

14631478
<!-- Application JavaScript -->
1479+
<script src="js/config.js"></script>
14641480
<script src="js/spatial-analysis-simple.js"></script>
14651481
<script src="js/app.js"></script>
14661482
</body>

js/app.js

Lines changed: 135 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,13 @@ class ShelterAccessApp {
5353
optimalShelters: true,
5454
statisticalAreas: false,
5555
habitationClusters: false,
56-
accessibilityHeatmap: false
56+
accessibilityHeatmap: false,
57+
highlightSchools: false
5758
};
5859

60+
// Cached set of building indices covered by school shelters
61+
this.schoolCoveredBuildings = new Set();
62+
5963
// Precomputed shelter coverage data
6064
this.shelterCoverageData = null;
6165

@@ -71,8 +75,8 @@ class ShelterAccessApp {
7175

7276

7377

74-
// Mapbox token for terrain and other services (URL restricted)
75-
this.mapboxToken = 'pk.eyJ1IjoibnVybGFiMjAyNSIsImEiOiJjbWRrbW51ZnIweGxqMmxzNTJsc3pkODFkIn0.45x41GXrY4QBBqrHBlc7fw';
78+
// Loaded from js/config.js (gitignored)
79+
this.mapboxToken = window.MAPBOX_TOKEN || '';
7680

7781
// Basemap configuration - Mapbox primary, ESRI fallback
7882
this.currentBasemap = 'satellite';
@@ -130,7 +134,8 @@ class ShelterAccessApp {
130134
requestedSheltersLayer: document.getElementById('requestedSheltersLayer'),
131135
optimalSheltersLayer: document.getElementById('optimalSheltersLayer'),
132136
statisticalAreasLayer: document.getElementById('statisticalAreasLayer'),
133-
habitationClustersLayer: document.getElementById('habitationClustersLayer')
137+
habitationClustersLayer: document.getElementById('habitationClustersLayer'),
138+
highlightSchoolsLayer: document.getElementById('highlightSchoolsLayer')
134139
};
135140
}
136141

@@ -205,6 +210,27 @@ class ShelterAccessApp {
205210
return this.calculateShelterCoverage(shelter);
206211
}
207212

213+
/**
214+
* Compute the union of all building indices covered by school-type shelters
215+
*/
216+
computeSchoolCoverage() {
217+
const currentData = this.spatialAnalyzer.getCurrentData();
218+
if (!currentData.shelters) return;
219+
220+
const schoolShelters = currentData.shelters.features.filter(s =>
221+
s.properties && s.properties.type === 'School' && s.properties.status === 'Built'
222+
);
223+
224+
const covered = new Set();
225+
for (const shelter of schoolShelters) {
226+
const indices = this.getShelterCoverage(shelter);
227+
for (const idx of indices) {
228+
covered.add(idx);
229+
}
230+
}
231+
this.schoolCoveredBuildings = covered;
232+
}
233+
208234
/**
209235
* Initialize the application
210236
*/
@@ -534,6 +560,18 @@ class ShelterAccessApp {
534560
}
535561
});
536562

563+
if (this.elements.highlightSchoolsLayer) {
564+
this.elements.highlightSchoolsLayer.addEventListener('change', (e) => {
565+
this.layerVisibility.highlightSchools = e.target.checked;
566+
if (e.target.checked) {
567+
this.computeSchoolCoverage();
568+
} else {
569+
this.schoolCoveredBuildings = new Set();
570+
}
571+
this.updateVisualization();
572+
});
573+
}
574+
537575

538576
}
539577

@@ -902,12 +940,12 @@ class ShelterAccessApp {
902940

903941
// === BUILDINGS LAYER (Simplified GeoJSON) ===
904942
const shouldShowBuildings = this.layerVisibility.buildings ||
905-
(this.hoveredShelter && this.hoveredBuildings.length > 0);
943+
(this.hoveredShelter && this.hoveredBuildings.length > 0) ||
944+
this.layerVisibility.highlightSchools;
906945

907946
if (shouldShowBuildings && currentData.buildings && currentData.buildings.features.length > 0) {
908947
let buildingsToShow;
909-
if (this.layerVisibility.buildings) {
910-
// Show all buildings when layer is enabled
948+
if (this.layerVisibility.buildings || this.layerVisibility.highlightSchools) {
911949
buildingsToShow = currentData.buildings.features;
912950
} else {
913951
// Only show covered buildings when layer is disabled (highlighting mode)
@@ -924,7 +962,7 @@ class ShelterAccessApp {
924962
id: 'buildings-geojson',
925963
data: buildingsToShow.map((feature, index) => ({
926964
...feature,
927-
_index: index // Add array index for identification
965+
_index: index
928966
})),
929967
pickable: false,
930968
stroked: true,
@@ -935,37 +973,51 @@ class ShelterAccessApp {
935973
const buildingIndex = d._index || 0;
936974

937975
if (this.layerVisibility.buildings) {
938-
// Check if this building is covered by hovered shelter (turn green)
939976
if (this.hoveredShelter && this.hoveredBuildings.includes(buildingIndex)) {
940-
return [0, 255, 0, 150]; // Bright green for hover coverage
977+
return [0, 255, 0, 150];
978+
}
979+
if (this.layerVisibility.highlightSchools && this.schoolCoveredBuildings.has(buildingIndex)) {
980+
return [34, 197, 94, 170]; // Green for school-covered buildings
981+
}
982+
return [59, 130, 246, 180];
983+
} else if (this.layerVisibility.highlightSchools) {
984+
if (this.schoolCoveredBuildings.has(buildingIndex)) {
985+
return [34, 197, 94, 170];
941986
}
942-
return [59, 130, 246, 180]; // Blue for all other buildings when layer is enabled (matches menu buttons)
987+
return [59, 130, 246, 60]; // Faded blue for non-covered
943988
} else {
944-
// Buildings layer is disabled - only show covered buildings in green
945-
return [0, 255, 0, 200]; // Bright green for covered buildings
989+
return [0, 255, 0, 200];
946990
}
947991
},
948992
getLineColor: d => {
949993
const buildingIndex = d._index || 0;
950994

951995
if (this.layerVisibility.buildings) {
952-
// Check if this building is covered by hovered shelter (turn green)
953996
if (this.hoveredShelter && this.hoveredBuildings.includes(buildingIndex)) {
954-
return [0, 255, 0, 255]; // Bright green outline for hover
997+
return [0, 255, 0, 255];
998+
}
999+
if (this.layerVisibility.highlightSchools && this.schoolCoveredBuildings.has(buildingIndex)) {
1000+
return [34, 197, 94, 255];
1001+
}
1002+
return [59, 130, 246, 220];
1003+
} else if (this.layerVisibility.highlightSchools) {
1004+
if (this.schoolCoveredBuildings.has(buildingIndex)) {
1005+
return [34, 197, 94, 255];
9551006
}
956-
return [59, 130, 246, 220]; // Blue outline for all other buildings (matches menu buttons)
1007+
return [59, 130, 246, 80];
9571008
} else {
958-
// Buildings layer is disabled - only show covered building outlines in green
959-
return [0, 255, 0, 255]; // Bright green outline for covered buildings
1009+
return [0, 255, 0, 255];
9601010
}
9611011
},
9621012
getLineWidth: d => {
9631013
const buildingIndex = d._index || 0;
9641014

965-
// Thicker outline for highlighted buildings
9661015
if (this.hoveredShelter && this.hoveredBuildings.includes(buildingIndex)) {
9671016
return 2;
9681017
}
1018+
if (this.layerVisibility.highlightSchools && this.schoolCoveredBuildings.has(buildingIndex)) {
1019+
return 2;
1020+
}
9691021
return 1;
9701022
}
9711023
}));
@@ -1110,19 +1162,57 @@ class ShelterAccessApp {
11101162
);
11111163

11121164
if (existingShelters.length > 0) {
1113-
layers.push(new deck.IconLayer({
1114-
id: 'existing-shelters',
1115-
data: existingShelters,
1116-
pickable: true,
1117-
getIcon: () => this.getShelterIcon('existing'),
1118-
getPosition: d => d.geometry.coordinates,
1119-
onHover: (info) => this.handleHover(info),
1120-
onClick: (info) => this.handleClick(info),
1121-
loadOptions: this.getSVGLoadOptions(),
1122-
textureParameters: this.getTextureParameters(),
1123-
alphaCutoff: 0.05, // Clean edges for better visual quality
1124-
...this.getIconSizeConfig('existing')
1125-
}));
1165+
if (this.layerVisibility.highlightSchools) {
1166+
const nonSchoolShelters = existingShelters.filter(s => s.properties.type !== 'School');
1167+
const schoolShelters = existingShelters.filter(s => s.properties.type === 'School');
1168+
1169+
if (nonSchoolShelters.length > 0) {
1170+
layers.push(new deck.IconLayer({
1171+
id: 'existing-shelters',
1172+
data: nonSchoolShelters,
1173+
pickable: true,
1174+
getIcon: () => this.getShelterIcon('existing'),
1175+
getPosition: d => d.geometry.coordinates,
1176+
onHover: (info) => this.handleHover(info),
1177+
onClick: (info) => this.handleClick(info),
1178+
loadOptions: this.getSVGLoadOptions(),
1179+
textureParameters: this.getTextureParameters(),
1180+
alphaCutoff: 0.05,
1181+
...this.getIconSizeConfig('existing')
1182+
}));
1183+
}
1184+
if (schoolShelters.length > 0) {
1185+
layers.push(new deck.ScatterplotLayer({
1186+
id: 'school-shelters',
1187+
data: schoolShelters,
1188+
pickable: true,
1189+
getPosition: d => d.geometry.coordinates,
1190+
getRadius: 40,
1191+
radiusMinPixels: 6,
1192+
radiusMaxPixels: 14,
1193+
getFillColor: [34, 197, 94, 220],
1194+
getLineColor: [255, 255, 255, 255],
1195+
lineWidthMinPixels: 2,
1196+
stroked: true,
1197+
onHover: (info) => this.handleHover(info),
1198+
onClick: (info) => this.handleClick(info)
1199+
}));
1200+
}
1201+
} else {
1202+
layers.push(new deck.IconLayer({
1203+
id: 'existing-shelters',
1204+
data: existingShelters,
1205+
pickable: true,
1206+
getIcon: () => this.getShelterIcon('existing'),
1207+
getPosition: d => d.geometry.coordinates,
1208+
onHover: (info) => this.handleHover(info),
1209+
onClick: (info) => this.handleClick(info),
1210+
loadOptions: this.getSVGLoadOptions(),
1211+
textureParameters: this.getTextureParameters(),
1212+
alphaCutoff: 0.05,
1213+
...this.getIconSizeConfig('existing')
1214+
}));
1215+
}
11261216
}
11271217
}
11281218

@@ -1254,6 +1344,15 @@ class ShelterAccessApp {
12541344
});
12551345
}
12561346

1347+
if (this.layerVisibility.highlightSchools) {
1348+
legendItems.push({
1349+
type: 'color-dot',
1350+
className: 'school-shelter',
1351+
label: 'Schools',
1352+
color: 'rgb(34, 197, 94)'
1353+
});
1354+
}
1355+
12571356
// Clear existing legend items
12581357
this.elements.legendItems.innerHTML = '';
12591358

@@ -1274,17 +1373,19 @@ class ShelterAccessApp {
12741373
iconDiv.className = `legend-icon ${item.className}`;
12751374

12761375
if (item.type === 'svg-icon') {
1277-
// Use actual SVG icons that match the map icons exactly
12781376
const img = document.createElement('img');
12791377
img.src = item.iconSrc;
12801378
img.className = 'legend-icon-img';
12811379
iconDiv.appendChild(img);
12821380
} else if (item.type === 'color-box') {
1283-
// Use inner div for color boxes to match SVG icon structure
12841381
const colorBox = document.createElement('div');
12851382
colorBox.className = 'legend-color-box';
12861383
colorBox.style.background = item.color;
12871384
iconDiv.appendChild(colorBox);
1385+
} else if (item.type === 'color-dot') {
1386+
const dot = document.createElement('div');
1387+
dot.style.cssText = `width:10px;height:10px;border-radius:50%;background:${item.color};border:2px solid #fff;box-shadow:0 0 2px rgba(0,0,0,0.3);`;
1388+
iconDiv.appendChild(dot);
12881389
}
12891390

12901391
const label = document.createElement('span');

0 commit comments

Comments
 (0)