-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.html
985 lines (937 loc) · 52.8 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
<!DOCTYPE html>
<html>
<head>
<!-- this is the title of the page (not the map) and will appear in the browser tab -->
<title>CSV-2-Storymap</title>
<!-- this controls the layout on mobile browsers, if needed -->
<!--<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">-->
<!-- reference existing css libraries below -->
<!-- load leaflet.css library here -->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<!-- for icon support -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" />
<!-- for a collapsible sidebar -->
<link rel="stylesheet" href="css/leaflet-sidebar.css" />
<!-- custom css styles go below -->
<style>
body {
padding: 0;
margin: 0;
}
html,
body,
#map {
height: 100%;
font: 10pt "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* make these divs invisible initially */
#download-code, #dropdown-instructions, #map_parameters, #sidebar-info {
display: none;
}
i {
color: white;
}
/* control the popup size and allow for scrolling of overflow */
.leaflet-popup-content-wrapper {
line-height: 10px;
border-radius: 2px;
height: 10 px;
max-height: 300px;
max-width: 350px;
overflow: auto;
}
.leaflet-popup {
position: absolute;
text-align: center;
}
.leaflet-popup-content {
min-width: 100 px !important;
}
/* resizes the images in the sidebar */
.resize {
width: 300px;
height: auto;
}
p {
font-size: 12px;
}
h2 {
font-size: 13px;
margin-bottom: 0px;
margin-top: 0px;
}
h3 {
font-size: 12px;
font-weight: normal;
margin-bottom: 0px;
margin-top: 0px;
}
.br {
display: block;
margin-bottom: 0em;
}
.brmedium {
display: block;
margin-bottom: 1em;
}
@media (max-width: 1000px) {
.sidebar-header {
font-size: 15px;
}
p {
font-size: 11px;
}
h2 {
font-size: 12px;
margin-bottom: 0px;
margin-top: 0px;
}
h3 {
font-size: 11px;
font-weight: normal;
margin-bottom: 0px;
margin-top: 0px;
}
.resize {
width: 200px;
height: auto;
}
}
</style>
</head>
<body>
<!-- the sidebar div -->
<div id="sidebar" class="sidebar collapsed">
<!-- Nav tabs -->
<div class="sidebar-tabs">
<ul role="tablist">
<li><a href="#home" role="tab"><i class="fa fa-bars"></i></a></li>
</ul>
</div>
<!-- the sidebar contents -->
<div class="sidebar-content">
<div class="sidebar-pane" id="home">
<!-- map title goes here -->
<h1 class="sidebar-header">CSV-2-Storymap<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>
<br>
<div>
<br>
<b><h2>About This Project</h2></b>
<span class='br'></span>
<h3>For instructions on how to use this application, please see the repository <a href='https://github.com/jebowe3/CSV-2-Storymap'>here</a>. This application was created to allow users to generate an open source story map quickly and easily from a formatted csv file. After adding your selected spreadsheet, you should see markers for each entry colored by the provided
chapter. These chapters will also appear in the dropdown menu so that you can filter your content by the selected category. Enjoy!</h3>
<p style='padding-bottom:0px'></p>
</div>
<!-- title change form -->
<form>
<p><b><label for="fname">Choose a title for your story map:</label></b><p>
<input type="text" id="map-name" name="map-name">
<button id="myBtn" type='button' onclick="changeTitle()">Submit</button>
</form>
<div id="browse-buttons">
<!-- csv selection instructions -->
<p><b>Browse for the input csv file.</b></p>
<input type="file" id="fileBox" />
</div>
<!-- dropdown filter instructions -->
<div id="dropdown-instructions">
<p><b>Choose from the options in the drop-down menu to filter results by the type of entry.</b></p>
</div>
<!-- the form contents -->
<form id="map_parameters" name="map_parameters" action="#" accept-charset="utf-8" class="inlineForm">
<select id="chapter-select" class="div-toggle" data-target=".my-info-1"></select>
</form>
<span class='brmedium'></span>
<div id="sidebar-info" class="my-info-1">
<div id="content-info" class="contentinfo hide"></div>
</div>
<!-- button to download code -->
<div id="download-code">
<p><b>Download your story map code.</b></p>
<button id="download-button" type='button' onclick="document.getElementById('link').click()">Download</button>
</div>
</div>
</div>
</div>
<!-- the map container -->
<div id="map" class="sidebar-map"></div>
<!-- reference existing js libraries below -->
<!-- load leaflet.js library here -->
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<!-- d3js for requesting files into web application -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.7/d3.min.js"></script>
<!-- jquery for easy access to divs -->
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<!-- for svg icons -->
<script src="js/svg-icon.js"></script>
<!-- for the collapsible sidebar -->
<script src="js/leaflet-sidebar.js"></script>
<!-- custom javascript goes below -->
<script>
// set the desired marker-level zoom here
const markerZoom = 14;
// instantiate and define a Leaflet map
const map = L.map('map', {
center: [0, -70], // give map initial coordinates
zoom: 2 // set initial zoom level
});
// add CartoDB Voyager map tile service to the map
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd',
maxZoom: 20
}).addTo(map);
// add a scale bar to the map
L.control.scale({
position: 'bottomright'
}).addTo(map);
// define and add the sidebar to the map (we may want to swap for sidebar-v2: https://github.com/Turbo87/sidebar-v2)
const sidebar = L.control.sidebar('sidebar').addTo(map);
// show sidebar content on map load
sidebar.open('home');
// define the input for the user-defined map name
const input = document.getElementById("map-name");
// Execute a function when the user presses a key on the keyboard
input.addEventListener("keypress", function(event) {
// If the user presses the "Enter" key on the keyboard
if (event.key === "Enter") {
// Cancel the default action, if needed
event.preventDefault();
// Trigger the button element with a click
document.getElementById("myBtn").click();
}
});
// define an empty array to hold sidebar content
const chapterContent = [];
// define an empty array to hold the selected chapter
const chapterSelected = [];
// define an empty feature group to hold filtered markers
const selectedLayers = L.featureGroup().addTo(map);
// define the sidebar information
const info = document.getElementById("content-info");
// define an empty array to hold the 'data-point' (coordinate) attribute of each story div
const points = [];
// define an empty array to hold all chapter values
const chapterNames = [];
// define an empty array to hold orders for matching
const scrollOrds = [];
// define an empty array to hold all 'data-point' order values
const orders = [];
// on load, provide an option to add a csv file
window.onload = function() {
// after selecting the input csv...
document.getElementById('fileBox').onchange = function() {
// read the csv and create the dropdown from the csv contents
readCSV(this);
// add additional sidebar contents
document.getElementById("dropdown-instructions").style.display="block";
document.getElementById("map_parameters").style.display="block";
document.getElementById("sidebar-info").style.display="block";
}
};
// define a function to read the user-selected csv
function readCSV(csv) {
// define the map name
const name = document.getElementById('map-name').value;
// identify the input file
const file = csv.files[0];
// create a working url to the input csv file
const path = (window.URL || window.webkitURL).createObjectURL(file);
// use d3 to access and parse spreadsheet data
d3.csv(path).then(function(data) {
// make an empty geoJSON object
markersGeoJSON = {
"type": "FeatureCollection",
"features": []
};
// for each entry in the csv...
data.forEach(function(entry) {
// define a JSON feature
feature = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [entry.X, entry.Y] // use the X and Y columns from the csv
},
"properties": entry // add all columns as properties
}
// push the feature to the empty markersGeoJSON
markersGeoJSON.features.push(feature);
});
// call the _drawMap function
drawMap(markersGeoJSON);
// add additional sidebar contents
document.getElementById("download-code").style.display="block";
// call the downloadCode function
downloadCode(markersGeoJSON, name);
});
}; // end readCSV function
// define a function to draw the map with the input markers
function drawMap(markersGeoJSON) {
// empty the chapterNames array
chapterNames.length = 0;
// empty the chapterContent array
chapterContent.length = 0;
// iterate through each feature of the data and...
(markersGeoJSON.features).forEach(function(entry){
// push each chapter value to the empty chapterNames array
chapterNames.push(entry.properties.chapter);
});
// filter the chapterNames array for unique values and define
const eachChapter = chapterNames.filter((v, i, a) => a.indexOf(v) === i);
// establish the initial dropdown html form contents
let tmpHTML = '<select><option value="0">All</option>';
// for each unique chapter, create a dropdown select option
for (let csv = 0; csv < eachChapter.length; csv++) tmpHTML += '<option value="' + eachChapter[csv] + '">' + eachChapter[csv] + '</option>';
tmpHTML += '</select>';
// add this content to the dynamic dropdown
document.getElementById("chapter-select").innerHTML = tmpHTML;
// construct empty JavaScript Map (keys mapped to values)
const paletteMap = new Map();
// map all chapters to a dynamic RGB color string
for (let i = 0; i < eachChapter.length; i++) {
var palette;
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
palette = "rgb(" + r + "," + g + "," + b + ")";
paletteMap.set(eachChapter[i], palette)
};
// define a leaflet geojson layer with the markers
const markers = L.geoJSON(markersGeoJSON, {
// assign the appropriate icon to each point
pointToLayer: function(feature, latlng) {
return L.marker(latlng, {
icon: new L.DivIcon.SVGIcon({
color: paletteMap.get(feature.properties.chapter), // get dynamic RGB color string
fillColor: paletteMap.get(feature.properties.chapter), // get dynamic RGB color string
fillOpacity: 0.8,
iconSize: [25, 35]
})
});
},
// on each marker...
onEachFeature: function(feature, layer) {
// define the layer properties
const props = feature.properties;
// bind popup content
layer.bindPopup('<b><h2>' + props.name + "</h2></b><span class='br'></span><h3>" + props.chapter + '<hr>' + "<span class='br'></span>" + "</h3><span class='brmedium'></span><b><h2>Description:</h2></b><span class='br'></span><h3>" +
props.description + "</h3>");
// change the cursor to a pointer on marker hover
layer.on('mouseover', function() {
$('.leaflet-container').css('cursor', 'pointer');
});
// change the cursor back to default on mouseout
layer.on('mouseout', function() {
$('.leaflet-container').css('cursor', '');
});
}
}).addTo(map);
// initially, add all markers to the selectedLayers layer group
selectedLayers.addLayer(markers);
// INITIALLY, ADD ALL DESCRIPTIONS FROM ALL ENTRIES TO THE SIDEBAR
// iterate through each marker
markers.eachLayer(function(layer) {
// define the marker properties
const props = layer.feature.properties;
// call the getImg function
var imgContent = getImg(layer, props);
// call the getVid function
var vidContent = getVid(layer, props);
// call the getAudio function
var audioContent = getAudio(layer, props);
// push content to the chapterContent array
chapterContent.push("<div class='story' order='" + props.order + "' data-point='" + props.Y + "," + props.X + "'><b><h2>" + props.name + "</h2></b><span class='br'></span><h3>" + props.chapter + "<span class='br'></span>" + "</h3><span class='brmedium'></span><h3>" + props.description + "</h3>" + imgContent + vidContent + audioContent + "<span class='brmedium'></span></div>");
});
// define some spacing for the end of the scroll bar
const scrollEndHeight = 200;
// add a final div to the story map scroll bar
chapterContent.push(
"<div><b><h2>See the Code</h2></b><span class='br'></span><h3>To see the code for this map, please go to: <a href='https://github.com/jebowe3/CSV-2-Storymap'>CSV-2-Storymap</a>.</h3><p style='padding-bottom:" + scrollEndHeight + "px'></p></div>");
// redefine the div
info.innerHTML = chapterContent.join("<span class='br'></span>");
// END ADDING ALL DESCRIPTIONS FROM ALL ENTRIES TO THE SIDEBAR
// get the difference between the map's central meridian and the visible map's central meridian at the marker zoom level
const winDiff = getWinDiff();
// upon a dropdown menu submission...
$('#chapter-select')[0].onchange = function(e) {
// call the filterMarkers function on the markers
filterMarkers(markers/*, winDiff*/);
};
// call the scrollMarkers function on the markers
scrollMarkers(markers, winDiff);
// call the scrollOpenPopup function on the markers
scrollOpenPopup(markers);
}; // end drawMap function
// define the scrollMarkers function
function scrollMarkers(markers, winDiff) {
// fit the map to the markers adding padding to the top left so all markers are visible with the sidebar open
map.fitBounds(markers.getBounds(), {paddingTopLeft: [document.getElementById('sidebar').getBoundingClientRect().right,0]});
// set the scrollTimeout to null
let scrollTimeout = null;
// detects a scroll event in the sidebar
$('.sidebar-content').on("scroll", function(e) {
// with each scroll, empty the scrollOrds array
scrollOrds.length = 0;
// iterate through each story div in the sidebar
$('.story').each(function() {
// gets the horizontal line crossing the middle of the window
const midLine = $(window).height() / 2;
// defines the vertical start of each story div
const divStart = $(this).offset().top;
// defines the vertical end of each story div
const divEnd = divStart + $(this)[0].offsetHeight;
// defines the latitude associated with the story div
const lat = $(this)[0].getAttribute('data-point').split(',')[0];
// defines the longitude associated with the story div
const lng = $(this)[0].getAttribute('data-point').split(',')[1];
// define the order of the story div
const order = $(this)[0].getAttribute('order');
// if the start and end of the div crosses the middle of the sidebar...
if (divStart < midLine && divEnd > midLine) {
// highlight the target div on scroll with an off-white shade
$(this)[0].style.backgroundColor = "#EFEEEC";
// narrow down the points array to the last two points in the list
points.splice(0, points.length - 1);
// define a story div point
const point = $(this)[0].getAttribute('data-point');
// push the point into the points array
points.push(point);
// push the order into the orders array
orders.push(order);
// if the last point does not equal the point before it, or the points are equal and the orders are not...
if (points[0] != points[1] || points[0] == points[1] && orders[0] != orders[1]) {
//if (points[0] != points[1]) {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(function() {
// zoom to the marker connected to the story div, making an affordance for the offset
map.flyTo([lat, lng - winDiff], markerZoom);
}, 200);
} // this condition prevents jarring associated with scrolling by waiting for a point change to reset the map view
// push the story order to the scrollOrds array
scrollOrds.push(order);
} else {
// reset the div's background color to white
$(this)[0].style.backgroundColor = "white";
}
// upon a change in the window size...
window.addEventListener('resize', function() {
const resizeWinDiff = getWinDiffResize();
// if the start and end of the div crosses the middle of the sidebar...
if (divStart < midLine && divEnd > midLine) {
// zoom to the marker connected to the story div
map.flyTo([lat, lng - resizeWinDiff], markerZoom);
}
});
});
});
}; // end of scrollMarkers() function
// define the scrollOpenPopup function
function scrollOpenPopup(markers) {
// make a function that clicks the correct marker when scrolling stops
let popupTimeout = null;
// detects a scroll event in the sidebar
$('.sidebar-content').on("scroll", function() {
if (popupTimeout) clearTimeout(popupTimeout);
popupTimeout = setTimeout(function() {
// iterate through each marker
markers.eachLayer(function(layer){
// define the order of each marker
const markerOrds = layer.feature.properties.order;
// if the first story order matches the marker order...
if (markerOrds === scrollOrds[0]) {
// fire a click on the marker, opening the popup
layer.fire('click');
layer.setZIndexOffset(1000);
} else {
layer.setZIndexOffset(0);
}
});
}, 200); // set the timeout period
});
}; // end of scrollOpenPopup() function
// define the filterMarkers function
function filterMarkers(markers) {
// empty the chapterContent array
chapterContent.length = 0;
// clear the content-info div
info.innerHTML = chapterContent.join("");
// empty the chapterSelected array
chapterSelected.length = 0;
// empty the layer group
selectedLayers.clearLayers();
// push the selected chapter to the empty chapterSelected array
chapterSelected.push($('#chapter-select')[0].value);
// iterate through each marker
markers.eachLayer(function(layer) {
// define the marker properties
const props = layer.feature.properties;
// call the getImg function
var imgContent = getImg(layer, props);
// call the getVid function
var vidContent = getVid(layer, props);
// call the getAudio function
var audioContent = getAudio(layer, props);
// check if the selected chapter matches the marker category
if (chapterSelected[0] == props.chapter) {
// and add it to the map in case of a match
selectedLayers.addLayer(layer);
// push content to the chapterContent array
chapterContent.push("<div class='story' order='" + props.order + "' data-point='" + props.Y + "," + props.X + "'><b><h2>" + props.name + "</h2></b><span class='br'></span><h3>" + props.chapter + "<span class='br'></span>" +
"</h3><span class='brmedium'></span><h3>" + props.description + "</h3>" + imgContent + vidContent + audioContent + "<span class='brmedium'></span></div>");
// redefine the div
//info.innerHTML = chapterContent.join("<hr>");
} else if (chapterSelected[0] == 0) {
// or add all if "All" is selected
selectedLayers.addLayer(layer);
// push content to the chapterContent array
chapterContent.push("<div class='story' order='" + props.order + "' data-point='" + props.Y + "," + props.X + "'><b><h2>" + props.name + "</h2></b><span class='br'></span><h3>" + props.chapter + "<span class='br'></span>" +
"</h3><span class='brmedium'></span><h3>" + props.description + "</h3>" + imgContent + vidContent + audioContent + "<span class='brmedium'></span></div>");
// redefine the div
//info.innerHTML = chapterContent.join("<hr>");
} else {
// or remove it from the map if there is no match
selectedLayers.removeLayer(layer);
}
});
// define some spacing for the end of the scroll bar
const scrollEndHeight = 200;
// add a final div to the story map scroll bar
chapterContent.push(
"<div><b><h2>See the Code</h2></b><span class='br'></span><h3>To see the code for this map, please go to: <a href='https://github.com/jebowe3/CSV-2-Storymap'>CSV-2-Storymap</a>.</h3><p style='padding-bottom:" +
scrollEndHeight + "px'></p></div>");
// redefine the div
info.innerHTML = chapterContent.join("<span class='br'></span>");
// fit the map to the markers adding padding to the top left so all markers are visible with the sidebar open
map.fitBounds(markers.getBounds(), {paddingTopLeft: [document.getElementById('sidebar').getBoundingClientRect().right,0]});
}; // end of filter markers function
// the following gets the offset between the map central meridian and visible map central meridian
function getWinDiff() {
// quickly set zoom to the marker level zoom to gather the following data
map.setZoom(markerZoom);
// the longitude of the right side of the map
const rightLng = map.getBounds().getNorthEast().lng;
// the longitude of the left side of the visible map
const leftLng = map.containerPointToLatLng([document.getElementById('sidebar').getBoundingClientRect().right,0]).lng;
// get the longitudinal distance between the edges of the visible map, divide by 2, and add to the longitude of the left side to get the visible map's central meridian
const centerLng = leftLng + ((rightLng - leftLng) / 2);
// get the difference between the map's central meridian and the visible map's central meridian
const winDiff = centerLng - L.latLng(map.getCenter()).lng;
// reset the zoom back to the initial map zoom
map.setZoom(2);
return winDiff;
}; // end getWinDiff function
// write a function to get the meridian offset on page resize
function getWinDiffResize() {
// the longitude of the right side of the map
const rightLng = map.getBounds().getNorthEast().lng;
// the longitude of the left side of the visible map
const leftLng = map.containerPointToLatLng([document.getElementById('sidebar').getBoundingClientRect().right,0]).lng;
// get the longitudinal distance between the edges of the visible map, divide by 2, and add to the longitude of the left side to get the visible map's central meridian
const centerLng = leftLng + ((rightLng - leftLng) / 2);
// get the difference between the map's central meridian and the visible map's central meridian
const winDiff = centerLng - L.latLng(map.getCenter()).lng;
return winDiff;
}; // end getWinDiffResize function
// define a function to change the map title
function changeTitle() {
// define the map name
const name = document.getElementById('map-name').value;
// add it to the sidebar header
$(".sidebar-content h1").html(name + '<span class="sidebar-close"><i class="fa fa-caret-left"></i></span>');
// add it to the page tab
document.title = document.querySelector('input').value;
// call the closeSidebar function
closeSidebar();
}; // end changeTitle function
// define a function to close the sidebar
function closeSidebar() {
// listen for a click on the caret icon
document.querySelector("span.sidebar-close").addEventListener('click', (e) => {
// close the sidebar
sidebar.close('home');
});
}; // end closeSidebar() function
// define the getImg function
function getImg(layer, props) {
// define the image content
let siteImage = props.image_url;
// define the image description
let imgDesc = props.image_description;
siteImage = siteImage || undefined;
if (siteImage != undefined) {
// check if there is an image description
if (imgDesc != "") {
var imgContent = "<span class='brmedium'></span><img class='resize' src='" + props.image_url + "'" + "/><span class='brmedium'></span><h3>" + props.image_description + "</h3>";
} else {
var imgContent = "<span class='brmedium'></span><img class='resize' src='" + props.image_url + "'" + "/>";
}
} else {
var imgContent = "";
};
return imgContent;
}; // end of getImg function
// define getVid function
function getVid(layer, props) {
// define the video content
let siteVideo = props.video_url;
// define the video description
let vidDesc = props.video_description;
// define regExp and...
let regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
// use it to break apart a youtube url into a list of significant fragments
let match = siteVideo.match(regExp);
// check if the video url is present
if (siteVideo != "") {
// check if there is a video description
if (vidDesc != "") {
// further check if the video is from youtube
if (match && match[2].length == 11) {
// if so define vidContent as follows
var vidContent = "<span class='brmedium'></span><iframe class='resize' src='" + 'https://www.youtube.com/embed/' + match[2] + '?autoplay=0' + "'>" + "</iframe><span class='brmedium'></span><h3>" + props.video_description + "</h3>";
} else {
// if not define vidContent as follows
var vidContent = "<span class='brmedium'></span><video class='resize' controls>" + "<source src='" + siteVideo + "' type='video/mp4'>" + "</video><span class='brmedium'></span><h3>" + props.video_description + "</h3>";
}
} else {
// further check if the video is from youtube
if (match && match[2].length == 11) {
// if so define vidContent as follows
var vidContent = "<span class='brmedium'></span><iframe class='resize' src='" + 'https://www.youtube.com/embed/' + match[2] + '?autoplay=0' + "'>" + "</iframe>";
} else {
// if not define vidContent as follows
var vidContent = "<span class='brmedium'></span><video class='resize' controls>" + "<source src='" + siteVideo + "' type='video/mp4'>" + "</video>";
}
}
} else {
// if the video url is not present...
var vidContent = "";
};
return vidContent;
}; // end getVid function
// define getAudio function
function getAudio(layer, props) {
// define the audio content
let siteAudio = props.audio_url;
// define the audio description
let audioDesc = props.audio_description;
siteAudio = siteAudio || undefined;
if (siteAudio != undefined) {
// check if there is an audio description
if (audioDesc != "") {
var audioContent = "<span class='brmedium'></span><audio controls class='resize' src='" + props.audio_url + "'" + "type=\"audio/mpeg\"></audio><span class='brmedium'></span><h3>" + props.audio_description + "</h3>";
} else {
var audioContent = "<span class='brmedium'></span><audio controls class='resize' src='" + props.audio_url + "'" + "type=\"audio/mpeg\"></audio>";
}
} else {
var audioContent = "";
};
return audioContent;
}; // end getAudio function
// define the function to download map code
function downloadCode(markersGeoJSON, name) {
// create a new index.html file and allow user to save
const html = '<!DOCTYPE html>' +
'<html>' +
'<head>' +
'<title>' + name + '</title>' +
'<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />' +
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" />' +
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jebowe3/CSV-2-Storymap/css/leaflet-sidebar.css" />' +
'<style>' +
'body {padding: 0;margin: 0;}' +
'html,body,#map {height: 100%;font: 10pt "Helvetica Neue", Arial, Helvetica, sans-serif;}' +
'i {color: white;}' +
'.leaflet-popup-content-wrapper {line-height: 10px;border-radius: 2px;height: 10 px;max-height: 300px;max-width: 350px;overflow: auto;}' +
'.leaflet-popup {position: absolute;text-align: center;}' +
'.leaflet-popup-content {min-width: 100 px !important;}' +
'.resize {width: 300px;height: auto;}' +
'p {font-size: 12px;}' +
'h2 {font-size: 13px;margin-bottom: 0px;margin-top: 0px;}' +
'h3 {font-size: 12px;font-weight: normal;margin-bottom: 0px;margin-top: 0px;}' +
'.br {display: block;margin-bottom: 0em;}' +
'.brmedium {display: block;margin-bottom: 1em;}' +
'@media (max-width: 1000px) {' +
'.sidebar-header {font-size: 15px;}' +
'p {font-size: 11px;}' +
'h2 {font-size: 12px;margin-bottom: 0px;margin-top: 0px;}' +
'h3 {font-size: 11px;font-weight: normal;margin-bottom: 0px;margin-top: 0px;}' +
'.resize {width: 200px;height: auto;}' +
'}' +
'</style>' +
'</head>' +
'<body>' +
'<div id="sidebar" class="sidebar collapsed">' +
'<div class="sidebar-tabs">' +
'<ul role="tablist">' +
'<li><a href="#home" role="tab"><i class="fa fa-bars"></i></a></li>' +
'</ul>' +
'</div>' +
'<div class="sidebar-content">' +
'<div class="sidebar-pane" id="home">' +
'<h1 class="sidebar-header">' + name + '<span class="sidebar-close"><i class="fa fa-caret-left"></i></span></h1>' +
'<br>' +
'<div>' +
'<br>' +
'<b><h2>About This Project</h2></b>' +
'<span class="br"></span>' +
'<h3>This map was created using <a href="https://github.com/jebowe3/CSV-2-Storymap" target="_blank">CSV-2-Storymap</a>, which takes a user input csv file and generates thematically coded markers for each entry based on the "chapter" attribute in the spreadsheet. These values are then fed to the dropdown menu allowing for user filtration of the scrolling sidebar content.</h3>' +
'<p style="padding-bottom:0px"></p>' +
'</div>' +
'<div id="dropdown-instructions">' +
'<p><b>Choose from the options in the drop-down menu to filter results by the type of entry.</b></p>' +
'</div>' +
'<form id="map_parameters" name="map_parameters" action="#" accept-charset="utf-8" class="inlineForm">' +
'<select id="chapter-select" class="div-toggle" data-target=".my-info-1"></select>' +
'</form>' +
'<span class=\'brmedium\'></span>' +
'<div id="sidebar-info" class="my-info-1">' +
'<div id="content-info" class="contentinfo hide"></div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'<div id="map" class="sidebar-map"></div>' +
'<script src="https://unpkg.com/[email protected]/dist/leaflet.js"><\/script>' +
'<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.7/d3.min.js"><\/script>' +
'<script src="https://code.jquery.com/jquery-3.1.1.min.js"><\/script>' +
'<script src="https://cdn.jsdelivr.net/gh/jebowe3/CSV-2-Storymap/js/svg-icon.js"><\/script>' +
'<script src="https://cdn.jsdelivr.net/gh/jebowe3/CSV-2-Storymap/js/leaflet-sidebar.js"><\/script>' +
'<script>const markersGeoJSON = ' + JSON.stringify(markersGeoJSON) + ';<\/script>' +
'<script>' +
'const markerZoom = 14;' +
'const map = L.map("map", {center: [0, -70], zoom: 2 });' +
'const sidebar = L.control.sidebar("sidebar").addTo(map);' +
'sidebar.open("home");' +
'const chapterContent = [];' +
'const chapterSelected = [];' +
'const selectedLayers = L.featureGroup().addTo(map);' +
'const info = document.getElementById("content-info");' +
'const points = [];' +
'const chapterNames = [];' +
'const scrollOrds = [];' +
'const orders = [];' +
'let mapTimeout = null;' +
'if (mapTimeout) clearTimeout(mapTimeout);' +
'mapTimeout = setTimeout(function() {' +
'L.tileLayer(\'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png\', {attribution: \'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>\', subdomains: \'abcd\', maxZoom: 20}).addTo(map);' +
'L.control.scale({position: "bottomright"}).addTo(map);' +
'drawMap(markersGeoJSON);' +
'}, 500);' +
'function drawMap(markersGeoJSON) {' +
'(markersGeoJSON.features).forEach(function(entry){' +
'chapterNames.push(entry.properties.chapter);' +
'});' +
'const eachChapter = chapterNames.filter((v, i, a) => a.indexOf(v) === i);' +
'let tmpHTML = \'<select><option value="0">All</option>\';' +
'for (let csv = 0; csv < eachChapter.length; csv++) tmpHTML += \'<option value="\' + eachChapter[csv] + \'">\' + eachChapter[csv] + \'</option>\';' +
'tmpHTML += \'</select>\';' +
'document.getElementById("chapter-select").innerHTML = tmpHTML;' +
'const paletteMap = new Map();' +
'for (let i = 0; i < eachChapter.length; i++) {var palette; const r = Math.floor(Math.random() * 255); const g = Math.floor(Math.random() * 255); const b = Math.floor(Math.random() * 255); palette = "rgb(" + r + "," + g + "," + b + ")"; paletteMap.set(eachChapter[i], palette)};' +
'const markers = L.geoJSON(markersGeoJSON, {' +
'pointToLayer: function(feature, latlng) {' +
'return L.marker(latlng, {' +
'icon: new L.DivIcon.SVGIcon({' +
'color: paletteMap.get(feature.properties.chapter),' +
'fillColor: paletteMap.get(feature.properties.chapter),' +
'fillOpacity: 0.8,' +
'iconSize: [25, 35]' +
'})' +
'});' +
'},' +
'onEachFeature: function(feature, layer) {' +
'const props = feature.properties;' +
'layer.bindPopup(\'<b><h2>\'' + '+ props.name +' + '"</h2></b><span class=\'br\'></span><h3>"' + '+ props.chapter +' + '\'<hr>\' + "<span class=\'br\'></span>" + "</h3><span class=\'brmedium\'></span><b><h2>Description:</h2></b><span class=\'br\'></span><h3>"' + '+ props.description +' + '"</h3>");' +
"layer.on('mouseover', function() {" +
"$('.leaflet-container').css('cursor', 'pointer');" +
'});' +
"layer.on('mouseout', function() {" +
"$('.leaflet-container').css('cursor', '');" +
'});' +
'}' +
'}).addTo(map);' +
'selectedLayers.addLayer(markers);' +
'markers.eachLayer(function(layer) {' +
'const props = layer.feature.properties;' +
'var imgContent = getImg(layer, props);' +
'var vidContent = getVid(layer, props);' +
'var audioContent = getAudio(layer, props);' +
'chapterContent.push("<div class=\'story\' order=\'" + props.order + "\' data-point=\'" + props.Y + "," + props.X + "\'><b><h2>" + props.name + "</h2></b><span class=\'br\'></span><h3>" + props.chapter + "<span class=\'br\'></span>" + "</h3><span class=\'brmedium\'></span><h3>" + props.description + "</h3>" + imgContent + vidContent + audioContent + "<span class=\'brmedium\'></span></div>");' +
'});' +
'const scrollEndHeight = 200;' +
'chapterContent.push("<div><b><h2>See the Code</h2></b><span class=\'br\'></span><h3>To see the code for this map, please go to: <a href=\'https://github.com/jebowe3/CSV-2-Storymap\'>CSV-2-Storymap</a>.</h3><p style=\'padding-bottom:" + scrollEndHeight + "px\'></p></div>");' +
'info.innerHTML = chapterContent.join("<span class=\'br\'></span>");' +
'const winDiff = getWinDiff();' +
"$('#chapter-select')[0].onchange = function(e) {" +
'filterMarkers(markers);' +
'};' +
'scrollMarkers(markers, winDiff);' +
'scrollOpenPopup(markers);' +
'};' +
'function scrollMarkers(markers, winDiff) { ' +
'map.fitBounds(markers.getBounds(), {paddingTopLeft: [document.getElementById(\'sidebar\').getBoundingClientRect().right,0]});' +
'let scrollTimeout = null;' +
'$(\'.sidebar-content\').on("scroll", function(e) {' +
'scrollOrds.length = 0;' +
'$(\'.story\').each(function() {' +
'const midLine = $(window).height() / 2;' +
'const divStart = $(this).offset().top;' +
'const divEnd = divStart + $(this)[0].offsetHeight;' +
'const lat = $(this)[0].getAttribute(\'data-point\').split(\',\')[0];' +
'const lng = $(this)[0].getAttribute(\'data-point\').split(\',\')[1];' +
'const order = $(this)[0].getAttribute(\'order\');' +
'if (divStart < midLine && divEnd > midLine) {' +
'$(this)[0].style.backgroundColor = "#EFEEEC";' +
'points.splice(0, points.length - 1);' +
'const point = $(this)[0].getAttribute(\'data-point\');' +
'points.push(point);' +
'orders.push(order);' +
'if (points[0] != points[1] || points[0] == points[1] && orders[0] != orders[1]) {' +
'if (scrollTimeout) clearTimeout(scrollTimeout);' +
'scrollTimeout = setTimeout(function() {' +
'map.flyTo([lat, lng - winDiff], markerZoom);' +
'}, 200);' +
'}' +
'scrollOrds.push(order);' +
'} else {' +
'$(this)[0].style.backgroundColor = "white";' +
'}' +
'window.addEventListener(\'resize\', function() {' +
'const resizeWinDiff = getWinDiffResize();' +
'if (divStart < midLine && divEnd > midLine) {' +
'map.flyTo([lat, lng - resizeWinDiff], markerZoom);' +
'}' +
'});' +
'});' +
'});' +
'};' +
'function scrollOpenPopup(markers) {' +
'let popupTimeout = null;' +
'$(\'.sidebar-content\').on("scroll", function() {' +
'if (popupTimeout) clearTimeout(popupTimeout);' +
'popupTimeout = setTimeout(function() {' +
'markers.eachLayer(function(layer){' +
'const markerOrds = layer.feature.properties.order;' +
'if (markerOrds === scrollOrds[0]) {' +
'layer.fire(\'click\');' +
'layer.setZIndexOffset(1000);' +
'} else {' +
'layer.setZIndexOffset(0);' +
'}' +
'});' +
'}, 200);' +
'});' +
'};' +
'function filterMarkers(markers) {' +
'chapterContent.length = 0;' +
'info.innerHTML = chapterContent.join("");' +
'chapterSelected.length = 0;' +
'selectedLayers.clearLayers();' +
'chapterSelected.push($(\'#chapter-select\')[0].value);' +
'markers.eachLayer(function(layer) {' +
'const props = layer.feature.properties;' +
'var imgContent = getImg(layer, props);' +
'var vidContent = getVid(layer, props);' +
'var audioContent = getAudio(layer, props);' +
'if (chapterSelected[0] == props.chapter) {' +
'selectedLayers.addLayer(layer);' +
'chapterContent.push("<div class=\'story\' order=\'" + props.order + "\' data-point=\'" + props.Y + "," + props.X + "\'><b><h2>" + props.name + "</h2></b><span class=\'br\'></span><h3>" + props.chapter + "<span class=\'br\'></span>" + "</h3><span class=\'brmedium\'></span><h3>" + props.description + "</h3>" + imgContent + vidContent + audioContent + "<span class=\'brmedium\'></span></div>");' +
'} else if (chapterSelected[0] == 0) {' +
'selectedLayers.addLayer(layer);' +
'chapterContent.push("<div class=\'story\' order=\'" + props.order + "\' data-point=\'" + props.Y + "," + props.X + "\'><b><h2>" + props.name + "</h2></b><span class=\'br\'></span><h3>" + props.chapter + "<span class=\'br\'></span>" + "</h3><span class=\'brmedium\'></span><h3>" + props.description + "</h3>" + imgContent + vidContent + audioContent + "<span class=\'brmedium\'></span></div>");' +
'} else {' +
'selectedLayers.removeLayer(layer);' +
'}' +
'});' +
'const scrollEndHeight = 200;' +
'chapterContent.push(\"<div><b><h2>See the Code</h2></b><span class=\'br\'></span><h3>To see the code for this map, please go to: <a href=\'https://github.com/jebowe3/CSV-2-Storymap\'>CSV-2-Storymap</a>.</h3><p style=\'padding-bottom:\" + scrollEndHeight + \"px\'></p></div>\");' +
'info.innerHTML = chapterContent.join("<span class=\'br\'></span>");' +
'map.fitBounds(markers.getBounds(), {paddingTopLeft: [document.getElementById(\'sidebar\').getBoundingClientRect().right,0]});' +
'};' +
'function getWinDiff() {' +
'map.setZoom(markerZoom);' +
'const rightLng = map.getBounds().getNorthEast().lng;' +
'const leftLng = map.containerPointToLatLng([document.getElementById(\'sidebar\').getBoundingClientRect().right,0]).lng;' +
'const centerLng = leftLng + ((rightLng - leftLng) / 2);' +
'const winDiff = centerLng - L.latLng(map.getCenter()).lng;' +
'map.setZoom(2);' +
'return winDiff;' +
'};' +
'function getWinDiffResize() {' +
'const rightLng = map.getBounds().getNorthEast().lng;' +
'const leftLng = map.containerPointToLatLng([document.getElementById(\'sidebar\').getBoundingClientRect().right,0]).lng;' +
'const centerLng = leftLng + ((rightLng - leftLng) / 2);' +
'const winDiff = centerLng - L.latLng(map.getCenter()).lng;' +
'return winDiff;' +
'};' +
'function getImg(layer, props) {' +
'let siteImage = props.image_url;' +
'let imgDesc = props.image_description;' +
'siteImage = siteImage || undefined;' +
'if (siteImage != undefined) {' +
'if (imgDesc != "") {' +
'var imgContent = "<span class=\'brmedium\'></span><img class=\'resize\' src=\'" + props.image_url + "\'" + "/><span class=\'brmedium\'></span><h3>" + props.image_description + "</h3>";' +
'} else {' +
'var imgContent = "<span class=\'brmedium\'></span><img class=\'resize\' src=\'" + props.image_url + "\'" + "/>";' +
'}' +
'} else {' +
'var imgContent = "";' +
'};' +
'return imgContent;' +
'};' +
'function getVid(layer, props) {' +
'let siteVideo = props.video_url;' +
'let vidDesc = props.video_description;' +
'let regExp = /^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|\\&v=)([^#\\&\\?]*).*/;' +
'let match = siteVideo.match(regExp);' +
'if (siteVideo != "") {' +
'if (vidDesc != "") {' +
'if (match && match[2].length == 11) {' +
'var vidContent = "<span class=\'brmedium\'></span><iframe class=\'resize\' src=\'" + \'https://www.youtube.com/embed/\' + match[2] + \'?autoplay=0\' + "\'>" + "</iframe><span class=\'brmedium\'></span><h3>" + props.video_description + "</h3>";' +
'} else {' +
'var vidContent = "<span class=\'brmedium\'></span><video class=\'resize\' controls>" + "<source src=\'" + siteVideo + "\' type=\'video/mp4\'>" + "</video><span class=\'brmedium\'></span><h3>" + props.video_description + "</h3>";' +
'}' +
'} else {' +
'if (match && match[2].length == 11) {' +
'var vidContent = "<span class=\'brmedium\'></span><iframe class=\'resize\' src=\'" + \'https://www.youtube.com/embed/\' + match[2] + \'?autoplay=0\' + "\'>" + "</iframe>";' +
'} else {' +
'var vidContent = "<span class=\'brmedium\'></span><video class=\'resize\' controls>" + "<source src=\'" + siteVideo + "\' type=\'video/mp4\'>" + "</video>";' +
'}' +
'}' +
'} else {' +
'var vidContent = "";' +
'};' +
'return vidContent;' +
'};' +
'function getAudio(layer, props) {' +
'let siteAudio = props.audio_url;' +
'let audioDesc = props.audio_description;' +
'siteAudio = siteAudio || undefined;' +
'if (siteAudio != undefined) {' +
'if (audioDesc != "") {' +
'var audioContent = "<span class=\'brmedium\'></span><audio controls class=\'resize\' src=\'" + props.audio_url + "\'" + "type=\'audio/mpeg\'></audio><span class=\'brmedium\'></span><h3>" + props.audio_description + "</h3>";' +
'} else {' +
'var audioContent = "<span class=\'brmedium\'></span><audio controls class=\'resize\' src=\'" + props.audio_url + "\'" + "type=\'audio/mpeg\'></audio>";' +
'}' +
'} else {' +
'var audioContent = "";' +
'};' +
'return audioContent;' +
'};' +
'<\/script>' +
'<\/body>' +
'</html>';
const a = document.createElement('a');
const linkText = document.createTextNode("");
a.appendChild(linkText);
a.id = 'link';
a.style.visibility = "hidden";
a.title = "my title text";
a.href = "data:text/html," + encodeURIComponent(html);
a.download = "index.html";
document.getElementById('download-button').appendChild(a);
}; // end downloadCode function
</script>
</body>
</html>