mirror of
https://github.com/flyandi/mazda-custom-application-sdk
synced 2024-12-23 19:52:51 -06:00
688 lines
14 KiB
JavaScript
688 lines
14 KiB
JavaScript
/**
|
|
* Custom Applications SDK for Mazda Connect Infotainment System
|
|
*
|
|
* A mini framework that allows to write custom applications for the Mazda Connect Infotainment System
|
|
* that includes an easy to use abstraction layer to the JCI system.
|
|
*
|
|
* Written by Andreas Schwarz (http://github.com/flyandi/mazda-custom-applications-sdk)
|
|
* Copyright (c) 2016. All rights reserved.
|
|
*
|
|
* WARNING: The installation of this application requires modifications to your Mazda Connect system.
|
|
* If you don't feel comfortable performing these changes, please do not attempt to install this. You might
|
|
* be ending up with an unusuable system that requires reset by your Dealer. You were warned!
|
|
*
|
|
* This program 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.
|
|
*
|
|
* This program 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 this program.
|
|
* If not, see http://www.gnu.org/licenses/
|
|
*
|
|
*/
|
|
|
|
|
|
/**
|
|
* Speedometer Application
|
|
*
|
|
* This is an implementation of the famous Speedometer by @serezhka
|
|
*/
|
|
|
|
CustomApplicationsHandler.register("app.speedometer", new CustomApplication({
|
|
|
|
/**
|
|
* (require)
|
|
*
|
|
* An object array that defines resources to be loaded such as javascript's, css's, images, etc
|
|
*
|
|
* All resources are relative to the applications root path
|
|
*/
|
|
|
|
require: {
|
|
|
|
/**
|
|
* (js) defines javascript includes
|
|
*/
|
|
|
|
js: [],
|
|
|
|
/**
|
|
* (css) defines css includes
|
|
*/
|
|
|
|
css: ['app.css'],
|
|
|
|
/**
|
|
* (images) defines images that are being preloaded
|
|
*
|
|
* Images are assigned to an id
|
|
*/
|
|
|
|
images: {
|
|
|
|
},
|
|
},
|
|
|
|
/**
|
|
* (settings)
|
|
*
|
|
* An object that defines application settings
|
|
*/
|
|
|
|
settings: {
|
|
|
|
/**
|
|
* (terminateOnLost)
|
|
*
|
|
* If set to 'true' this will remove the stateless life cycle and always
|
|
* recreate the application once the focus is lost. Otherwise by default
|
|
* the inital created state will stay alive across the systems runtime.
|
|
*
|
|
* Default is false or not set
|
|
* /
|
|
|
|
// terminateOnLost: false,
|
|
|
|
/**
|
|
* (title) The title of the application in the Application menu
|
|
*/
|
|
|
|
title: 'Speedometer',
|
|
|
|
/**
|
|
* (statusbar) Defines if the statusbar should be shown
|
|
*/
|
|
|
|
statusbar: true,
|
|
|
|
/**
|
|
* (statusbarIcon) defines the status bar icon
|
|
*
|
|
* Set to true to display the default icon app.png or set a string to display
|
|
* a fully custom icon.
|
|
*
|
|
* Icons need to be 37x37
|
|
*/
|
|
|
|
statusbarIcon: true,
|
|
|
|
/**
|
|
* (statusbarTitle) overrides the statusbar title, otherwise title is used
|
|
*/
|
|
|
|
statusbarTitle: 'Speedometer',
|
|
|
|
/**
|
|
* (statusbarHideHomeButton) hides the home button in the statusbar
|
|
*/
|
|
|
|
// statusbarHideHomeButton: false,
|
|
|
|
/**
|
|
* (hasLeftButton) indicates if the UI left button / return button should be shown
|
|
*/
|
|
|
|
hasLeftButton: false,
|
|
|
|
/**
|
|
* (hasMenuCaret) indicates if the menu item should be displayed with an caret
|
|
*/
|
|
|
|
hasMenuCaret: false,
|
|
|
|
/**
|
|
* (hasRightArc) indicates if the standard right car should be displayed
|
|
*/
|
|
|
|
hasRightArc: false,
|
|
|
|
},
|
|
|
|
/**
|
|
* Scales
|
|
*/
|
|
|
|
scales: {
|
|
|
|
na: {
|
|
unit: 'mph',
|
|
unitLabel: 'MPH',
|
|
transformSpeed: DataTransform.toMPH,
|
|
scaleMin: 0, // 0 = 0mph
|
|
scaleMax: 13, // 12 = 120mph
|
|
scaleMinSpeed: 0,
|
|
scaleMaxSpeed: 120,
|
|
scaleStep: 10, // every 10 miles / hour
|
|
scaleAngle: 148,
|
|
scaleRadius: 170,
|
|
scaleOffsetStep: 4.8,
|
|
scaleOffsetX: -11,
|
|
scaleOffsetY: 0,
|
|
scaleWidth: 278,
|
|
scaleHeight: 241,
|
|
},
|
|
|
|
eu: {
|
|
unit: 'kmh',
|
|
unitLabel: 'km/h',
|
|
scaleMin: 0, // 0 = 0mph
|
|
scaleMax: 13, // 12 = 120km/h
|
|
scaleMinSpeed: 0,
|
|
scaleMaxSpeed: 240,
|
|
scaleStep: 20, // every 20 km/h
|
|
scaleAngle: 148,
|
|
scaleRadius: 170,
|
|
scaleOffsetStep: 4.6,
|
|
scaleOffsetX: -15,
|
|
scaleOffsetY: 0,
|
|
scaleWidth: 278,
|
|
scaleHeight: 241,
|
|
},
|
|
|
|
|
|
},
|
|
|
|
// default scale
|
|
scale: false,
|
|
|
|
|
|
/**
|
|
* Statistics
|
|
*/
|
|
|
|
statistics: {
|
|
topSpeed: 0,
|
|
speeds: [],
|
|
averageSpeeds: [],
|
|
},
|
|
|
|
|
|
/***
|
|
*** User Interface Life Cycles
|
|
***/
|
|
|
|
/**
|
|
* (created)
|
|
*
|
|
* Executed when the application gets initialized
|
|
*
|
|
* Add any content that will be static here
|
|
*/
|
|
|
|
created: function() {
|
|
|
|
// create speedometer panel
|
|
this.speedoMeter = $("<div/>").attr("id", "speedometer").appendTo(this.canvas);
|
|
|
|
this.speedoUnit = $("<div/>").attr("id", "speedounit").appendTo(this.speedoMeter);
|
|
|
|
this.speedoDial = $("<div/>").attr("id", "speedodial").appendTo(this.canvas);
|
|
|
|
this.speedoRPM = $("<div/>").attr("id", "speedorpm").appendTo(this.canvas);
|
|
this.speedoRPMIndicator = $("<div/>").addClass("circle").appendTo(this.speedoRPM);
|
|
|
|
this.speedoRPMLabel = $("<label/>").css({
|
|
position:'absolute',
|
|
right:0,
|
|
top:0,
|
|
}).hide().appendTo(this.canvas);
|
|
|
|
this.speedoIndicator = $("<div/>").attr("id", "speedoindicator").appendTo(this.canvas);
|
|
|
|
this.speedoCurrent = $("<div/>").append("0").attr("id", "speedocurrent").appendTo(this.canvas);
|
|
|
|
this.speedoDialText = $("<div/>").attr("id", "speedodialtext").appendTo(this.canvas);
|
|
|
|
//this.speedoGraph = $("<canvas/>").attr({id: "speedograph", width: 260, height: 150}).appendTo(this.canvas);
|
|
|
|
// create gps
|
|
this.createGPSPanel();
|
|
|
|
// initialize scale
|
|
this.updateSpeedoScale();
|
|
|
|
// updates speed
|
|
//this.updateSpeedoGraph();
|
|
|
|
// register events
|
|
this.subscribe(VehicleData.vehicle.speed, function(value) {
|
|
|
|
this.setSpeedPosition(value);
|
|
|
|
}.bind(this));
|
|
|
|
this.subscribe(VehicleData.gps.heading, function(value) {
|
|
|
|
this.setGPSHeading(value);
|
|
|
|
}.bind(this));
|
|
|
|
this.subscribe(VehicleData.vehicle.rpm, function(value, params) {
|
|
|
|
this.setRPMPosition(value, params);
|
|
|
|
}.bind(this));
|
|
|
|
},
|
|
|
|
/**
|
|
* (focused)
|
|
*
|
|
* Executes when the application gets the focus. You can either use this event to
|
|
* build the application or use the created() method to predefine the canvas and use
|
|
* this method to run your logic.
|
|
*/
|
|
|
|
focused: function() {
|
|
|
|
// start collection
|
|
/*this.collectorTimer = setInterval(function() {
|
|
|
|
this.collectStatistics();
|
|
|
|
}.bind(this), 1000);*/
|
|
|
|
// update graph
|
|
//this.updateSpeedoGraph();
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* (lost)
|
|
*
|
|
* Lost is executed when the application looses it's context. You can specify any
|
|
* logic that you want to run before the application gets removed from the DOM.
|
|
*
|
|
* If you enabled terminateOnLost you may want to save the state of your app here.
|
|
*/
|
|
|
|
lost: function() {
|
|
|
|
// stop collection
|
|
clearInterval(this.collectorTimer);
|
|
|
|
},
|
|
|
|
/***
|
|
*** Events
|
|
***/
|
|
|
|
/**
|
|
* (event) onControllerEvent
|
|
*
|
|
* Called when a new (multi)controller event is available
|
|
*/
|
|
|
|
onControllerEvent: function(eventId) {
|
|
|
|
switch(eventId) {
|
|
|
|
|
|
case "selectStart":
|
|
|
|
var region = this.getRegion() == "na" ? "eu" : "na";
|
|
|
|
this.setRegion(region);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
* (event) onRegionChange
|
|
*
|
|
* Called when the region changes
|
|
*/
|
|
|
|
onRegionChange: function(region) {
|
|
|
|
this.updateSpeedoScale();
|
|
|
|
//this.updateSpeedoGraph();
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* (createGPSPanel)
|
|
*/
|
|
|
|
createGPSPanel: function() {
|
|
|
|
this.gpsPanel = $("<div/>").attr("id", "gps").appendTo(this.canvas)
|
|
this.gpsCompass = $("<div/>").attr("id", "gpscompass").appendTo(this.canvas);
|
|
|
|
var rose = [];
|
|
|
|
// create rose
|
|
['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'].forEach(function(direction) {
|
|
|
|
rose.push($("<div/>").addClass(direction.length == 2 ? "small" : "").append(direction).appendTo(this.gpsCompass));
|
|
|
|
}.bind(this));
|
|
|
|
// apply radial transformation
|
|
this.createScaleRadial(rose, {
|
|
scaleMin: 0,
|
|
scaleMax: 8,
|
|
scaleStep: 45,
|
|
scaleAngle: -90,
|
|
scaleRadius: 78,
|
|
scaleOffsetStep: 0,
|
|
scaleOffsetX: 126,
|
|
scaleOffsetY: 132,
|
|
scaleWidth: 179,
|
|
scaleHeight: 179,
|
|
scaleHalfAngle: function(angle, radian, field) {
|
|
if(angle % 2) {
|
|
return angle < 0 || angle == 135 ? 45 : -45
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
/**
|
|
* (updateSpeedoGraph)
|
|
*/
|
|
|
|
updateSpeedoGraph: function() {
|
|
|
|
// prepare
|
|
var region = this.getRegion(),
|
|
scale = this.scales[region] || this.scales.na,
|
|
canvas = this.speedoGraph.get(0),
|
|
ctx = canvas.getContext('2d');
|
|
|
|
// clear
|
|
canvas.width = canvas.width;
|
|
|
|
// create divider
|
|
ctx.strokeStyle = "rgba(255, 255, 255, 0.75)";
|
|
ctx.lineWidth = 2;
|
|
ctx.setLineDash([2, 2]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, 75);
|
|
ctx.lineTo(260, 75);
|
|
ctx.stroke();
|
|
|
|
// draw graph
|
|
if(this.statistics.averageSpeeds.length) {
|
|
|
|
var ds = Math.round(260 / (this.statistics.averageSpeeds.length));
|
|
|
|
ctx.strokeStyle = "rgba(255, 40, 25, 0.9)";
|
|
ctx.setLineDash([0, 0]);
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
|
|
this.statistics.averageSpeeds.forEach(function(avg, index) {
|
|
|
|
var x = 260 - (index * ds),
|
|
y = 120 - DataTransform.scaleValue(avg, [0, 240], [0, 90]);
|
|
|
|
if(index == 0) {
|
|
ctx.moveTo(x, y);
|
|
} else {
|
|
ctx.lineTo(x, y);
|
|
}
|
|
|
|
});
|
|
ctx.stroke();
|
|
|
|
}
|
|
|
|
|
|
// draw labels
|
|
ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
|
|
ctx.font = "17px Tipperary, Arial, Helvetica, sans-serif";
|
|
ctx.fillText(scale.scaleMaxSpeed, 5, 20);
|
|
ctx.fillText(scale.scaleMinSpeed, 5, 140);
|
|
|
|
// draw unit display
|
|
|
|
|
|
// create divider
|
|
$("<div/>").addClass("divider").appendTo(this.speedoGraph);
|
|
|
|
// show
|
|
this.speedoGraph.fadeIn('fast');
|
|
|
|
},
|
|
|
|
/**
|
|
* (updateSpeedoScale)
|
|
*/
|
|
|
|
updateSpeedoScale: function() {
|
|
|
|
// hide old content
|
|
if(this.hasSpeedoDialText) {
|
|
this.speedoDialText.fadeOut('fast', function() {
|
|
this.hasSpeedoDialText = false;
|
|
this.updateSpeedoScale();
|
|
}.bind(this));
|
|
return;
|
|
}
|
|
|
|
// clear main container
|
|
this.speedoDialText.empty().hide();
|
|
|
|
// prepare
|
|
var region = this.getRegion(),
|
|
scale = this.scales[region] || this.scales.na,
|
|
container = $("<div/>").addClass("container").appendTo(this.speedoDialText),
|
|
fields = [];
|
|
|
|
// set scale
|
|
this.scale = scale;
|
|
|
|
// create scale
|
|
for(var s = scale.scaleMin; s < scale.scaleMax; s++) {
|
|
// create scale label
|
|
fields.push($("<div/>").addClass("speedotext").append(s * scale.scaleStep).appendTo(container));
|
|
}
|
|
|
|
// apply radial transformation
|
|
this.createScaleRadial(fields, scale);
|
|
|
|
// also update some other containers
|
|
this.speedoUnit.html(scale.unitLabel);
|
|
|
|
this.speedoDialText.fadeIn('fast');
|
|
|
|
this.setSpeedPosition(this.__speed);
|
|
|
|
// update content
|
|
this.hasSpeedoDialText = true;
|
|
|
|
// return the container
|
|
return container;
|
|
},
|
|
|
|
/**
|
|
* (createScaleRadial) creates a radial container
|
|
*/
|
|
|
|
createScaleRadial: function(fields, scale) {
|
|
|
|
var radius = scale.scaleRadius,
|
|
width = scale.scaleWidth,
|
|
height = scale.scaleHeight,
|
|
ox = scale.scaleOffsetX,
|
|
oy = scale.scaleOffsetY,
|
|
angle = scale.scaleAngle,
|
|
radian = scale.scaleAngle * (Math.PI / 180),
|
|
step = (2 * Math.PI) / (scale.scaleMax - scale.scaleMin + scale.scaleOffsetStep);
|
|
|
|
|
|
fields.forEach(function(field) {
|
|
|
|
// calculate positon
|
|
var x = Math.round(width / 2 + radius * Math.cos(radian) - field.width()/2),
|
|
y = Math.round(height / 2 + radius * Math.sin(radian) - field.height()/2);
|
|
|
|
field.css({
|
|
top: oy + y,
|
|
left: ox + x
|
|
});
|
|
|
|
if(this.is.fn(scale.scaleHalfAngle)) {
|
|
|
|
var value = scale.scaleHalfAngle(angle, radian, field);
|
|
|
|
if(value !== false) {
|
|
field.css({
|
|
transform: 'rotate(' + value + 'deg)'
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
radian += step;
|
|
angle = radian * (180 / Math.PI);
|
|
}.bind(this));
|
|
},
|
|
|
|
|
|
/**
|
|
* (setSpeedPosition)
|
|
*/
|
|
|
|
setSpeedPosition: function(speed) {
|
|
|
|
// prepare
|
|
speed = speed || 0;
|
|
this.__speed = speed;
|
|
|
|
// update statistics
|
|
if(speed > this.statistics.topSpeed) {
|
|
this.statistics.topSpeed = speed;
|
|
}
|
|
|
|
// get localized reference speed
|
|
var refSpeed = this.transformValue(this.__speed, this.scale.transformSpeed);
|
|
if(refSpeed < this.scale.scaleMinSpeed) refSpeed = this.scale.scaleMinSpeed
|
|
if(refSpeed > this.scale.scaleMaxSpeed) refSpeed = this.scale.scaleMaxSpeed;
|
|
|
|
// calculate speed on scale
|
|
speed = DataTransform.scaleValue(refSpeed, [this.scale.scaleMinSpeed, this.scale.scaleMaxSpeed], [0, 240]);
|
|
|
|
// set label
|
|
this.speedoCurrent.html(refSpeed);
|
|
|
|
// update dial
|
|
if(speed < 0) speed = 0;
|
|
if(speed > 240) speed = 240;
|
|
|
|
speed = -120 + (speed);
|
|
|
|
// stop current animation
|
|
if(this.speedoIndicatorAnimation) {
|
|
this.speedoIndicatorAnimation.stop();
|
|
}
|
|
this.speedoIndicatorAnimation = $({deg: this.__oldspeed || 0}).stop().animate({deg: speed}, {
|
|
duration: 1000,
|
|
step: function(d) {
|
|
this.speedoIndicator.css({
|
|
transform: 'rotate(' + d + 'deg)'
|
|
});
|
|
}.bind(this)
|
|
});
|
|
this.__oldspeed = speed;
|
|
},
|
|
|
|
|
|
/**
|
|
* (setGPSHeading)
|
|
*/
|
|
|
|
setGPSHeading: function(heading) {
|
|
|
|
// 0 = North, 180 = Souths
|
|
this.gpsPanel.css({
|
|
transform: 'rotate(' + heading + 'deg)'
|
|
});
|
|
},
|
|
|
|
/**
|
|
* (setRPMPosition)
|
|
*/
|
|
|
|
setRPMPosition: function(rpm, params) {
|
|
|
|
this.speedoRPMLabel.html(rpm);
|
|
// min
|
|
if(rpm < 1000) {
|
|
rpm = 0;
|
|
|
|
} else {
|
|
// calculate value
|
|
rpm = 80 + DataTransform.scaleValue(rpm, [params.min, params.max], [0, 100]);
|
|
}
|
|
|
|
if(rpm == this.__oldrpm) return; // no update for that
|
|
|
|
// stop current animation
|
|
if(this.speedoRPMIndicatorAnimation) {
|
|
this.speedoRPMIndicatorAnimation.stop();
|
|
}
|
|
this.speedoRPMIndicatorAnimation = $({deg: this.__oldrpm || 0}).stop().animate({deg: rpm}, {
|
|
duration: 1000,
|
|
step: function(d) {
|
|
|
|
this.speedoRPMIndicator.css({
|
|
transform: 'rotate(' + d + 'deg)',
|
|
opacity: DataTransform.scaleValue(d, [0, 180], [0.5, 1])
|
|
});
|
|
|
|
}.bind(this)
|
|
});
|
|
|
|
this.__oldrpm = rpm;
|
|
},
|
|
|
|
|
|
/**
|
|
* (collectStatistics) starts collecting statistics and redraws the graph
|
|
*/
|
|
|
|
collectStatistics: function() {
|
|
|
|
return;
|
|
|
|
this.statistics.speeds.push(this.__speed);
|
|
|
|
if(this.statistics.speeds.length >= 5) {
|
|
|
|
// calculate average
|
|
var t = 0;
|
|
this.statistics.speeds.forEach(function(v) { t += v;});
|
|
|
|
var avg = Math.round(t / this.statistics.speeds.length);
|
|
|
|
// push to average list
|
|
this.statistics.averageSpeeds.unshift(avg);
|
|
|
|
if(this.statistics.averageSpeeds.length > 15) {
|
|
this.statistics.averageSpeeds.pop();
|
|
}
|
|
|
|
this.statistics.speeds = [];
|
|
|
|
// update display
|
|
this.updateSpeedoGraph();
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
})); /** EOF **/
|