/// Version 1.0.0
/// Last modified 24.5.2014
///
/// Copyright (C) 2014 Veli-Mikko Puupponen, Niko Mononen and Atte Söderlund
///
/// Halyri-system is a prototype emergency call system. Its purpose is to
/// demonstrate the use of the advanced capabilities available in the current
/// generation smartphones in facilitating the emergency service dispatcher's
/// capability to determine the nature of the emergency and to dispatch help.
///
/// For more information, see the README file of this package.
///
/// The MIT License (MIT)
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to
/// deal in the Software without restriction, including without limitation the
/// rights to use, copy, modify, merge, publish, distribute, sublicense,
/// and/or sell copies of the Software, and to permit persons to whom the
/// Software is furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
/// IN THE SOFTWARE.
///
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Hake_WPF.CallCenterReference;
using System.IO;
using NSpeex;
using NAudio.Wave;
using GraphClass;
using Hake_WPF.AudioVideoManagers;
using SimpleUdpMediaClient.Packets;
using Hake_WPF.Network;
using System.Windows.Documents;
namespace Hake_WPF
{
/// Atte Söderlund
///
/// The interaction logic for MainWindow.xaml. A user can ask information from mobile client including video and location. The user can also send tutorials.
/// The user can also manage assigments which are shown in the listbox and in the map by the pushpins.
public partial class MainWindow : Window
{
private ObservableCollection assignments = new ObservableCollection();
private Assignment ownAssignment;
private Connection connection;
private AudioVideoTransferManager audioVideoManager;
private bool assigmentActive = false;
private Graph measurementGraph;
private Settings settings = new Settings();
private int angle = 0;
private SettingsWindow settingsWindow;
private MapController mapController = new MapController();
private bool isVideoPlaying = false;
private bool isMeasurementPlaying = false;
private bool FirstMeasurementPacket = true;
private const int pingIntervalSeconds = 10;
private const String endpointAddress = "net.tcp://130.234.9.165:15101/HalyriServer/services/WcfCallCenterService.svc/";
///
/// The function initializes components and focuses map. Also it connects to the server by endpointAddress
/// that is in settings or by default. Then it adds eventhandlers and informs the user if the connection
/// is not established in the case there is some problem. It also starts Pinger that pings server.
///
public MainWindow()
{
InitializeComponent();
measurementGraph = new Graph(MeasurementDataGrid);
mapController.AddMap(map);
mapController.MapWindowClosingEvent += mapController_MapWindowClosingEvent;
String tempEndpointAddress;
if ((tempEndpointAddress = (String)settings.Get(Settings.ENDPOINTADDRESS)) == null)
tempEndpointAddress = endpointAddress;
connection = new Connection("TestUser","testPassword");
audioVideoManager = new AudioVideoTransferManager(connection);
audioVideoManager.JpgImageReceivedEvent += IncomingPictureHandler;
connection.AssigmentUpdatedEvent += connection_AssigmentUpdatedEvent;
assignments = new ObservableCollection(connection.Connect(tempEndpointAddress).OrderBy(x => x.State).ThenBy(x => x.Priority).ThenByDescending
(x => x.Time).ToArray());
connection.receiver.MeasurementDataReceivedEvent += MeasurementDataReceived;
AssignmentsListBox.ItemsSource = assignments;
if (!connection.IsConnected())
MessageBox.Show("Ei voitu yhdistää");
mapController.AddAllPushpins(assignments);
Pinger pinger = new Pinger(pingIntervalSeconds, connection);
pinger.StartPinger();
}
///
/// Enables MapTopOwnWindowButton when MapWindow is closing.
///
void mapController_MapWindowClosingEvent()
{
MapToOwnWindowButton.IsEnabled = true;
}
///
/// Clears everything from map and disposes it. Then makes new window and makes the same map to it.
/// Disables button and adds eventhandler for closing window that it can be put back to same place.
///
private void MapToOwnWindow_Click(object sender, RoutedEventArgs e)
{
mapController.MapToOwnWindow(assignments);
MapToOwnWindowButton.IsEnabled = false;
}
///
/// Centers map to what ever listbox Assigment is selected or none if not selected.
///
private void CenterMapButton_Click(object sender, RoutedEventArgs e)
{
if (AssignmentsListBox.SelectedItem != null && ((Assignment)AssignmentsListBox.SelectedItem).Location != null)
mapController.CenterMaps(assignments[AssignmentsListBox.SelectedIndex]);
}
///
/// The function disconnects from the server before closing program. Also if the mapwindows or/and the settingswindow is open, it closes those too.
///
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
if (connection.IsConnected())
connection.Disconnect();
if (mapController != null)
mapController.Close();
if (settingsWindow != null)
settingsWindow.Close();
base.OnClosing(e);
}
///
/// Replaces assignments with a new assignment if there is one already with same guid.
/// If there is not assignment with the guid, adds new assignment
/// to assignments. If location is set, adds pushpin. If locationtype is set to userspecified takes old location
/// from previous assignment and adds its puhspin so it disapear. Also displays textmessages if there is any.
///
/// Observablecollection of new connections
void connection_AssigmentUpdatedEvent(ObservableCollection newConnections)
{
int assignmentListBoxindex = AssignmentsListBox.SelectedIndex;
bool locationChanged = false;
for (int i = 0; i < newConnections.Count;i++ )
{
Assignment assignment = assignments.FirstOrDefault(connection => connection.Guid.Contains(newConnections[i].Guid));
if (assignment != null)
{
int index = assignments.IndexOf(assignment);
if (newConnections[i].Location != assignments[index].Location)
{
locationChanged = true;
}
assignments.Insert(index, newConnections[i]);
assignments.RemoveAt(index + 1);
mapController.AddAllPushpins(assignments);
AssignmentsListBox.SelectedIndex = assignmentListBoxindex;
if (!assignments[index].ClientConnected && assigmentActive && AssignmentsListBox.SelectedIndex == index)
UnHandleAssignmentButton_Click(this, new RoutedEventArgs());
}
else
{
assignments.Insert(0,newConnections[i]);
//TODO:Sort list correctly
//assignments = new ObservableCollection(assignments.OrderBy(x => x.State).ThenBy(x => x.Priority).ThenByDescending(x => x.Time).ToArray());
}
}
if (AssignmentsListBox.SelectedIndex >= 0 && ((Assignment)AssignmentsListBox.SelectedItem).TextMessages.Count > 0)
LoadAllTextMessagesFromSelectedItem();
if (locationChanged && AssignmentsListBox.SelectedIndex != -1)
mapController.CenterMaps(assignments[AssignmentsListBox.SelectedIndex]);
}
///
/// Clears all messages from inbox and then adds only selected assignments messages to it.
/// Makes timestamp bolded and adds date if it is not sent today.
///
private void LoadAllTextMessagesFromSelectedItem()
{
ChatInbox.Children.Clear();
Assignment assignment = assignments[AssignmentsListBox.SelectedIndex];
foreach (TextMessageDto message in assignment.TextMessages)
{
TextBlock newMessageTextBlock = new TextBlock()
{
TextWrapping = TextWrapping.Wrap,
Width= ChatInbox.ActualWidth
};
newMessageTextBlock.Inlines.Add(new Run()
{
Text = ((message.TimeStamp.Date.Equals(DateTime.Now.Date)) ? message.TimeStamp.ToShortTimeString() : message.TimeStamp.ToString()) + " " + message.Originator + ":\n",
FontWeight = FontWeights.Bold
});
newMessageTextBlock.Inlines.Add(new Run() { Text = message.Content });
ChatInbox.Children.Add(newMessageTextBlock);
}
ChatScroll.ScrollToEnd();
}
///
/// If no assignment is selected -> returns. If user is handling a assignment sets assingmentlistbox selection to old one
/// and returns so user cannot change selection while assignment is active. Also centers map when assigmentslistbox selection is changed.
/// If selecteditem has location it enables centermapbutton. Also clears chat and loads selected assignments chat messages.
/// Also clears videoplayer and ekg canvas.
///
private void AssignmentsListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (AssignmentsListBox.SelectedItem == null)
return;
if (ownAssignment != null)
{
AssignmentsListBox.SelectedIndex = assignments.IndexOf(assignments.First(x => x.Guid == ownAssignment.Guid));
return;
}
CenterMapButton.IsEnabled = false;
if (assignments[AssignmentsListBox.SelectedIndex].Location != null)
{
mapController.CenterMaps(assignments[AssignmentsListBox.SelectedIndex]);
CenterMapButton.IsEnabled = true;
}
ChangeAssigmentPriorityComboBox.SelectedIndex = (int)assignments[AssignmentsListBox.SelectedIndex].Priority;
ImageElement.Source = null;
MeasurementDataGrid.Children.Clear();
measurementGraph = new Graph(MeasurementDataGrid);
mapController.AddAllPushpins(assignments);
Assignment selectedAssignment = assignments[AssignmentsListBox.SelectedIndex];
LoadAllTextMessagesFromSelectedItem();
}
///
/// If there is none selected in assignmentlistbox, shows messagebox that informs user to select one.
/// Switch HandleAssignmentButton and handlleassignmentmennuitem content and click handler to vice versa.
/// State changing happens in own thread so it won't block ui thread. Enables buttons that can now be used.
///
private void HandleAssigment_Click(object sender, RoutedEventArgs e)
{
if (AssignmentsListBox.SelectedItem == null)
{
MessageBox.Show("Valitse ensin tehtävä!","Huomio!",MessageBoxButton.OK,MessageBoxImage.Information);
return;
}
if (((Assignment)AssignmentsListBox.SelectedItem).State == Assignment.States.Finalized)
{
MessageBox.Show("Käsiteltyä tehtävää ei voi ottaa käsittelyyn!", "Virhe!", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
if (((Assignment)AssignmentsListBox.SelectedItem).State == Assignment.States.InProgress)
{
if (MessageBox.Show("Tehtävä, jota yrität ottaa käsittelyyn, on jo käsittelyssä. Oletko varma, että haluat ottaa " +
"tehtävän käsittelyyn?", "Varoitus!", MessageBoxButton.YesNo, MessageBoxImage.Warning)==MessageBoxResult.No)
return;
}
if (!((Assignment)AssignmentsListBox.SelectedItem).ClientConnected)
{
MessageBox.Show("Tehtävää ei voitu ottaa käsittelyyn, koska puhelin on katkaissut yhteyden.",
"Virhe!", MessageBoxButton.OK);
return;
}
assigmentActive = true;
assignments[AssignmentsListBox.SelectedIndex].IsHandler = true;
ownAssignment =assignments[AssignmentsListBox.SelectedIndex];
Thread thread = new Thread(connection.ProcessAssignment);
thread.Start(ownAssignment);
connection.AssignmentTakenForHandlingEvent += connection_AssignmentTakenForHandlingEvent;
ownAssignment.IsHandler = true;
HandleAssignmentButton.Click -= HandleAssigment_Click;
HandleAssignmentMenuItem.Click -= HandleAssigment_Click;
HandleAssignmentButton.Click += UnHandleAssignmentButton_Click;
HandleAssignmentMenuItem.Click += UnHandleAssignmentButton_Click;
HandleAssignmentButton.Content = "Lopeta käsittely";
HandleAssignmentMenuItem.Header = "Lopeta käsittely";
ControlsStackPanel.IsEnabled = true;
GetLocationButton.IsEnabled = true;
getVideoButton.IsEnabled = true;
ChatSendButton.IsEnabled = true;
CenterMapButton.IsEnabled = true;
ChatTextBox.IsEnabled = true;
}
///
/// Opens audio both ways.
///
void connection_AssignmentTakenForHandlingEvent()
{
audioVideoManager.EnableIncomingAudio();
audioVideoManager.EnableOutgoingAudio();
}
///
/// Switch HandleAssignmentButton content and click handler to vice versa.
/// State changing happens in own thread so it won't block ui thread. Disables buttons that
/// user cannot use when assingment is not active.
///
void UnHandleAssignmentButton_Click(object sender, RoutedEventArgs e)
{
assigmentActive = false;
if (isVideoPlaying)
StopVideoButton_Click(this, new RoutedEventArgs());
if (isMeasurementPlaying)
CancelMeasurementDataFromDevice_Click(this,new RoutedEventArgs());
connection.CancelAudioVideoMedia();
assignments.First(x => x.Guid == ownAssignment.Guid).IsHandler = false;
Thread thread = new Thread(connection.UnProcessAssignment);
thread.Start(ownAssignment);
HandleAssignmentButton.Click -= UnHandleAssignmentButton_Click;
HandleAssignmentButton.Click += HandleAssigment_Click;
HandleAssignmentButton.Click -= UnHandleAssignmentButton_Click;
HandleAssignmentMenuItem.Click += HandleAssigment_Click;
ownAssignment = null;
HandleAssignmentButton.Content = "Ota tehtävä käsittelyyn";
HandleAssignmentMenuItem.Header = "Ota tehtävä käsittelyyn";
ControlsStackPanel.IsEnabled = false;
GetLocationButton.IsEnabled = false;
getVideoButton.IsEnabled = false;
ChatSendButton.IsEnabled = false;
ChatTextBox.IsEnabled = false;
ChatInbox.Children.Clear();
audioVideoManager.DisableIncomingAudio();
audioVideoManager.DisableOutgoingAudio();
}
///
/// Asks connection to show map for mobile client.
///
private void GetMobileUserLocationButton_Click(object sender, RoutedEventArgs e)
{
if (assigmentActive)
connection.RequestAction(RemoteActionDto.requestShowLocationMap);
}
///
/// Switch getVideoButton content and click handler and to vice versa.
///
private void GetVideoButton_Click(object sender, RoutedEventArgs e)
{
if (!assigmentActive)
return;
connection.RequestAudioVideoMedia();
getVideoButton.Content = "Lopeta videokuva";
getVideoButton.Click -= GetVideoButton_Click;
getVideoButton.Click += StopVideoButton_Click;
isVideoPlaying = true;
}
///
/// Switch getVideoButton content and click handler and to vice versa.
///
private void StopVideoButton_Click(object sender, RoutedEventArgs e)
{
connection.CancelOnlyPicture();
getVideoButton.Content = "Pyydä videokuvaa";
getVideoButton.Click -= StopVideoButton_Click;
getVideoButton.Click += GetVideoButton_Click;
isVideoPlaying = false;
}
///
/// Tries first cast sender to comboboxitem and from its tag cast it to priority and then ask connection to update it.
/// If cast to comboboxitem fails it tries to cast it to menuitem and tries same again.
///
private void ChangeAssigmentPriority_Click(object sender, RoutedEventArgs e)
{
try
{
connection.ChangePriority((Assignment)AssignmentsListBox.SelectedItem,(Assignment.priorities)Convert.ToInt32(((MenuItem)sender).Tag));
}
catch(InvalidCastException)
{
connection.ChangePriority((Assignment)AssignmentsListBox.SelectedItem, (Assignment.priorities)Convert.ToInt32(((ComboBoxItem)ChangeAssigmentPriorityComboBox.SelectedItem).Tag));
}
catch(NullReferenceException)
{
return;
}
}
/// Niko Mononen
///
/// Requests measurement data from remote device and moves to cancel request state. If ekg expander is collapsed -> expands it.
///
private void RequestMeasurementDataFromDevice_Click(object sender, RoutedEventArgs e)
{
if (!assigmentActive)
return;
// Request list of instruments. Move somewhere else. Returnvalues in ConnectionDto Instrumentarray
connection.RequestAction(RemoteActionDto.requestInstrumentList);
MeasurementDataExpander.IsExpanded = true;
FirstMeasurementPacket = true;
MeasurementDataGrid.Children.Clear();
measurementGraph = new Graph(MeasurementDataGrid);
try
{
connection.RequestMeasurementData(new MeasurementInstrumentDto());
RequestMeasurementDataButton.Content = "Lopeta";
RequestMeasurementDataButton.Click -= RequestMeasurementDataFromDevice_Click;
RequestMeasurementDataButton.Click += CancelMeasurementDataFromDevice_Click;
isMeasurementPlaying = true;
}
catch(Exception)
{
return;
}
}
/// Niko Mononen
///
/// Request cancel from mobile client and returns to request start mode.
///
private void CancelMeasurementDataFromDevice_Click(object sender, RoutedEventArgs e)
{
try
{
connection.CancelMeasurementData(new MeasurementInstrumentDto());
RequestMeasurementDataButton.Content = "Pyydä";
RequestMeasurementDataButton.Click -= CancelMeasurementDataFromDevice_Click;
RequestMeasurementDataButton.Click += RequestMeasurementDataFromDevice_Click;
isMeasurementPlaying = false;
}
catch(Exception)
{
return;
}
}
/// Niko Mononen
///
/// Adds points to graph from bytes array and scrolls to end of it.
///
/// Where we want measurementdata
/// The data
/// Samples per second
private void MeasurementDataReceived(object sender, EventArgs e, MeasurementInstrumentDto instrument, byte[] measurementData,
int dataSamplesPerSecond)
{
measurementGraph.SetBytesPerSecond(dataSamplesPerSecond*instrument.DataSampleSize);
for (int i = (FirstMeasurementPacket || (!FirstMeasurementPacket && instrument.HeaderRepeatsOnEveryPacket))?
instrument.DataHeaderLength : 0; i < measurementData.Length; i++)
{
//TODO SCALE data
measurementGraph.AddPoint((int) measurementData[i]);
}
measurementGraph.ScrollToEnd();
FirstMeasurementPacket = false;
}
///
/// Only works if chattextbox is not empty and there is active assignment if not, returns.
/// This just sends message to mobile client and then clears input box.
///
private void ChatSendButton_Click(object sender, RoutedEventArgs e)
{
if (ChatTextBox.Text.Length == 0 || !assigmentActive)
return;
connection.SendTextMessage(ChatTextBox.Text);
ChatTextBox.Text = "";
}
/// Veli-Mikko Puupponen
///
/// Displays the incoming "video" pictures from the mobile emergency
/// client in the Image named ImageElement defined in the MainWindow.xaml
///
/// Image of any common format in a byte array
private void IncomingPictureHandler(object sender, byte[] image)
{
if (image == null)
return;
BitmapImage bitmapSource = null;
Application.Current.Dispatcher.Invoke((Action)delegate
{
try
{
using (var imageStream = new MemoryStream(image))
{
bitmapSource = new BitmapImage();
bitmapSource.BeginInit();
bitmapSource.CacheOption = BitmapCacheOption.OnLoad;
bitmapSource.StreamSource = imageStream;
bitmapSource.EndInit();
bitmapSource.Freeze();
}
ImageElement.Source = bitmapSource;
}
catch (InvalidOperationException e)
{
Debug.WriteLine("MainWindow IncomingPictureHandler"+e.ToString());
}
});
}
///
/// If key pressed is enter clicks chats send button.
///
private void ChatTextBox_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
ChatSendButton_Click(this, new RoutedEventArgs());
}
///
/// Transforms imageElemnt center point to origo and then rotates it 90 degrees and then transforms
/// it back to same x,y position.
///
private void RotateVideoToLeftButton_Click(object sender, RoutedEventArgs e)
{
if (!assigmentActive)
return;
angle += 90;
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(new TranslateTransform(-ImageElement.ActualWidth/2, -ImageElement.ActualHeight/2));
transformGroup.Children.Add(new RotateTransform(angle));
transformGroup.Children.Add(new TranslateTransform(ImageElement.ActualWidth / 2, ImageElement.ActualHeight / 2));
ImageElement.RenderTransform = transformGroup;
}
///
/// Transforms imageElemnt center point to origo and then rotates it -90 degrees and then transforms
/// it back to same x,y position.
///
private void RotateVideoToRightButton_Click(object sender, RoutedEventArgs e)
{
if (!assigmentActive)
return;
angle -= 90;
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(new TranslateTransform(-ImageElement.ActualWidth / 2, -ImageElement.ActualHeight / 2));
transformGroup.Children.Add(new RotateTransform(angle));
transformGroup.Children.Add(new TranslateTransform(ImageElement.ActualWidth / 2, ImageElement.ActualHeight / 2));
ImageElement.RenderTransform = transformGroup;
}
///
/// Opens settings window.
///
private void SettingsMenuItem_Click(object sender, RoutedEventArgs e)
{
settingsWindow = new SettingsWindow();
settingsWindow.Show();
}
///
/// Changes expander header to "avaa".
///
private void MeasurementDataExpander_Collapsed(object sender, RoutedEventArgs e)
{
MeasurementDataExpander.Header = "Avaa";
}
///
/// Changes expander header to "piilota".
///
private void MeasurementDataExpander_Expanded(object sender, RoutedEventArgs e)
{
MeasurementDataExpander.Header = "Piilota";
}
}
}