From 17f94642089bf8eff2eb806fe22e314d1dd6d570 Mon Sep 17 00:00:00 2001 From: Nabeel Shahzad Date: Mon, 12 Mar 2018 17:30:52 -0500 Subject: [PATCH] first pass splitting js into commonjs modules --- Makefile | 2 +- package-lock.json | 3 + public/assets/system/js/phpvms.js | 3 +- public/assets/system/js/vendor.js | 496 ++++++++++++++++++ public/mix-manifest.json | 9 +- resources/js/admin/app.js | 11 + resources/js/admin/sidebar.js | 90 ++++ resources/js/bootstrap.js | 27 + resources/js/frontend/app.js | 5 + resources/js/maps/airspace_map.js | 33 ++ resources/js/maps/base_map.js | 71 +++ resources/js/maps/config.js | 4 + resources/js/maps/index.js | 17 + resources/js/maps/live_map.js | 124 +++++ resources/js/maps/route_map.js | 123 +++++ resources/views/admin/app.blade.php | 1 + .../layouts/default/flights/map.blade.php | 2 +- .../layouts/default/pireps/map.blade.php | 2 +- webpack.mix.js | 21 + 19 files changed, 1039 insertions(+), 5 deletions(-) create mode 100644 resources/js/admin/app.js create mode 100644 resources/js/admin/sidebar.js create mode 100644 resources/js/bootstrap.js create mode 100644 resources/js/frontend/app.js create mode 100644 resources/js/maps/airspace_map.js create mode 100644 resources/js/maps/base_map.js create mode 100644 resources/js/maps/config.js create mode 100644 resources/js/maps/index.js create mode 100644 resources/js/maps/live_map.js create mode 100644 resources/js/maps/route_map.js diff --git a/Makefile b/Makefile index 0c67bbd7..8ac026da 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ build: # This is to build all the stylesheets, etc .PHONY: build-assets build-assets: - npm update + #npm update npm run dev .PHONY: install diff --git a/package-lock.json b/package-lock.json index 5c8815d5..a00e9175 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,6 +2,9 @@ "requires": true, "lockfileVersion": 1, "dependencies": { + "Leaflet.Geodesic": { + "version": "git+https://git@github.com/henrythasler/Leaflet.Geodesic.git#7d710dd13020efb7c9901f5b3448a4541f507c56" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", diff --git a/public/assets/system/js/phpvms.js b/public/assets/system/js/phpvms.js index 5360d736..2ff3e1ad 100644 --- a/public/assets/system/js/phpvms.js +++ b/public/assets/system/js/phpvms.js @@ -3,7 +3,8 @@ * @type {{render_airspace_map, render_live_map, render_route_map}} */ -const phpvms = (function() { +let phpvms = {}; +phpvms.map = (function() { const PLAN_ROUTE_COLOR = '#36b123'; const ACTUAL_ROUTE_COLOR = '#172aea'; diff --git a/public/assets/system/js/vendor.js b/public/assets/system/js/vendor.js index 90aa9aef..15b3e209 100644 --- a/public/assets/system/js/vendor.js +++ b/public/assets/system/js/vendor.js @@ -41883,3 +41883,499 @@ module.exports = function() {} } }); })(); + +"use strict"; + +// This file is part of Leaflet.Geodesic. +// Copyright (C) 2017 Henry Thasler +// based on code by Chris Veness Copyright (C) 2014 https://github.com/chrisveness/geodesy +// +// Leaflet.Geodesic is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Leaflet.Geodesic is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Leaflet.Geodesic. If not, see . + + +/** Extend Number object with method to convert numeric degrees to radians */ +if (typeof Number.prototype.toRadians === "undefined") { + Number.prototype.toRadians = function() { + return this * Math.PI / 180; + }; +} + +/** Extend Number object with method to convert radians to numeric (signed) degrees */ +if (typeof Number.prototype.toDegrees === "undefined") { + Number.prototype.toDegrees = function() { + return this * 180 / Math.PI; + }; +} + +var INTERSECT_LNG = 179.999; // Lng used for intersection and wrap around on map edges + +L.Geodesic = L.Polyline.extend({ + options: { + color: "blue", + steps: 10, + dash: 1, + wrap: true + }, + + initialize: function(latlngs, options) { + this.options = this._merge_options(this.options, options); + this.options.dash = Math.max(1e-3, Math.min(1, parseFloat(this.options.dash) || 1)); + this.datum = {}; + this.datum.ellipsoid = { + a: 6378137, + b: 6356752.3142, + f: 1 / 298.257223563 + }; // WGS-84 + this._latlngs = this._generate_Geodesic(latlngs); + L.Polyline.prototype.initialize.call(this, this._latlngs, this.options); + }, + + setLatLngs: function(latlngs) { + this._latlngs = this._generate_Geodesic(latlngs); + L.Polyline.prototype.setLatLngs.call(this, this._latlngs); + }, + + /** + * Calculates some statistic values of current geodesic multipolyline + * @returns (Object} Object with several properties (e.g. overall distance) + */ + getStats: function() { + let obj = { + distance: 0, + points: 0, + polygons: this._latlngs.length + }, poly, points; + + for (poly = 0; poly < this._latlngs.length; poly++) { + obj.points += this._latlngs[poly].length; + for (points = 0; points < (this._latlngs[poly].length - 1); points++) { + obj.distance += this._vincenty_inverse(this._latlngs[poly][points], + this._latlngs[poly][points + 1]).distance; + } + } + return obj; + }, + + + /** + * Creates geodesic lines from geoJson. Replaces all current features of this instance. + * Supports LineString, MultiLineString and Polygon + * @param {Object} geojson - geosjon as object. + */ + geoJson: function(geojson) { + + let normalized = L.GeoJSON.asFeature(geojson); + let features = normalized.type === "FeatureCollection" ? normalized.features : [ + normalized + ]; + this._latlngs = []; + for (let feature of features) { + let geometry = feature.type === "Feature" ? feature.geometry : + feature, + coords = geometry.coordinates; + + switch (geometry.type) { + case "LineString": + this._latlngs.push(this._generate_Geodesic([L.GeoJSON.coordsToLatLngs( + coords, 0)])); + break; + case "MultiLineString": + case "Polygon": + this._latlngs.push(this._generate_Geodesic(L.GeoJSON.coordsToLatLngs( + coords, 1))); + break; + case "Point": + case "MultiPoint": + console.log("Dude, points can't be drawn as geodesic lines..."); + break; + default: + console.log("Drawing " + geometry.type + + " as a geodesic is not supported. Skipping..."); + } + } + L.Polyline.prototype.setLatLngs.call(this, this._latlngs); + }, + + /** + * Creates a great circle. Replaces all current lines. + * @param {Object} center - geographic position + * @param {number} radius - radius of the circle in metres + */ + createCircle: function(center, radius) { + let polylineIndex = 0; + let prev = { + lat: 0, + lng: 0, + brg: 0 + }; + let step; + + this._latlngs = []; + this._latlngs[polylineIndex] = []; + + let direct = this._vincenty_direct(L.latLng(center), 0, radius, this.options + .wrap); + prev = L.latLng(direct.lat, direct.lng); + this._latlngs[polylineIndex].push(prev); + for (step = 1; step <= this.options.steps;) { + direct = this._vincenty_direct(L.latLng(center), 360 / this.options + .steps * step, radius, this.options.wrap); + let gp = L.latLng(direct.lat, direct.lng); + if (Math.abs(gp.lng - prev.lng) > 180) { + let inverse = this._vincenty_inverse(prev, gp); + let sec = this._intersection(prev, inverse.initialBearing, { + lat: -89, + lng: ((gp.lng - prev.lng) > 0) ? -INTERSECT_LNG : INTERSECT_LNG + }, 0); + if (sec) { + this._latlngs[polylineIndex].push(L.latLng(sec.lat, sec.lng)); + polylineIndex++; + this._latlngs[polylineIndex] = []; + prev = L.latLng(sec.lat, -sec.lng); + this._latlngs[polylineIndex].push(prev); + } else { + polylineIndex++; + this._latlngs[polylineIndex] = []; + this._latlngs[polylineIndex].push(gp); + prev = gp; + step++; + } + } else { + this._latlngs[polylineIndex].push(gp); + prev = gp; + step++; + } + } + + L.Polyline.prototype.setLatLngs.call(this, this._latlngs); + }, + + /** + * Creates a geodesic Polyline from given coordinates + * Note: dashed lines are under work + * @param {Object} latlngs - One or more polylines as an array. See Leaflet doc about Polyline + * @returns (Object} An array of arrays of geographical points. + */ + _generate_Geodesic: function(latlngs) { + let _geo = [], _geocnt = 0; + + for (let poly = 0; poly < latlngs.length; poly++) { + _geo[_geocnt] = []; + let prev = L.latLng(latlngs[poly][0]); + for (let points = 0; points < (latlngs[poly].length - 1); points++) { + // use prev, so that wrapping behaves correctly + let pointA = prev; + let pointB = L.latLng(latlngs[poly][points + 1]); + if (pointA.equals(pointB)) { + continue; + } + let inverse = this._vincenty_inverse(pointA, pointB); + _geo[_geocnt].push(prev); + for (let s = 1; s <= this.options.steps;) { + let distance = inverse.distance / this.options.steps; + // dashed lines don't go the full distance between the points + let dist_mult = s - 1 + this.options.dash; + let direct = this._vincenty_direct(pointA, inverse.initialBearing, distance*dist_mult, this.options.wrap); + let gp = L.latLng(direct.lat, direct.lng); + if (Math.abs(gp.lng - prev.lng) > 180) { + let sec = this._intersection(pointA, inverse.initialBearing, { + lat: -89, + lng: ((gp.lng - prev.lng) > 0) ? -INTERSECT_LNG : INTERSECT_LNG + }, 0); + if (sec) { + _geo[_geocnt].push(L.latLng(sec.lat, sec.lng)); + _geocnt++; + _geo[_geocnt] = []; + prev = L.latLng(sec.lat, -sec.lng); + _geo[_geocnt].push(prev); + } else { + _geocnt++; + _geo[_geocnt] = []; + _geo[_geocnt].push(gp); + prev = gp; + s++; + } + } else { + _geo[_geocnt].push(gp); + // Dashed lines start a new line + if (this.options.dash < 1){ + _geocnt++; + // go full distance this time, to get starting point for next line + let direct_full = this._vincenty_direct(pointA, inverse.initialBearing, distance*s, this.options.wrap); + _geo[_geocnt] = []; + prev = L.latLng(direct_full.lat, direct_full.lng); + _geo[_geocnt].push(prev); + } + else prev = gp; + s++; + } + } + } + _geocnt++; + } + return _geo; + }, + + /** + * Vincenty direct calculation. + * based on the work of Chris Veness (https://github.com/chrisveness/geodesy) + * + * @private + * @param {number} initialBearing - Initial bearing in degrees from north. + * @param {number} distance - Distance along bearing in metres. + * @returns (Object} Object including point (destination point), finalBearing. + */ + + _vincenty_direct: function(p1, initialBearing, distance, wrap) { + var φ1 = p1.lat.toRadians(), + λ1 = p1.lng.toRadians(); + var α1 = initialBearing.toRadians(); + var s = distance; + + var a = this.datum.ellipsoid.a, + b = this.datum.ellipsoid.b, + f = this.datum.ellipsoid.f; + + var sinα1 = Math.sin(α1); + var cosα1 = Math.cos(α1); + + var tanU1 = (1 - f) * Math.tan(φ1), + cosU1 = 1 / Math.sqrt((1 + tanU1 * tanU1)), + sinU1 = tanU1 * cosU1; + var σ1 = Math.atan2(tanU1, cosα1); + var sinα = cosU1 * sinα1; + var cosSqα = 1 - sinα * sinα; + var uSq = cosSqα * (a * a - b * b) / (b * b); + var A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * + uSq))); + var B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))); + + var σ = s / (b * A), + σʹ, iterations = 0; + var sinσ, cosσ; + var cos2σM; + do { + cos2σM = Math.cos(2 * σ1 + σ); + sinσ = Math.sin(σ); + cosσ = Math.cos(σ); + var Δσ = B * sinσ * (cos2σM + B / 4 * (cosσ * (-1 + 2 * cos2σM * + cos2σM) - + B / 6 * cos2σM * (-3 + 4 * sinσ * sinσ) * (-3 + 4 * cos2σM * + cos2σM))); + σʹ = σ; + σ = s / (b * A) + Δσ; + } while (Math.abs(σ - σʹ) > 1e-12 && ++iterations); + + var x = sinU1 * sinσ - cosU1 * cosσ * cosα1; + var φ2 = Math.atan2(sinU1 * cosσ + cosU1 * sinσ * cosα1, (1 - f) * + Math.sqrt(sinα * sinα + x * x)); + var λ = Math.atan2(sinσ * sinα1, cosU1 * cosσ - sinU1 * sinσ * cosα1); + var C = f / 16 * cosSqα * (4 + f * (4 - 3 * cosSqα)); + var L = λ - (1 - C) * f * sinα * + (σ + C * sinσ * (cos2σM + C * cosσ * (-1 + 2 * cos2σM * cos2σM))); + + var λ2; + if (wrap) { + λ2 = (λ1 + L + 3 * Math.PI) % (2 * Math.PI) - Math.PI; // normalise to -180...+180 + } else { + λ2 = (λ1 + L); // do not normalize + } + + var revAz = Math.atan2(sinα, -x); + + return { + lat: φ2.toDegrees(), + lng: λ2.toDegrees(), + finalBearing: revAz.toDegrees() + }; + }, + + /** + * Vincenty inverse calculation. + * based on the work of Chris Veness (https://github.com/chrisveness/geodesy) + * + * @private + * @param {LatLng} p1 - Latitude/longitude of start point. + * @param {LatLng} p2 - Latitude/longitude of destination point. + * @returns {Object} Object including distance, initialBearing, finalBearing. + * @throws {Error} If formula failed to converge. + */ + _vincenty_inverse: function(p1, p2) { + var φ1 = p1.lat.toRadians(), + λ1 = p1.lng.toRadians(); + var φ2 = p2.lat.toRadians(), + λ2 = p2.lng.toRadians(); + + var a = this.datum.ellipsoid.a, + b = this.datum.ellipsoid.b, + f = this.datum.ellipsoid.f; + + var L = λ2 - λ1; + var tanU1 = (1 - f) * Math.tan(φ1), + cosU1 = 1 / Math.sqrt((1 + tanU1 * tanU1)), + sinU1 = tanU1 * cosU1; + var tanU2 = (1 - f) * Math.tan(φ2), + cosU2 = 1 / Math.sqrt((1 + tanU2 * tanU2)), + sinU2 = tanU2 * cosU2; + + var λ = L, + λʹ, iterations = 0; + var cosSqα, sinσ, cos2σM, cosσ, σ, sinλ, cosλ; + do { + sinλ = Math.sin(λ); + cosλ = Math.cos(λ); + var sinSqσ = (cosU2 * sinλ) * (cosU2 * sinλ) + (cosU1 * sinU2 - + sinU1 * cosU2 * cosλ) * (cosU1 * sinU2 - sinU1 * cosU2 * cosλ); + sinσ = Math.sqrt(sinSqσ); + if (sinσ == 0) return 0; // co-incident points + cosσ = sinU1 * sinU2 + cosU1 * cosU2 * cosλ; + σ = Math.atan2(sinσ, cosσ); + var sinα = cosU1 * cosU2 * sinλ / sinσ; + cosSqα = 1 - sinα * sinα; + cos2σM = cosσ - 2 * sinU1 * sinU2 / cosSqα; + if (isNaN(cos2σM)) cos2σM = 0; // equatorial line: cosSqα=0 (§6) + var C = f / 16 * cosSqα * (4 + f * (4 - 3 * cosSqα)); + λʹ = λ; + λ = L + (1 - C) * f * sinα * (σ + C * sinσ * (cos2σM + C * cosσ * (- + 1 + 2 * cos2σM * cos2σM))); + } while (Math.abs(λ - λʹ) > 1e-12 && ++iterations < 100); + if (iterations >= 100) { + console.log("Formula failed to converge. Altering target position."); + return this._vincenty_inverse(p1, { + lat: p2.lat, + lng: p2.lng - 0.01 + }); + // throw new Error('Formula failed to converge'); + } + + var uSq = cosSqα * (a * a - b * b) / (b * b); + var A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * + uSq))); + var B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))); + var Δσ = B * sinσ * (cos2σM + B / 4 * (cosσ * (-1 + 2 * cos2σM * + cos2σM) - + B / 6 * cos2σM * (-3 + 4 * sinσ * sinσ) * (-3 + 4 * cos2σM * + cos2σM))); + + var s = b * A * (σ - Δσ); + + var fwdAz = Math.atan2(cosU2 * sinλ, cosU1 * sinU2 - sinU1 * cosU2 * + cosλ); + var revAz = Math.atan2(cosU1 * sinλ, -sinU1 * cosU2 + cosU1 * sinU2 * + cosλ); + + s = Number(s.toFixed(3)); // round to 1mm precision + return { + distance: s, + initialBearing: fwdAz.toDegrees(), + finalBearing: revAz.toDegrees() + }; + }, + + + /** + * Returns the point of intersection of two paths defined by point and bearing. + * based on the work of Chris Veness (https://github.com/chrisveness/geodesy) + * + * @param {LatLon} p1 - First point. + * @param {number} brng1 - Initial bearing from first point. + * @param {LatLon} p2 - Second point. + * @param {number} brng2 - Initial bearing from second point. + * @returns {Object} containing lat/lng information of intersection. + * + * @example + * var p1 = LatLon(51.8853, 0.2545), brng1 = 108.55; + * var p2 = LatLon(49.0034, 2.5735), brng2 = 32.44; + * var pInt = LatLon.intersection(p1, brng1, p2, brng2); // pInt.toString(): 50.9078°N, 4.5084°E + */ + _intersection: function(p1, brng1, p2, brng2) { + // see http://williams.best.vwh.net/avform.htm#Intersection + + var φ1 = p1.lat.toRadians(), + λ1 = p1.lng.toRadians(); + var φ2 = p2.lat.toRadians(), + λ2 = p2.lng.toRadians(); + var θ13 = Number(brng1).toRadians(), + θ23 = Number(brng2).toRadians(); + var Δφ = φ2 - φ1, + Δλ = λ2 - λ1; + + var δ12 = 2 * Math.asin(Math.sqrt(Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / + 2))); + if (δ12 == 0) return null; + + // initial/final bearings between points + var θ1 = Math.acos((Math.sin(φ2) - Math.sin(φ1) * Math.cos(δ12)) / + (Math.sin(δ12) * Math.cos(φ1))); + if (isNaN(θ1)) θ1 = 0; // protect against rounding + var θ2 = Math.acos((Math.sin(φ1) - Math.sin(φ2) * Math.cos(δ12)) / + (Math.sin(δ12) * Math.cos(φ2))); + var θ12, θ21; + if (Math.sin(λ2 - λ1) > 0) { + θ12 = θ1; + θ21 = 2 * Math.PI - θ2; + } else { + θ12 = 2 * Math.PI - θ1; + θ21 = θ2; + } + + var α1 = (θ13 - θ12 + Math.PI) % (2 * Math.PI) - Math.PI; // angle 2-1-3 + var α2 = (θ21 - θ23 + Math.PI) % (2 * Math.PI) - Math.PI; // angle 1-2-3 + + if (Math.sin(α1) == 0 && Math.sin(α2) == 0) return null; // infinite intersections + if (Math.sin(α1) * Math.sin(α2) < 0) return null; // ambiguous intersection + + //α1 = Math.abs(α1); + //α2 = Math.abs(α2); + // ... Ed Williams takes abs of α1/α2, but seems to break calculation? + + var α3 = Math.acos(-Math.cos(α1) * Math.cos(α2) + + Math.sin(α1) * Math.sin(α2) * Math.cos(δ12)); + var δ13 = Math.atan2(Math.sin(δ12) * Math.sin(α1) * Math.sin(α2), + Math.cos(α2) + Math.cos(α1) * Math.cos(α3)); + var φ3 = Math.asin(Math.sin(φ1) * Math.cos(δ13) + + Math.cos(φ1) * Math.sin(δ13) * Math.cos(θ13)); + var Δλ13 = Math.atan2(Math.sin(θ13) * Math.sin(δ13) * Math.cos(φ1), + Math.cos(δ13) - Math.sin(φ1) * Math.sin(φ3)); + var λ3 = λ1 + Δλ13; + λ3 = (λ3 + 3 * Math.PI) % (2 * Math.PI) - Math.PI; // normalise to -180..+180º + + return { + lat: φ3.toDegrees(), + lng: λ3.toDegrees() + }; + }, + + /** + * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1 + * @param obj1 + * @param obj2 + * @returns obj3 a new object based on obj1 and obj2 + */ + _merge_options: function(obj1, obj2) { + let obj3 = {}; + for (let attrname in obj1) { + obj3[attrname] = obj1[attrname]; + } + for (let attrname in obj2) { + obj3[attrname] = obj2[attrname]; + } + return obj3; + } +}); + +L.geodesic = function(latlngs, options) { + return new L.Geodesic(latlngs, options); +}; diff --git a/public/mix-manifest.json b/public/mix-manifest.json index c8d8bac0..331cfbc4 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -1,8 +1,15 @@ { + "/js/admin/app.js": "/js/admin/app.js?id=1cc53441cb171210435d", + "/js/frontend/app.js": "/js/frontend/app.js?id=650885634c97f0fe2deb", "/assets/admin/vendor/paper-dashboard.css": "/assets/admin/vendor/paper-dashboard.css?id=3bbf7dd2a80739ab63b9", "/assets/frontend/css/now-ui-kit.css": "/assets/frontend/css/now-ui-kit.css?id=9923ce002ceafb1d740a", + "/js/admin/vendor.js": "/js/admin/vendor.js?id=1c5ddb087f24b16da40f", + "/js/admin/manifest.js": "/js/admin/manifest.js?id=ce6566a24afe6e358977", "/assets/fonts/glyphicons-halflings-regular.woff2": "/assets/fonts/glyphicons-halflings-regular.woff2?id=b5b5055c6d812c0f9f0d", "/assets/admin/fonts/glyphicons-halflings-regular.woff2": "/assets/admin/fonts/glyphicons-halflings-regular.woff2?id=b5b5055c6d812c0f9f0d", + "/assets/admin/js/icheck.js": "/assets/admin/js/icheck.js?id=14d0d9d9cae155580c5e", + "/assets/admin/js/bootstrap-editable.js": "/assets/admin/js/bootstrap-editable.js?id=48c6c28ec6f8b768f0d3", + "/assets/admin/js/bootstrap-editable.min.js": "/assets/admin/js/bootstrap-editable.min.js?id=3a6eeef3fbdb0d4cf5ba", "/assets/admin/img/clear.png": "/assets/admin/img/clear.png?id=0e92f4c3efc6988a3c96", "/assets/admin/img/loading.gif": "/assets/admin/img/loading.gif?id=90a4b76b4f11558691f6", "/assets/admin/css/vendor.min.css": "/assets/admin/css/vendor.min.css?id=cae3dbc399c60b06b967", @@ -10,7 +17,7 @@ "/assets/admin/css/blue.png": "/assets/admin/css/blue.png?id=753a3c0dec86d3a38d9c", "/assets/admin/css/blue@2x.png": "/assets/admin/css/blue@2x.png?id=97da23d47b838cbd4bef", "/assets/system/js/jquery.js": "/assets/system/js/jquery.js?id=6a07da9fae934baf3f74", - "/assets/system/js/vendor.js": "/assets/system/js/vendor.js?id=79d997fd74bdb7f37b0b", + "/assets/system/js/vendor.js": "/assets/system/js/vendor.js?id=0e6df5b264518fb37f48", "/assets/system/css/vendor.min.css": "/assets/system/css/vendor.min.css?id=23451051c1ac99cae5fb", "/assets/system/css/installer.css": "/assets/system/css/installer.css?id=1101b178b0383008aaa1", "/assets/system/js/installer-vendor.js": "/assets/system/js/installer-vendor.js?id=efb330ef732d2f856623" diff --git a/resources/js/admin/app.js b/resources/js/admin/app.js new file mode 100644 index 00000000..3273d61d --- /dev/null +++ b/resources/js/admin/app.js @@ -0,0 +1,11 @@ +/** + * Any functionality required for the admin app + */ + +require('./../bootstrap'); +require('eonasdan-bootstrap-datetimepicker'); + +require('./sidebar'); + +// Import the mapping function +window.phpvms.map = require('../maps/index'); diff --git a/resources/js/admin/sidebar.js b/resources/js/admin/sidebar.js new file mode 100644 index 00000000..a918ed87 --- /dev/null +++ b/resources/js/admin/sidebar.js @@ -0,0 +1,90 @@ + +const jquery = require('jquery'); + +const getStorage = function (key) { + const st = window.localStorage.getItem(key); + + console.log('storage: ', key, st); + if (_.isNil(st)) { + return { + "menu": [], + }; + } + + return JSON.parse(st); +}; + +const saveStorage = function (key, obj) { + console.log('save: ', key, obj); + window.localStorage.setItem(key, JSON.stringify(obj)); +}; + +const addItem = function (obj, item) { + + if (_.isNil(obj)) { + obj = []; + } + + const index = _.indexOf(obj, item); + if (index === -1) { + obj.push(item); + } + + return obj; +}; + +const removeItem = function (obj, item) { + + if (_.isNil(obj)) { + obj = []; + } + + const index = _.indexOf(obj, item); + if (index !== -1) { + console.log("removing", item); + obj.splice(index, 1); + } + + return obj; +}; + +jquery(document).ready(function () { + + $(".select2").select2(); + + let storage = getStorage("phpvms.admin"); + + // see what menu items should be open + for (let idx = 0; idx < storage.menu.length; idx++) { + const id = storage.menu[idx]; + const elem = jquery(".collapse#" + id); + elem.addClass("in").trigger("show.bs.collapse"); + + const caret = jquery("a." + id + " b"); + caret.addClass("pe-7s-angle-down"); + caret.removeClass("pe-7s-angle-right"); + } + + jquery(".collapse").on("hide.bs.collapse", function () { + console.log('hiding'); + const id = jquery(this).attr('id'); + const elem = jquery("a." + id + " b"); + elem.removeClass("pe-7s-angle-down"); + elem.addClass("pe-7s-angle-right"); + + removeItem(storage.menu, id); + saveStorage("phpvms.admin", storage); + }); + + jquery(".collapse").on("show.bs.collapse", function () { + console.log('showing'); + const id = jquery(this).attr('id'); + const caret = jquery("a." + id + " b"); + caret.addClass("pe-7s-angle-down"); + caret.removeClass("pe-7s-angle-right"); + + addItem(storage.menu, id); + saveStorage("phpvms.admin", storage); + }); + +}); diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js new file mode 100644 index 00000000..6b40e65c --- /dev/null +++ b/resources/js/bootstrap.js @@ -0,0 +1,27 @@ +/** + * Bootstrap any Javascript libraries required + */ + +window._ = require('lodash'); +window.Popper = require('popper.js').default; +window.$ = window.jquery = require('jquery'); +window.select2 = require('select2'); +window.pjax = require('pjax'); + +// Container for phpVMS specific functions +window.phpvms = { + +}; + +/** + * Configure Axios + */ +window.axios = require('axios'); +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; +let token = document.head.querySelector('meta[name="csrf-token"]'); + +if (token) { + window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; +} else { + console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); +} diff --git a/resources/js/frontend/app.js b/resources/js/frontend/app.js new file mode 100644 index 00000000..4304cb43 --- /dev/null +++ b/resources/js/frontend/app.js @@ -0,0 +1,5 @@ + +require('./../bootstrap'); + +// Import the mapping function +window.phpvms.map = require('../maps/index'); diff --git a/resources/js/maps/airspace_map.js b/resources/js/maps/airspace_map.js new file mode 100644 index 00000000..bf7f8bcc --- /dev/null +++ b/resources/js/maps/airspace_map.js @@ -0,0 +1,33 @@ + +const _ = require('lodash'); +const leaflet = require('leaflet'); + +import draw_base_map from './base_map'; + +/** + * Render a map with the airspace, etc around a given set of coords + * e.g, the airport map + * @param opts + */ +export default (opts) => { + opts = _.defaults(opts, { + render_elem: 'map', + overlay_elem: '', + lat: 0, + lon: 0, + zoom: 12, + layers: [], + set_marker: false, + }); + + let map = draw_base_map(opts); + const coords = [opts.lat, opts.lon]; + console.log('Applying coords', coords); + + map.setView(coords, opts.zoom); + if (opts.set_marker === true) { + leaflet.marker(coords).addTo(map); + } + + return map; +}; diff --git a/resources/js/maps/base_map.js b/resources/js/maps/base_map.js new file mode 100644 index 00000000..67b0bf40 --- /dev/null +++ b/resources/js/maps/base_map.js @@ -0,0 +1,71 @@ +const _ = require('lodash'); +const leaflet = require('leaflet'); + +export default (opts) => { + + opts = _.defaults(opts, { + render_elem: 'map', + center: [29.98139, -95.33374], + zoom: 5, + maxZoom: 10, + layers: [], + set_marker: false, + }); + + let feature_groups = []; + /*var openaip_airspace_labels = new leaflet.TileLayer.WMS( + "http://{s}.tile.maps.openaip.net/geowebcache/service/wms", { + maxZoom: 14, + minZoom: 12, + layers: 'openaip_approved_airspaces_labels', + tileSize: 1024, + detectRetina: true, + subdomains: '12', + format: 'image/png', + transparent: true + }); + + openaip_airspace_labels.addTo(map);*/ + + const opencyclemap_phys_osm = new leaflet.TileLayer( + 'http://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}.png?apikey=f09a38fa87514de4890fc96e7fe8ecb1', { + maxZoom: 14, + minZoom: 4, + format: 'image/png', + transparent: true + }); + + feature_groups.push(opencyclemap_phys_osm); + + /*const openaip_cached_basemap = new leaflet.TileLayer("http://{s}.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_basemap@EPSG%3A900913@png/{z}/{x}/{y}.png", { + maxZoom: 14, + minZoom: 4, + tms: true, + detectRetina: true, + subdomains: '12', + format: 'image/png', + transparent: true + }); + + feature_groups.push(openaip_cached_basemap); + */ + + const openaip_basemap_phys_osm = leaflet.featureGroup(feature_groups); + + let map = leaflet.map('map', { + layers: [openaip_basemap_phys_osm], + center: opts.center, + zoom: opts.zoom, + scrollWheelZoom: false, + }); + + const attrib = leaflet.control.attribution({position: 'bottomleft'}); + attrib.addAttribution("Thunderforest"); + attrib.addAttribution("openAIP"); + attrib.addAttribution("OpenStreetMap contributors"); + attrib.addAttribution("OpenWeatherMap"); + + attrib.addTo(map); + + return map; +}; diff --git a/resources/js/maps/config.js b/resources/js/maps/config.js new file mode 100644 index 00000000..7661fe1f --- /dev/null +++ b/resources/js/maps/config.js @@ -0,0 +1,4 @@ + +export let + PLAN_ROUTE_COLOR = '#36b123', + ACTUAL_ROUTE_COLOR = '#172aea'; diff --git a/resources/js/maps/index.js b/resources/js/maps/index.js new file mode 100644 index 00000000..6158f8e5 --- /dev/null +++ b/resources/js/maps/index.js @@ -0,0 +1,17 @@ +/** + * All of the functionality required for maps + */ + +window.L = require('leaflet'); +require('Leaflet.Geodesic'); +require('leaflet-rotatedmarker'); + +import render_airspace_map from './airspace_map'; +import render_live_map from './live_map'; +import render_route_map from './route_map'; + +export { + render_airspace_map, + render_live_map, + render_route_map, +}; diff --git a/resources/js/maps/live_map.js b/resources/js/maps/live_map.js new file mode 100644 index 00000000..9bef4f68 --- /dev/null +++ b/resources/js/maps/live_map.js @@ -0,0 +1,124 @@ + +const _ = require('lodash'); +const leaflet = require('leaflet'); + +import draw_base_map from './base_map'; +import {ACTUAL_ROUTE_COLOR} from './config'; + +/** + * Render the live map + * @param opts + * @private + */ +export default (opts) => { + + opts = _.defaults(opts, { + update_uri: '/api/acars', + pirep_uri: '/api/pireps/{id}/acars', + positions: null, + render_elem: 'map', + aircraft_icon: '/assets/img/acars/aircraft.png', + }); + + const map = draw_base_map(opts); + const aircraftIcon = leaflet.icon({ + iconUrl: opts.aircraft_icon, + iconSize: [42, 42], + iconAnchor: [21, 21], + }); + + let layerFlights = null; + let layerSelFlight = null; + let layerSelFlightFeature = null; + let layerSelFlightLayer = null; + + /** + * When a flight is clicked on, show the path, etc for that flight + * @param feature + * @param layer + */ + const onFlightClick = (feature, layer) => { + + const uri = opts.pirep_uri.replace('{id}', feature.properties.pirep_id); + + const flight_route = $.ajax({ + url: uri, + dataType: "json", + error: console.log + }); + + $.when(flight_route).done((routeJson) => { + if (layerSelFlight !== null) { + map.removeLayer(layerSelFlight); + } + + layerSelFlight = leaflet.geodesic([], { + weight: 7, + opacity: 0.9, + color: ACTUAL_ROUTE_COLOR, + wrap: false, + }).addTo(map); + + layerSelFlight.geoJson(routeJson.line); + + layerSelFlightFeature = feature; + layerSelFlightLayer = layer; + //map.fitBounds(layerSelFlight.getBounds()); + }); + }; + + const updateMap = () => { + + console.log('reloading flights from acars...'); + + /** + * AJAX UPDATE + */ + + let flights = $.ajax({ + url: opts.update_uri, + dataType: "json", + error: console.log + }); + + $.when(flights).done(function (flightGeoJson) { + + if (layerFlights !== null) { + layerFlights.clearLayers(); + } + + layerFlights = leaflet.geoJSON(flightGeoJson, { + onEachFeature: (feature, layer) => { + + layer.on({ + click: (e) => { + onFlightClick(feature, layer); + } + }); + + let popup_html = ""; + if (feature.properties && feature.properties.popup) { + popup_html += feature.properties.popup; + } + + layer.bindPopup(popup_html); + }, + pointToLayer: function (feature, latlon) { + return leaflet.marker(latlon, { + icon: aircraftIcon, + rotationAngle: feature.properties.heading + }); + } + }); + + layerFlights.addTo(map); + + if (layerSelFlight !== null) { + onFlightClick(layerSelFlightFeature, layerSelFlightLayer); + } + }); + }; + + updateMap(); + setInterval(updateMap, 10000); +}; diff --git a/resources/js/maps/route_map.js b/resources/js/maps/route_map.js new file mode 100644 index 00000000..657edcff --- /dev/null +++ b/resources/js/maps/route_map.js @@ -0,0 +1,123 @@ +const _ = require('lodash'); +const leaflet = require('leaflet'); + +import draw_base_map from './base_map'; +import {ACTUAL_ROUTE_COLOR, PLAN_ROUTE_COLOR} from './config'; + +/** + * Show some popup text when a feature is clicked on + * @param feature + * @param layer + */ +export const onFeaturePointClick = (feature, layer) => { + let popup_html = ""; + if (feature.properties && feature.properties.popup) { + popup_html += feature.properties.popup; + } + + layer.bindPopup(popup_html); +}; + +/** + * Show each point as a marker + * @param feature + * @param latlng + * @returns {*} + */ +export const pointToLayer = (feature, latlng) => { + return leaflet.circleMarker(latlng, { + radius: 12, + fillColor: "#ff7800", + color: "#000", + weight: 1, + opacity: 1, + fillOpacity: 0.8 + }); +}; + +/** + * + * @param opts + * @private + */ +export default (opts) => { + + opts = _.defaults(opts, { + route_points: null, + planned_route_line: null, + actual_route_points: null, + actual_route_line: null, + render_elem: 'map', + }); + + console.log(opts); + + let map = draw_base_map(opts); + + let geodesicLayer = leaflet.geodesic([], { + weight: 7, + opacity: 0.9, + color: PLAN_ROUTE_COLOR, + steps: 50, + wrap: false, + }).addTo(map); + + geodesicLayer.geoJson(opts.planned_route_line); + + try { + map.fitBounds(geodesicLayer.getBounds()); + } catch (e) { + console.log(e); + } + + // Draw the route points after + if (opts.route_points !== null) { + let route_points = leaflet.geoJSON(opts.route_points, { + onEachFeature: onFeaturePointClick, + pointToLayer: pointToLayer, + style: { + "color": PLAN_ROUTE_COLOR, + "weight": 5, + "opacity": 0.65, + }, + }); + + route_points.addTo(map); + } + + /** + * draw the actual route + */ + + if (opts.actual_route_line !== null && opts.actual_route_line.features.length > 0) { + let geodesicLayer = leaflet.geodesic([], { + weight: 7, + opacity: 0.9, + color: ACTUAL_ROUTE_COLOR, + steps: 50, + wrap: false, + }).addTo(map); + + geodesicLayer.geoJson(opts.actual_route_line); + + try { + map.fitBounds(geodesicLayer.getBounds()); + } catch (e) { + console.log(e); + } + } + + if (opts.actual_route_points !== null && opts.actual_route_points.features.length > 0) { + let route_points = leaflet.geoJSON(opts.actual_route_points, { + onEachFeature: onFeaturePointClick, + pointToLayer: pointToLayer, + style: { + "color": ACTUAL_ROUTE_COLOR, + "weight": 5, + "opacity": 0.65, + }, + }); + + route_points.addTo(map); + } +}; diff --git a/resources/views/admin/app.blade.php b/resources/views/admin/app.blade.php index e6454f73..1ff27bea 100644 --- a/resources/views/admin/app.blade.php +++ b/resources/views/admin/app.blade.php @@ -189,5 +189,6 @@ $(document).ready(function () { }); + @yield('scripts') diff --git a/resources/views/layouts/default/flights/map.blade.php b/resources/views/layouts/default/flights/map.blade.php index b4fdbf46..f58403c9 100644 --- a/resources/views/layouts/default/flights/map.blade.php +++ b/resources/views/layouts/default/flights/map.blade.php @@ -11,7 +11,7 @@ @section('scripts')