diff --git a/src/app.css b/src/app.css
new file mode 100644
index 0000000..f2ad8c5
--- /dev/null
+++ b/src/app.css
@@ -0,0 +1,73 @@
+body {
+ margin: 0;
+ padding: 0;
+ font-family: 'PT Mono', monospace;
+ /* font-family: 'Special Elite', cursive !important */
+}
+
+#map {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 100%;
+}
+
+.footer {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ /* Set the fixed height of the footer here */
+ height: 30px;
+ line-height: 30px; /* Vertically center the text there */
+ background-color: #f5f5f5;
+ font-size: 12px
+}
+
+/* removing footer on mobile */
+@media screen and (max-width: 400px) {
+ .footer {
+ display:none
+ }
+}
+
+/* big modal */
+.modal-lg {
+ max-width:1140px !important;
+}
+
+.mapboxgl-map{
+ font-family: 'PT Mono', monospace;
+ /* font-family: 'Special Elite', cursive !important */
+}
+
+.mapboxgl-ctrl-logo{
+ display: none !important;
+}
+
+
+/* paper plane animation */
+#canvas3d{
+ display:none;
+ transition: all .3s;
+ position:fixed;
+ height:100vh;
+ width:100vw;
+ background-color: rgba(0,0,0,0);
+ z-index:9999;
+ margin-top: 50vh; /* poussé de la moitié de hauteur de viewport */
+ transform: translateY(-50%) translateX(-50%); /* tiré de la moitié de sa propre hauteur */
+ margin-left: 50vw; /* poussé de la moitié de hauteur de viewport */
+}
+
+/* qill text editor */
+#messageTextArea {
+ min-height: 200px;
+ border-radius: 0px 0px .25em .25em !important;
+ border-color: #ced4da !important
+}
+
+.ql-toolbar{
+ border-radius: .25em .25em 0px 0px !important;
+ border-color: #ced4da !important
+}
+
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..c84ea25
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,4 @@
+var config = {
+ mapboxKey: 'pk.eyJ1IjoibGVvLWlnYSIsImEiOiJjazY2bGV3MTYxMjV3M25sMmdtNWluM2wzIn0.2THSqD6nz9OhE0Xsjnbw1g',
+ owmKey: "39d5c3be709ba4f5b6230cdffdedd2ac",
+}
\ No newline at end of file
diff --git a/src/geocoder.js b/src/geocoder.js
new file mode 100644
index 0000000..2c1c5cb
--- /dev/null
+++ b/src/geocoder.js
@@ -0,0 +1,67 @@
+let langGeocoder = "fr"
+
+// Expediteur
+var expeLocList = []
+var expeLoc;
+$('#expeGeocoderPhoton').typeahead({
+ source: function (query, process) {
+ expeLocList = []
+ var url = "https://photon.komoot.io/api?q="+query+"&lang="+langGeocoder+"&limit=4&osm_tag=place:city&osm_tag=place:town&osm_tag=place:village&osm_tag=place:municipality";
+ var res = []
+ return $.getJSON(url,function(data){
+ for (var i in data.features){
+ res.push(data.features[i].properties.name+" ("+data.features[i].properties.state+", "+data.features[i].properties.country+")")
+ expeLocList.push({
+ name: data.features[i].properties.name,
+ center: data.features[i].geometry.coordinates,
+ fullname: data.features[i].properties.name+" ("+data.features[i].properties.state+", "+data.features[i].properties.country+")"
+ })
+ }
+ return process(res)
+ })
+ },
+ matcher: function(item){ //needed when spelling not exact
+ return true
+ },
+
+ afterSelect: function (obj) {
+ for (var i in expeLocList){
+ if (expeLocList[i].fullname == obj){
+ expeLoc = expeLocList[i]
+ }
+ }
+ }
+})
+
+//Destinataire
+var destLocList = []
+var destLoc;
+$('#destGeocoderPhoton').typeahead({
+ source: function (query, process) {
+ destLocList = []
+ var url = "https://photon.komoot.io/api?q="+query+"&lang="+langGeocoder+"&limit=4&osm_tag=place:city&osm_tag=place:town&osm_tag=place:village&osm_tag=place:municipality";
+ var res = []
+ return $.getJSON(url,function(data){
+ for (var i in data.features){
+ res.push(data.features[i].properties.name+" ("+data.features[i].properties.state+", "+data.features[i].properties.country+")")
+ destLocList.push({
+ name: data.features[i].properties.name,
+ center: data.features[i].geometry.coordinates,
+ fullname: data.features[i].properties.name+" ("+data.features[i].properties.state+", "+data.features[i].properties.country+")"
+ })
+ }
+ return process(res)
+ })
+ },
+ matcher: function(item){ //needed when spelling not exact
+ return true
+ },
+
+ afterSelect: function (obj) {
+ for (var i in destLocList){
+ if (destLocList[i].fullname == obj){
+ destLoc = destLocList[i]
+ }
+ }
+ }
+})
\ No newline at end of file
diff --git a/src/map.js b/src/map.js
new file mode 100644
index 0000000..e6a34fa
--- /dev/null
+++ b/src/map.js
@@ -0,0 +1,266 @@
+mapboxgl.accessToken = config.mapboxKey;
+const owmKey = config.owmKey
+
+var map = new mapboxgl.Map({
+ container: 'map',
+ style: {
+ "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf",
+ "light": {
+ "anchor": "viewport",
+ "color": "white",
+ "intensity": 0.1
+ },
+ 'version': 8,
+ 'sources': {
+ 'default-background': {
+ 'type': 'raster',
+ 'tiles': [
+ //'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/VIIRS_CityLights_2012/default//GoogleMapsCompatible_Level8/{z}/{y}/{x}.jpg'
+ //'https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{z}/{y}/{x}'
+ //'https://stamen-tiles-b.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg'
+ 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
+ ],
+ 'tileSize': 256
+ }
+ },
+ 'layers': [{
+ id: 'background',
+ type: 'background',
+ paint: {
+ 'background-color': '#ffffff'
+ }
+ },
+ {
+ 'id': 'default-background',
+ 'type': 'raster',
+ 'source': 'default-background',
+ 'minzoom': 0,
+ 'maxzoom': 18
+ }
+ ]
+ },
+ center: [0, 20],
+ zoom: 2,
+ attributionControl: false
+
+});
+//prevent world duplication
+map.setRenderWorldCopies(status === 'false')
+
+map.on('load', function () {
+ //displaying clouds from OpenWeatherMap free API not enough
+
+ // map.addSource('clouds', {
+ // 'type': 'raster',
+ // 'tiles': [
+ // 'https://tile.openweathermap.org/map/clouds_new/{z}/{x}/{y}.png?appid='+owmKey+''
+ // ],
+ // 'tileSize': 256
+ // });
+
+ // map.addLayer({
+ // 'id': 'clouds',
+ // 'type': 'raster',
+ // 'source': 'clouds',
+ // 'minzoom': 0,
+ // 'maxzoom': 10,
+ // 'paint':{
+ // 'raster-opacity':0.5
+ // }
+ // });
+
+ //loading icons and colors, too lazy to make a loop, to change.
+ map.loadImage('src/img/plane-FFE419.png',
+ function (error, image) {
+ if (error) throw error;
+ map.addImage('plane-FFE419', image);
+ })
+ map.loadImage('src/img/plane-66FF00.png',
+ function (error, image) {
+ if (error) throw error;
+ map.addImage('plane-66FF00', image);
+ })
+ map.loadImage('src/img/plane-FF5555.png',
+ function (error, image) {
+ if (error) throw error;
+ map.addImage('plane-FF5555', image);
+ })
+ var colors = ["FFE419","66FF00","FF5555"]
+
+ //get now in GMT+0
+ var nowGMTSeconds = moment.tz(new Date(), "Europe/London").unix()
+
+ //get the planes from firestore
+ db.collection("planes").where("deliverySecondsServer", ">", nowGMTSeconds).get().then((querySnapshot) => { //change query to get only the ones who are still flying
+ querySnapshot.forEach((doc) => {
+ var id = doc.id
+ var data = doc.data()
+ //creating arcs
+ var line = turf.lineString([data.startCoordo,data.destCoordo])
+ var lineDistance = turf.length(line);
+ var arc = [];
+ var steps = 500;
+ for (var i = 0; i < lineDistance; i += lineDistance / steps) {
+ var segment = turf.along(line, i);
+ arc.push(segment.geometry.coordinates);
+ }
+ line.geometry.coordinates = arc;
+
+ //random color
+ var randomColor = colors[Math.floor(Math.random() * colors.length)];
+
+ map.addSource('route-'+id+'', {
+ 'type': 'geojson',
+ 'data': line
+ });
+ map.addLayer({
+ 'id': 'route-'+id+'',
+ 'source': 'route-'+id+'',
+ 'type': 'line',
+ 'paint': {
+ 'line-width': 2,
+ 'line-color': '#'+randomColor
+ }
+ });
+ map.setLayoutProperty('route-'+id+'', 'visibility', 'none');
+
+
+ //creating and animating planes
+
+ // calculating area already travelled
+ var dateStart = moment(data.sentDate[0])
+ var dateEnd = moment(data.deliveryDate[0])
+ var dateNow = moment(new Date())
+ var totalSeconds = dateEnd.diff(dateStart,'seconds');
+ var travelledSeconds = dateNow.diff(dateStart,'seconds')
+ var travelRatio = travelledSeconds/totalSeconds
+ var lineDistance = turf.length(line);
+ var currentPosition = turf.along(line, lineDistance*travelRatio,{units: 'kilometers'})
+ var plane = turf.point(currentPosition.geometry.coordinates,data)
+
+ //calculating bearing based on two points, one just before and one just after
+ var positionBefore = turf.along(line, (lineDistance*travelRatio)-0.00001,{units: 'kilometers'})
+ var positionAfter = turf.along(line, (lineDistance*travelRatio)+0.00001,{units: 'kilometers'})
+ plane.properties.bearing = turf.bearing(
+ positionBefore,
+ positionAfter
+ );
+
+ map.addSource('plane-'+id+'', {
+ 'type': 'geojson',
+ 'data': plane
+ });
+ map.addLayer({
+ 'id': 'plane-'+id+'',
+ 'source': 'plane-'+id+'',
+ 'type': 'symbol',
+ 'layout': {
+ 'icon-image': 'plane-'+randomColor+'',
+ 'icon-rotate': ['get', 'bearing'],
+ 'icon-size':0.05,
+ 'icon-rotation-alignment': 'map',
+ 'icon-allow-overlap': true,
+ 'icon-ignore-placement': true
+ }
+ });
+
+ //animating
+ function animate(){
+ var dateStart = moment(data.sentDate[0])
+ var dateEnd = moment(data.deliveryDate[0])
+ var dateNow = moment(new Date())
+ var totalSeconds = dateEnd.diff(dateStart,'seconds');
+ var travelledSeconds = dateNow.diff(dateStart,'seconds')
+ var travelRatio = travelledSeconds/totalSeconds
+ var lineDistance = turf.length(line);
+ var currentPosition = turf.along(line, lineDistance*travelRatio,{units: 'kilometers'})
+ var plane = turf.point(currentPosition.geometry.coordinates,data)
+
+ //calculating bearing based on two points, one just before and one just after
+ var positionBefore = turf.along(line, (lineDistance*travelRatio)-0.00001,{units: 'kilometers'})
+ var positionAfter = turf.along(line, (lineDistance*travelRatio)+0.00001,{units: 'kilometers'})
+ plane.properties.bearing = turf.bearing(
+ positionBefore,
+ positionAfter
+ );
+ map.getSource('plane-'+id+'').setData(plane);
+ var nowGMTSeconds = moment.tz(new Date(), "Europe/London").unix()
+ //removing the plane from map if arrived
+ if (data.deliverySecondsServer < nowGMTSeconds){
+ map.setLayoutProperty('plane-'+id+'', 'visibility', 'none');
+ }
+ requestAnimationFrame(animate)
+ }
+ // uncomment to animate below, still some clipping issues
+ //animate()
+
+ //on click on a plane: get popup + display route
+ map.on('click', 'plane-'+id+'', function (e) {
+ var coordinates = e.features[0].geometry.coordinates.slice();
+ var prop = e.features[0].properties;
+ var sentDate = JSON.parse(prop.sentDate)[0]
+ var deliveryDate = JSON.parse(prop.deliveryDate)[0]
+ while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
+ coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
+ }
+ //displying route
+ map.setLayoutProperty('route-'+id+'', 'visibility', 'visible');
+
+ var popup = new mapboxgl.Popup()
+ .setLngLat(coordinates)
+ .setHTML("Origine: "+prop.startName+"
"+sentDate+"
Destination: "+prop.destName+"
"+deliveryDate+"")
+ .addTo(map);
+ //removing route
+ popup.on('close', function(){
+ map.setLayoutProperty('route-'+id+'', 'visibility', 'none');
+ });
+
+ });
+ // Change the cursor to a pointer when the mouse is over the places layer.
+ map.on('mouseenter', 'plane-'+id+'', function () {
+ map.getCanvas().style.cursor = 'pointer';
+ });
+
+ // Change it back to a pointer when it leaves.
+ map.on('mouseleave', 'plane-'+id+'', function () {
+ map.getCanvas().style.cursor = '';
+ });
+
+ //adapt icon size to zoom level
+ map.on('zoom', function() {
+ map.setLayoutProperty('plane-'+id+'', 'icon-size', (map.getZoom())*0.025);
+ });
+
+
+ //URL Parameter : zooming to ID and opening popup when url in format avion-poe.me?plane=id
+ var queryString = window.location.search;
+ queryString = queryString.substring(7)
+ if(id == queryString){
+ map.setLayoutProperty('route-'+id+'', 'visibility', 'visible');
+ var bboxTravel = turf.bbox(line);
+ var bounds = [[bboxTravel[0],bboxTravel[1]],[bboxTravel[2],bboxTravel[3]]]
+ map.fitBounds(bounds, {
+ padding: 65,
+ speed: 0.5, // make the flying slow
+ curve: 0.8, // change the speed at which it zooms out
+ // This can be any easing function: it takes a number between
+ // 0 and 1 and returns another number between 0 and 1.
+ easing: function(
+ t) {
+ return t;
+ },
+ });
+ var sentDate = plane.properties.sentDate[0]
+ var deliveryDate = plane.properties.deliveryDate[0]
+ var popup = new mapboxgl.Popup()
+ .setLngLat(currentPosition.geometry.coordinates)
+ .setHTML("Origine: "+plane.properties.startName+"
"+sentDate+"
Destination: "+plane.properties.destName+"
"+deliveryDate+"")
+ .addTo(map);
+ //removing route
+ popup.on('close', function(){
+ map.setLayoutProperty('route-'+id+'', 'visibility', 'none');
+ });
+ }
+ });
+ });
+})
\ No newline at end of file
diff --git a/src/newPlane.js b/src/newPlane.js
new file mode 100644
index 0000000..e751c59
--- /dev/null
+++ b/src/newPlane.js
@@ -0,0 +1,111 @@
+// init text editor in modal
+var quill = new Quill('#messageTextArea', {
+ modules: {
+ toolbar: [
+ [{ 'size': ['small', false, 'large'] }],
+ [{ 'font': [] }],
+ ['bold', 'italic', 'underline','strike'],
+ [{ 'color': [] }],
+ ]
+ },
+ placeholder: '',
+ theme: 'snow' // or 'bubble',
+});
+
+$("#sendNewPlane").on('click',function(){
+
+ //hiding navbar
+ $('.navbar-collapse').collapse('hide');
+ //hiding modal
+ $("#newPlaneModal").modal('hide')
+
+ var line = turf.lineString([expeLoc.center,destLoc.center]) //create line between start and end
+ var length = turf.length(line) //get length in Km
+ var speed = 30 //speed in km/h
+ var time = length/speed //time to delivery in hours
+
+
+ //get the local time with TZ for starting point
+ $.getJSON('https://api.mapbox.com/v4/examples.4ze9z6tv/tilequery/'+expeLoc.center[0]+','+expeLoc.center[1]+'.json?access_token='+mapboxgl.accessToken+'',function(startTime){
+ var expeTimezone = startTime.features[0].properties.TZID;
+ var sentDate = moment.tz(new Date(), expeTimezone);
+ sentDate = [sentDate.format(),expeTimezone]
+
+ //get the local time with TZ for end point
+ $.getJSON('https://api.mapbox.com/v4/examples.4ze9z6tv/tilequery/'+destLoc.center[0]+','+destLoc.center[1]+'.json?access_token='+mapboxgl.accessToken+'',function(endTime){
+ var destTimezone = endTime.features[0].properties.TZID;
+ var endDate = moment.tz(new Date(), destTimezone);
+ var deliveryDate = endDate.add(time, 'hours') // adding hours
+ deliveryDate = [endDate.format(),destTimezone]
+
+ //storing the endDate in GMT+0 in seconds for the query
+ var deliverySecondsServer = moment.tz(new Date(), "Europe/London").add(time, 'hours').unix()
+
+ var publicMessage = $("#publicMessage").prop("checked")
+ var message = $("#messageTextArea").val()
+
+ var data = {
+ 'message':message,
+ 'public':publicMessage,
+ 'expeMail':$("#expeMail").val(),
+ 'destMail':$("#destMail").val(),
+ 'expeName':expeLoc.name,
+ 'destName':destLoc.name,
+ 'expeCoordo':expeLoc.center,
+ 'destCoordo':destLoc.center,
+ 'sentDate': sentDate,
+ 'deliveryDate':deliveryDate,
+ 'deliverySecondsServer':deliverySecondsServer,
+ }
+ //sending confirm mail
+
+ //sending notif mail
+
+ //scheduling mail
+
+ //adding the plane
+ var planes = db.collection('planes')
+ if (!publicMessage){ //removing message from DB if not public
+ data.message = ''
+ }
+ planes.add({
+ public: data.public,
+ message:data.message,
+ destName: data.destName,
+ destCoordo: data.destCoordo,
+ startName: data.expeName,
+ startCoordo: data.expeCoordo,
+ sentDate: data.sentDate,
+ deliveryDate: data.deliveryDate,
+ deliverySecondsServer: data.deliverySecondsServer
+ });
+
+ // creating image for plane, need to link it to the inputs tog et entered values.
+ var canvas = document.getElementById("blankCanvas");
+ var ctx = canvas.getContext("2d");
+ ctx.fillStyle = "rgb(255,255,255)";
+ ctx.fillRect(0,0,400,400);
+ ctx.fillStyle = "rgb(0,0,0)";
+ ctx.font = "9px Courier";
+ ctx.fillText("Message: .- / -- --- -. / ... . -. ... --..-- /",10,60);
+ ctx.fillText("Expediteur: leo.martine@yahoo.fr - Annemasse",10,300);
+ ctx.fillText("Destinataire: leo.martine@yahoo.fr - Lyon",10,320);
+ $("#front").attr('src',document.getElementById("blankCanvas").toDataURL())
+
+
+ //display and animate plane
+ $("#canvas3d").css('display','block')
+ animePlane()
+ //moving plane at the end
+ setInterval(function(){
+ $("#canvas3d").css('transition','transform 2500ms ease-in-out')
+ $("#canvas3d").css('transform','translate(50vw, -150vh)')
+ },4400);
+ //reloading page (to change)
+ setInterval(function(){
+ window.location.href = "http://127.0.0.1:8887/?plane=1FTMbm9V9eindbwoJ9uW";
+ },5500);
+
+ })
+ });
+})
\ No newline at end of file