'use strict';
/**
* This widget displays an image of the track and where each car is on the track.
* It also displays the weather both as text and as a wind gauge.
* It uses the following widgets:
* {@link sra-track-map-car TrackMap/Car},
* {@link sra-track-map-finish-line TrackMap/FinishLine},
* {@link sra-weather-info WeatherInfo},
* {@link sra-wind-gauge WindGauge}.
* <p>
* Example:
* <p><b>
* <sra-track-map></sra-track-map><br />
* </b>
* <img src="../widgets/TrackMap/icon.png" atl="Image goes here"/>
* @ngdoc directive
* @name sra-track-map
* @param {String} data-sra-args-track-map-cars A semicolon list of cars to show.
* If you want to see the PITSTALL and PACECAR, you must include them in this list.
* Defaults to PITSTALL and all cars in the session.
* URL argument is TRACKMAPCARS.
* @param {String} data-sra-args-car-text-type Passed down to the {@link sra-track-map-car TrackMap/Car} widget.
* @param {String} data-sra-args-map-layer The type of map to show, "map", "osm", "mapquest".
* Defaults to "map".
* @param {boolean} data-sra-args-show-flags false turns the flags on/off. Defaults to true.
* @param {boolean} data-sra-args-show-wind-gauge false turns on/off the wind gauge. Defaults to true
* @param {boolean} data-sra-args-show-weather false turns on/off the weather information box. Defaults to true.
* @param {boolean} data-sra-args-show-title false turns on/off the title box. Defaults to true.
* @param {boolean} data-sra-args-show-info false turns off the track information box. Defaults to true.
* @param {boolean} data-sra-args-show-map true turns on/off the track layer. Defaults to true.
* @param {boolean} data-sra-args-show-path true turns on/off the current track path. Defaults to true.
* @param {boolean} data-sra-args-show-sectors true turns on/off the sector dots. Defaults to true. Since 1.19.
* @param {integer} data-sra-args-interval The interval, in milliseconds, that this widget will update from the server. Default is 100.
* @author Jeffrey Gilliam
* @since 1.0
* @copyright Copyright (C) 2015 - 2024 Jeffrey Gilliam
* @license Apache License 2.0
*/
define(['SIMRacingApps',
'openlayers/ol',
'css!widgets/TrackMap/TrackMap',
'widgets/TrackMap/Car/Car',
'widgets/TrackMap/FinishLine/FinishLine',
'widgets/TrackMap/MergePoint/MergePoint',
'widgets/TrackMap/Sector/Sector',
'widgets/DataTable/DataTable',
'widgets/Flags/Flags',
'widgets/WeatherInfo/WeatherInfo',
'widgets/WindGauge/WindGauge',
],
function(SIMRacingApps,ol) {
var self = {
name: "sraTrackMap",
url: 'TrackMap',
template: 'TrackMap.html',
defaultWidth: 800,
defaultHeight: 480,
defaultInterval: 100 //initialize with the default interval to how fast the cars will update
, maxcars: 64
, maxsectors: 15 //nurburgring long has 14 sectors.
};
self.module = angular.module('SIMRacingApps'); //get the main module
self.module.directive(self.name,
['sraDispatcher', '$filter', '$rootScope', '$document',
function(sraDispatcher, $filter, $rootScope, $document) {
return {
restrict: 'EA',
scope: true,
templateUrl: sraDispatcher.getWidgetUrl(self.url) + '/' + self.template,
controller: [ '$scope', function($scope) {
$scope.directiveName = self.name;
$scope.width = $scope.defaultWidth = self.defaultWidth;
$scope.height = $scope.defaultHeight = self.defaultHeight;
$scope.defaultInterval = self.defaultInterval;
//load translations
sraDispatcher.loadTranslations(sraDispatcher.getWidgetUrl(self.url),'text',function(path) {
$scope.translations = sraDispatcher.getTranslation(path);
});
$scope.pathCounter = 0;
$scope.sectorCounter = 0;
//initialize the $scope variables
$scope.imageUrl = "";
$scope.center = -10000;
$scope.resolution = -10000;
$scope.rotation = -10000;
$scope.cars = {};
$scope.carsQueue = [];
$scope.finishLeft = -10000;
$scope.finishTop = -10000;
$scope.finishDegrees = '0';
$scope.mergePointLeft= -10000;
$scope.mergePointTop = -10000;
$scope.sectors = {};
$scope.map = null;
$scope.dateformat = 'shortDate';
$scope.format = 'mediumTime';
$scope.tz = '';
$scope.time = 0;
$scope.arrayEqual = function (array1,array2) {
// if the other array is a falsy value, return
if (!array1 || !array2)
return false;
// compare lengths - can save a lot of time
if (array1.length != array2.length)
return false;
for (var i = 0, l=array1.length; i < l; i++) {
// Check if we have nested arrays
if (array1[i] instanceof Array && array2[i] instanceof Array) {
// recurse into the nested arrays
if (!array1[i].equals(array2[i]))
return false;
}
else if (array1[i] != array2[i]) {
// Warning - two different object instances will never be equal: {x:20} != {x:20}
return false;
}
}
return true;
};
$scope.createMap = function($element) {
var _lat = $scope.data.Track.Latitude.Value;
var _lng = $scope.data.Track.Longitude.Value;
var _north = $scope.data.Track.North.Value;
var _resolution = $scope.data.Track.Resolution.Value;
if (!$scope.map) {
var children = $element.find('div');
for (var i=0; i < children.length; i++) {
if (angular.element(children[i]).hasClass("SIMRacingApps-Widget-TrackMap-map")) {
//create the map.
$scope.map = new ol.Map({
layers: [
new ol.layer.Tile({
source: $scope.sraMapLayer.toUpperCase() == 'OSM'
? new ol.source.OSM()
: $scope.sraMapLayer.toUpperCase() == 'MAPQUEST'
? new ol.source.MapQuest({layer: 'sat'})
: null
})
],
target: children[i],
controls: ol.control.defaults({
zoom: false,
rotate: false,
attributionOptions: /** @type {olx.control.AttributionOptions} */ ({
collapsible: false
})
}),
view: new ol.View({
center: ol.proj.fromLonLat([_lng,_lat]),
rotation: ((_north + 90) / 360.0) * (Math.PI * 2),
resolution: (_resolution * (1/($scope.height/384)))
})
});
//we want a completely static map, so remove all user interactions
$scope.map.getInteractions().forEach(function(interaction) {
$scope.map.removeInteraction(interaction);
}, this);
}
}
}
if ($scope.map && _lng && _lat && _north) {
//if ($scope.map && (lng != $scope.lng || lat != $scope.lat || north != $scope.north)) {
//console.log("map("+lat+","+lng+","+resolution+","+north+")");
$scope.map.updateSize();
var center = ol.proj.fromLonLat([_lng,_lat]);
var resolution = (_resolution * (1/($scope.height/384)));
var rotation = (((_north + 90) / 360.0) * (Math.PI * 2));
if ($scope.center[0] != center[0]
|| $scope.center[1] != center[1]
|| $scope.resolution != resolution
|| $scope.rotation != rotation
) {
$scope.map.getView().setCenter($scope.center = center);
$scope.map.getView().setResolution($scope.resolution = resolution);
$scope.map.getView().setRotation($scope.rotation = rotation);
}
}
//if we got a map and there are cars to move, move them
if ($scope.map && $scope.carsQueue.length > 0) {
for (var i=0; i < $scope.carsQueue.length; i++)
$scope.moveCar($scope.carsQueue[i].carid,$scope.carsQueue.scope);
$scope.carsQueue = [];
}
};
$scope.getLocation = function(longitude,latitude,scope) {
if (scope.map) {
var location = scope.map.getPixelFromCoordinate(ol.proj.fromLonLat([longitude,latitude]));
//note: if map has not been rendered, the location returns NaN.
//TODO: This means cars will not get drawn until they move. Can we queue them up and keep retrying? Retry after the map renders?
if (location && !isNaN(location[0]) && !isNaN(location[1])) {
return location;
}
}
return null;
};
$scope.moveCar = function(carid,scope) {
if (!scope)
return;
if (!scope.map) {
//if the map is not drawn yet, save the cars we need to move in a queue.
$scope.carsQueue.push({ carid: carid, scope: scope});
}
else {
var longitude = scope.data.Car[carid].Longitude.Value;
var latitude = scope.data.Car[carid].Latitude.Value;
var location = scope.getLocation(longitude,latitude,scope);
//note: if map has not been rendered, the location returns NaN.
//TODO: This means cars will not get drawn until they move. Can we queue them up and keep retrying? Retry after the map renders?
if (location) {
scope.cars[carid].left = location[0];
scope.cars[carid].top = location[1];
}
else {
$scope.carsQueue.push({ carid: carid, scope: scope});
}
//draw the merge point
if (scope.data.Car.REFERENCE.MergePointLatitude) {
latitude = scope.data.Car.REFERENCE.MergePointLatitude.Value;
longitude = scope.data.Car.REFERENCE.MergePointLongitude.Value;
location = scope.getLocation(longitude,latitude,scope);
}
if (location) {
scope.mergePointLeft = location[0];
scope.mergePointTop = location[1];
}
//move the finish line and Pit Stall every time because we can't get the coordinates until the map renders
//TODO: Try and do this only when needed.
if (scope.data.Car.PITSTALL) {
longitude = scope.data.Car.PITSTALL.Longitude.Value;
latitude = scope.data.Car.PITSTALL.Latitude.Value;
location = scope.getLocation(longitude,latitude,scope);
if (location) {
scope.cars.PITSTALL.left = location[0];
scope.cars.PITSTALL.top = location[1];
}
}
//note: the pixel location returned is the center of the finish line
//we will set the Left/Top location to the center in the scope and then move it with CSS to center it.
latitude = scope.data.Track.Latitude.ONTRACK['100.0'].Value;
longitude = scope.data.Track.Longitude.ONTRACK['100.0'].Value;
location = scope.getLocation(longitude,latitude,scope);
if (location) {
var rotate = scope.data.Track.FinishLineRotation.Value;
scope.finishDegrees = rotate;
scope.finishLeft = location[0];
scope.finishTop = location[1];
}
}
};
$scope.moveSectors = function(newCoordinates, oldCoordinates) {
if ($scope.sraShowSectors) {
if (arguments.length == 3) {
if ($scope.arrayEqual(newCoordinates, oldCoordinates))
return;
}
if( Object.prototype.toString.call( newCoordinates ) === '[object Array]' ) {
//console.log((++$scope.sectorCounter)+" moveSectors("+(newCoordinates.length/4)+")");
for (var sector=0; sector < self.maxsectors; sector++) {
//first move them off screen in case they're not visible
$scope.sectors[sector].left = -100000;
$scope.sectors[sector].top = -100000;
//move sector if it's visible to it's location on the path
if ((sector*4) < newCoordinates.length) {
var latitude = newCoordinates[(sector*4)];
var longitude = newCoordinates[(sector*4)+1];
var location = $scope.getLocation(longitude,latitude,$scope);
if (location) {
$scope.sectors[sector].left = location[0];
$scope.sectors[sector].top = location[1];
}
}
}
}
}
};
$scope.getPath = function(path,oldPath) {
if ($scope.map) {
if (arguments.length == 3) {
if ($scope.arrayEqual(path,oldPath))
return;
}
if( Object.prototype.toString.call( path ) === '[object Array]' ) {
//console.log((++$scope.pathCounter)+" getPath("+path.length+")");
var d = "";
for (var point=0; point < path.length; point++) {
var longitude = path[point].Lon;
var latitude = path[point].Lat;
var location = $scope.getLocation(longitude,latitude,$scope);
//note: if map has not been rendered, the location returns NaN.
if (location) {
if (path[point].Type == -1.0) //Start
d += " M" + Math.round(location[0]) + " " + Math.round(location[1]);
else
if (path[point].Type == 0.0) //line to
d += " L" + Math.round(location[0]) + " " + Math.round(location[1]);
else
if (path[point].Type == 1.0) //end
d += " Z";
}
}
$scope.moveSectors($scope.data.Track.Sectors.COORDINATES.DEG.Value);
return d;
}
}
return "";
};
}]
, link: function($scope,$element,$attrs) {
//copy arguments to our $scope
$attrs.sraArgsData = $attrs.sraArgsData || "";
$scope.value =
$scope[self.name] = sraDispatcher.getTruthy($scope.sraArgsVALUE, $attrs[self.name], $attrs.sraArgsValue, "DefaultValue");
$scope.sraInterval = $attrs.sraInterval;
$scope.sraCarTextType = sraDispatcher.getTruthy($scope.sraArgsCARTEXTTYPE, $attrs.sraArgsCarTextType,"number");
$scope.sraShowFlags = sraDispatcher.getBoolean($scope.sraArgsSHOWFLAGS, $attrs.sraArgsShowFlags, true);
$scope.sraShowWindGauge = sraDispatcher.getBoolean($scope.sraArgsSHOWWINDGAUGE, $attrs.sraArgsShowWindGauge,true);
$scope.sraShowWeather = sraDispatcher.getBoolean($scope.sraArgsSHOWWEATHER, $attrs.sraArgsShowWeather, true);
$scope.sraShowTitle = sraDispatcher.getBoolean($scope.sraArgsSHOWTITLE, $attrs.sraArgsShowTitle, true);
$scope.sraShowInfo = sraDispatcher.getBoolean($scope.sraArgsSHOWINFO, $attrs.sraArgsShowInfo, true);
$scope.sraShowMap = sraDispatcher.getBoolean($scope.sraArgsSHOWMAP, $attrs.sraArgsShowMap, true);
$scope.sraShowPath = sraDispatcher.getBoolean($scope.sraArgsSHOWPATH, $attrs.sraArgsShowPath, true);
$scope.sraShowSectors = sraDispatcher.getBoolean($scope.sraArgsSHOWSECTORS, $attrs.sraArgsShowSectors, false);
$scope.sraMapLayer = sraDispatcher.getTruthy($scope.sraArgsMAPLAYER, $attrs.sraArgsMapLayer, 'map').toUpperCase();
$scope.sraCars = sraDispatcher.getTruthy($scope.sraArgsTRACKMAPCARS, $attrs.sraArgsTrackMapCars, '');
//put everything off screen, out of view, until we get the coordinates of where to put them
//TODO: Get a list of the cars from the server
if ($scope.sraCars) {
var aCars = $scope.sraCars.split(";");
for (var carid=0; carid < aCars.length; carid++) {
$scope.cars[aCars[carid]] = {};
$scope.cars[aCars[carid]].id = aCars[carid];
$scope.cars[aCars[carid]].left = -10000;
$scope.cars[aCars[carid]].top = -10000;
}
}
else {
$scope.cars.PITSTALL = {};
$scope.cars.PITSTALL.id = 'PITSTALL';
$scope.cars.PITSTALL.left = -10000;
$scope.cars.PITSTALL.top = -10000;
for (var carid=0; carid < self.maxcars; carid++) {
$scope.cars['I'+carid] = {};
$scope.cars['I'+carid].id = 'I'+carid;
$scope.cars['I'+carid].left = -10000;
$scope.cars['I'+carid].top = -10000;
}
}
//register for movement for all cars, including pit stall
for (var carid in $scope.cars) {
$attrs.sraArgsData += ";Car/"+carid+"/Latitude;Car/"+carid+"/Longitude";
$attrs.sraArgsData += ";Car/"+carid+"/Status";
$attrs.sraArgsData += ";Car/"+carid+"/IsEqual/REFERENCE";
$attrs.sraArgsData += ";Car/"+carid+"/IsEqual/PACECAR";
$attrs.sraArgsData += ";Car/"+carid+"/IsEqual/LEADER";
$attrs.sraArgsData += ";Car/"+carid+"/IsEqual/TRANSMITTING";
$attrs.sraArgsData += ";Car/"+carid+"/IsEqual/PITSTALL";
$scope.$watch("data.Car['"+carid+"'].Latitude.Value", new Function('newValue','oldValue','$scope','$scope.moveCar("'+carid+'",$scope);'));
$scope.$watch("data.Car['"+carid+"'].Longitude.Value", new Function('newValue','oldValue','$scope','$scope.moveCar("'+carid+'",$scope);'));
}
//setup for sector dots
$attrs.sraArgsData += ";Track/Sectors/COORDINATES/DEG";
for (var sector=0; sector < self.maxsectors; sector++) {
$scope.sectors[sector] = {};
$scope.sectors[sector].number = sector + 1;
$scope.sectors[sector].left = -10000;
$scope.sectors[sector].top = -10000;
}
$scope.$watch("data.Track.Sectors.COORDINATES.DEG.Value", $scope.moveSectors);
//now watch for track changes and move the finish line
$attrs.sraArgsData += ";Session/Time;Track/Latitude;Track/Longitude;Track/Resolution;Track/North;Track/Image/"+$scope.sraMapLayer;
$scope.$watch('data.Track.Latitude.Value', function() {$scope.createMap($element);});
$scope.$watch('data.Track.Longitude.Value', function() {$scope.createMap($element);});
$scope.$watch('data.Track.North.Value', function() {$scope.createMap($element);});
$scope.$watch('data.Track.Resolution.Value', function() {$scope.createMap($element);});
$scope.$watch("data.Session.Time.Value",function(value,oldvalue) {
$scope.time = new Date();
$scope.time.setTime((value*1000));
$scope.tz = $scope.data.Session.Time.State;
});
$scope.$watch('data.Track.Image["'+$scope.sraMapLayer+'"].Value',function(image) {
if (image) {
$scope.imageUrl = "/SIMRacingApps/Resource/"+image;
}
else {
$scope.imageUrl = "";
}
});
$attrs.sraArgsData += ";Track/FinishLineRotation;Track/Latitude/ONTRACK/100.0;Track/Longitude/ONTRACK/100.0";
$attrs.sraArgsData += ";Car/REFERENCE/MergePointLatitude;Car/REFERENCE/MergePointLongitude";
//register with the dispatcher
$scope.names = sraDispatcher.subscribe($scope,$attrs,self.defaultInterval); //register subscriptions and options to the dispatcher
//now setup to watch for path changes
if ($scope.sraShowPath) {
//register with the dispatcher
$attrs.sraArgsData = "Track/Path/ONTRACK;Track/Path/ONPITROAD";
$scope.names = sraDispatcher.subscribe($scope,$attrs,5000); //register subscriptions and options to the dispatcher
$scope.$watch('data.Track.Path.ONTRACK.Value', function(path) {
$scope.trackpath = $scope.getPath($scope.data.Track.Path.ONTRACK.Value);
});
$scope.$watch('data.Track.Path.ONPITROAD.Value', function(path) {
$scope.pitroadpath = $scope.getPath($scope.data.Track.Path.ONPITROAD.Value);
});
}
$rootScope.$on('sraResize', function() {
sraDispatcher.resize($scope,$element,self.defaultWidth,self.defaultHeight);
$scope.createMap($element);
if ($scope.sraShowPath) {
$scope.trackpath = $scope.getPath($scope.data.Track.Path.ONTRACK.Value);
$scope.pitroadpath = $scope.getPath($scope.data.Track.Path.ONPITROAD.Value);
}
$scope.moveSectors($scope.data.Track.Sectors.COORDINATES.DEG.Value);
});
}
};
}]);
return self;
});