/// 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"; } } }