584 lines
21 KiB
Swift
584 lines
21 KiB
Swift
//
|
||
// MainViewController.swift
|
||
// Guibe
|
||
//
|
||
// Created by Max Hunt on 28/05/2019.
|
||
// Copyright © 2019 8. All rights reserved.
|
||
//
|
||
|
||
import UIKit
|
||
import AVKit
|
||
import MapKit
|
||
import CoreLocation
|
||
import AVFoundation
|
||
|
||
|
||
class MainViewController: UIViewController, AVAudioPlayerDelegate, myProtocol {
|
||
|
||
|
||
// DELETE
|
||
let defaults = UserDefaults.standard
|
||
// DELETE
|
||
|
||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||
|
||
var etaTime: Int = -1
|
||
|
||
let locationManager = CLLocationManager()
|
||
var currentCoordinate: CLLocationCoordinate2D!
|
||
|
||
var player = AVAudioPlayer()
|
||
|
||
var steps = [MKRoute.Step]()
|
||
|
||
var stepCounter = 0
|
||
|
||
var previousDistanceToWaypoint: Double = 1000
|
||
var distanceToNextCoord: Double = 1000
|
||
|
||
var startedNavigation: Bool = false
|
||
|
||
var textSteps: [MKRoute.Step]?
|
||
|
||
|
||
// OUTLETS--------------OUTLETS
|
||
@IBOutlet weak var menuWindowView: UIView!
|
||
@IBOutlet weak var menuView: UIView!
|
||
@IBOutlet weak var menuBtn: UIButton!
|
||
@IBOutlet weak var dismissBtn: UIButton!
|
||
@IBOutlet weak var howToUseGuibeBtn: UIButton!
|
||
// ---------------------
|
||
@IBOutlet weak var searchBarBg: UIButton!
|
||
@IBOutlet weak var searchView: UIView!
|
||
@IBOutlet weak var searchBar: UISearchBar!
|
||
@IBOutlet weak var keybDismissBtn: UIButton!
|
||
// ---------------------
|
||
@IBOutlet weak var mapView: MKMapView!
|
||
// ---------------------
|
||
@IBOutlet weak var etaView: UIView!
|
||
@IBOutlet weak var startNaviBtn: UIButton!
|
||
@IBOutlet weak var etaLabel: UILabel!
|
||
@IBOutlet weak var startNaviImg: UIImageView!
|
||
@IBOutlet weak var xNaviBtn: UIButton!
|
||
// ---------------------
|
||
@IBOutlet weak var cancelBtn: UIButton!
|
||
// ---------------------
|
||
@IBOutlet weak var persEtaView: UIView!
|
||
@IBOutlet weak var persEtaLbl: UILabel!
|
||
// ---------------------
|
||
@IBOutlet weak var headingButton: UIButton!
|
||
// ---------------------
|
||
@IBOutlet weak var naviFinishedView: UIView!
|
||
@IBOutlet weak var arrivedAtDestinationBtn: UIButton!
|
||
|
||
@IBOutlet weak var debugLbl: UILabel!
|
||
// OUTLETS--------------OUTLETS
|
||
|
||
// ACTIONS--------------ACTIONS
|
||
@IBAction func menuBtnPressed(_ sender: Any) {
|
||
UIView.animate(withDuration: 0.2, animations: {self.menuWindowView.alpha = 1.0})
|
||
dismissBtn.isHidden = false
|
||
}
|
||
@IBAction func micBtnPressed(_ sender: Any) {
|
||
}
|
||
@IBAction func dismissBtnPressed(_ sender: Any) {
|
||
UIView.animate(withDuration: 0.2, animations: {self.menuWindowView.alpha = 0.0})
|
||
dismissBtn.isHidden = true
|
||
}
|
||
@IBAction func settingsBtnPressed(_ sender: Any) {
|
||
}
|
||
@IBAction func followBtnPressed(_ sender: Any) {
|
||
self.mapView.userTrackingMode = .follow
|
||
}
|
||
@IBAction func headingBtnPressed(_ sender: Any) {
|
||
self.mapView.userTrackingMode = .followWithHeading
|
||
}
|
||
// --------------------------
|
||
@IBAction func startNaviPressed(_ sender: Any) {
|
||
self.appDelegate.naviStarted = true
|
||
self.startedNavigation = true
|
||
self.stepCounter += 1
|
||
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||
self.searchBar.text = ""
|
||
self.etaView.alpha = 0.0
|
||
self.searchView.alpha = 0.0
|
||
self.cancelBtn.alpha = 1.0
|
||
self.persEtaView.alpha = 1.0
|
||
}
|
||
|
||
UIView.animate(withDuration: 0.5, animations: {self.startNaviImg.alpha = 1.0})
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||
UIView.animate(withDuration: 0.5, animations: {self.startNaviImg.alpha = 0.0})
|
||
self.playStart()
|
||
}
|
||
}
|
||
@IBAction func xNaviPressed(_ sender: Any) {
|
||
UIView.animate(withDuration: 0.5, animations: {self.etaView.alpha = 0.0})
|
||
mapView.removeOverlays(mapView.overlays)
|
||
}
|
||
@IBAction func cancelBtnPressed(_ sender: Any) {
|
||
self.stepCounter = 0
|
||
self.startedNavigation = false
|
||
self.appDelegate.naviStarted = false
|
||
mapView.removeOverlays(mapView.overlays)
|
||
UIView.animate(withDuration: 0.3, animations: {
|
||
self.searchView.alpha = 1.0
|
||
self.cancelBtn.alpha = 0.0
|
||
self.persEtaView.alpha = 0.0
|
||
})
|
||
self.debugLbl.text = "–––"
|
||
|
||
}
|
||
|
||
@IBAction func keybDismissBtnPressed(_ sender: Any) {
|
||
searchBar.endEditing(true)
|
||
self.keybDismissBtn.isHidden = true
|
||
}
|
||
|
||
// --------------------------
|
||
@IBAction func arrivedAtDestinationBtnPressed(_ sender: Any) {
|
||
UIView.animate(withDuration: 1, animations: {self.naviFinishedView.alpha = 0.0})
|
||
}
|
||
|
||
@IBAction func xDebug(_ sender: Any) {
|
||
finishedNavigationSuccessfully()
|
||
}
|
||
// ACTIONS--------------ACTIONS
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
|
||
menuWindowView.backgroundColor = UIColor.secondarySystemBackground
|
||
|
||
let debugVar = defaults.integer(forKey: "debug")
|
||
|
||
if debugVar == 2 {
|
||
debugLbl.alpha = 1.0
|
||
}
|
||
else {
|
||
debugLbl.alpha = 0.0
|
||
}
|
||
|
||
searchBar.delegate = self
|
||
searchBarBg.layer.shadowColor = UIColor.black.cgColor
|
||
searchBarBg.layer.cornerRadius = 10
|
||
searchBarBg.layer.shadowOffset = CGSize(width: 5, height: 7)
|
||
searchBarBg.layer.shadowRadius = 10
|
||
searchBarBg.layer.shadowOpacity = 0.2
|
||
|
||
|
||
|
||
|
||
keybDismissBtn.isHidden = true
|
||
|
||
menuWindowView.alpha = 0.0
|
||
menuWindowView.layer.shadowColor = UIColor.black.cgColor
|
||
menuWindowView.layer.cornerRadius = 13
|
||
menuWindowView.layer.shadowOffset = CGSize(width: 5, height: 7)
|
||
menuWindowView.layer.shadowRadius = 10
|
||
menuWindowView.layer.shadowOpacity = 0.2
|
||
|
||
dismissBtn.isHidden = true
|
||
|
||
mapView.delegate = self
|
||
|
||
etaView.alpha = 0.0
|
||
etaView.layer.shadowColor = UIColor.black.cgColor
|
||
etaView.layer.shadowOffset = CGSize(width: 0, height: -10)
|
||
etaView.layer.shadowRadius = 10
|
||
etaView.layer.shadowOpacity = 0.2
|
||
|
||
cancelBtn.alpha = 0.0
|
||
// cancelBtn.isHidden = true
|
||
cancelBtn.layer.shadowColor = UIColor.black.cgColor
|
||
cancelBtn.layer.cornerRadius = 15
|
||
cancelBtn.layer.shadowOffset = CGSize(width: 3, height: 6)
|
||
cancelBtn.layer.shadowRadius = 10
|
||
cancelBtn.layer.shadowOpacity = 0.2
|
||
|
||
startNaviBtn.layer.shadowColor = UIColor.black.cgColor
|
||
startNaviBtn.layer.cornerRadius = 13
|
||
startNaviBtn.layer.shadowOffset = CGSize(width: 5, height: 7)
|
||
startNaviBtn.layer.shadowRadius = 10
|
||
startNaviBtn.layer.shadowOpacity = 0.2
|
||
|
||
locationManager.delegate = self
|
||
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||
locationManager.startUpdatingLocation()
|
||
mapView.userTrackingMode = .follow
|
||
|
||
startNaviImg.alpha = 0.0
|
||
|
||
persEtaView.alpha = 0.0
|
||
persEtaView.layer.shadowColor = UIColor.black.cgColor
|
||
persEtaView.layer.cornerRadius = 7
|
||
persEtaView.layer.shadowOffset = CGSize(width: 5, height: 7)
|
||
persEtaView.layer.shadowRadius = 7
|
||
persEtaView.layer.shadowOpacity = 0.2
|
||
|
||
headingButton.layer.shadowColor = UIColor.black.cgColor
|
||
headingButton.layer.cornerRadius = 7
|
||
headingButton.layer.shadowOffset = CGSize(width: 5, height: 7)
|
||
headingButton.layer.shadowRadius = 7
|
||
headingButton.layer.shadowOpacity = 0.2
|
||
|
||
naviFinishedView.alpha = 0.0
|
||
|
||
arrivedAtDestinationBtn.layer.shadowColor = UIColor.black.cgColor
|
||
arrivedAtDestinationBtn.layer.cornerRadius = 50
|
||
arrivedAtDestinationBtn.layer.shadowOffset = CGSize(width: 5, height: 7)
|
||
arrivedAtDestinationBtn.layer.shadowRadius = 50
|
||
arrivedAtDestinationBtn.layer.shadowOpacity = 0.6
|
||
|
||
|
||
|
||
menuBtn.accessibilityLabel = "Menu"
|
||
headingButton.accessibilityLabel = "Heading"
|
||
searchBar.accessibilityLabel = "Search destination"
|
||
cancelBtn.accessibilityLabel = "Cancel navigation"
|
||
xNaviBtn.accessibilityLabel = "Cancel"
|
||
startNaviBtn.accessibilityLabel = "Start Navigation"
|
||
dismissBtn.accessibilityLabel = "Dismiss menu"
|
||
keybDismissBtn.accessibilityLabel = "Dismiss keyboard"
|
||
mapView.accessibilityElementsHidden = true
|
||
startNaviImg.accessibilityLabel = "Follow the vibe"
|
||
arrivedAtDestinationBtn.accessibilityLabel = "End Navigation"
|
||
howToUseGuibeBtn.accessibilityLabel = "How to use ghibe"
|
||
|
||
} //END OF VIEW DID LOAD
|
||
|
||
func getDirections(to destination: MKMapItem) {
|
||
let sourcePlacemark = MKPlacemark(coordinate: currentCoordinate)
|
||
let sourceMapItem = MKMapItem(placemark: sourcePlacemark)
|
||
|
||
let directionsRequest = MKDirections.Request()
|
||
directionsRequest.source = sourceMapItem
|
||
directionsRequest.destination = destination
|
||
directionsRequest.transportType = .walking
|
||
|
||
let directions = MKDirections(request: directionsRequest)
|
||
directions.calculate { (response, _) in
|
||
guard let response = response else { return }
|
||
|
||
guard let primaryRoute = response.routes.first else { return }
|
||
|
||
self.mapView.addOverlay(primaryRoute.polyline)
|
||
|
||
|
||
self.steps = primaryRoute.steps
|
||
for i in 0 ..< primaryRoute.steps.count {
|
||
let step = primaryRoute.steps[i]
|
||
// print(step.instructions)
|
||
// print(step.distance)
|
||
// -----------------------------Geofencing setup
|
||
let region = CLCircularRegion(center: step.polyline.coordinate,
|
||
radius: 15,
|
||
identifier: "\(i)")
|
||
// self.locationManager.startMonitoring(for: region)
|
||
// -----------------------------Geofencing setup
|
||
let circle = MKCircle(center: region.center, radius: region.radius)
|
||
self.mapView.addOverlay(circle)
|
||
}
|
||
|
||
let travelTime = Int(primaryRoute.expectedTravelTime/60)
|
||
let startDate = Date()
|
||
let ETATime = startDate.addingTimeInterval(TimeInterval(travelTime*60))
|
||
let ETAVarLbl = DateFormatter.localizedString(from: ETATime, dateStyle: DateFormatter.Style.none, timeStyle: DateFormatter.Style.short)
|
||
|
||
self.textSteps = primaryRoute.steps
|
||
|
||
self.appDelegate.textSteps = self.textSteps
|
||
self.appDelegate.currentStep = self.stepCounter
|
||
|
||
// let ivc = self.storyboard?.instantiateViewController(withIdentifier: "ivc") as! InstructionsViewController
|
||
// let wivc = WrittenInstructionsViewController()
|
||
// wivc.stepBySteps = self.textSteps
|
||
// wivc.currentStep = self.stepCounter
|
||
|
||
// ivc.stepsText = self.textSteps
|
||
// ivc.currentStep = self.stepCounter
|
||
|
||
|
||
self.etaLabel.text = "\(ETAVarLbl)"
|
||
self.persEtaLbl.text = "\(ETAVarLbl)"
|
||
UIView.animate(withDuration: 0.3, animations: {self.etaView.alpha = 1.0})
|
||
|
||
// self.startedNavigation = true
|
||
// self.stepCounter += 1
|
||
}
|
||
}
|
||
|
||
func startNavigation(toPlace: MKMapItem){
|
||
getDirections(to: toPlace)
|
||
}
|
||
|
||
} //END OF CLASS
|
||
|
||
|
||
extension MainViewController: CLLocationManagerDelegate {
|
||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||
guard let currentLocation = locations.first else { return }
|
||
currentCoordinate = currentLocation.coordinate
|
||
|
||
|
||
if startedNavigation == true {
|
||
let nextStep = steps[stepCounter]
|
||
let nextCoord = CLLocation(latitude: nextStep.polyline.coordinate.latitude, longitude: nextStep.polyline.coordinate.longitude)
|
||
distanceToNextCoord = currentLocation.distance(from: nextCoord)
|
||
|
||
if distanceToNextCoord < previousDistanceToWaypoint {
|
||
previousDistanceToWaypoint = currentLocation.distance(from: nextCoord)
|
||
}
|
||
if distanceToNextCoord > previousDistanceToWaypoint + 10 {
|
||
previousDistanceToWaypoint = distanceToNextCoord - 5
|
||
playTurnArd()
|
||
}
|
||
if distanceToNextCoord < 15 {
|
||
stepCounter += 1
|
||
if stepCounter < steps.count {
|
||
// let currentStep = steps[stepCounter]
|
||
let message = "\(steps[stepCounter-1].instructions)"
|
||
let maneuverCommand = String(message.prefix(10))
|
||
if player.volume != 1.0 {player.setVolume(1.0, fadeDuration: 1.0)}
|
||
switch (maneuverCommand) {
|
||
case "Turn right":
|
||
playRight()
|
||
break;
|
||
case "Bear right":
|
||
playRight()
|
||
break;
|
||
case "Turn left ":
|
||
playLeft()
|
||
break;
|
||
case "Bear left":
|
||
playLeft()
|
||
break;
|
||
case "The destin":
|
||
playDone()
|
||
finishedNavigationSuccessfully()
|
||
|
||
default:
|
||
playErr()
|
||
break;
|
||
}
|
||
|
||
// let speechUtterance = AVSpeechUtterance(string: message)
|
||
// speechSynthesizer.speak(speechUtterance)
|
||
previousDistanceToWaypoint = 1000
|
||
} else {
|
||
// let speechUtterance = AVSpeechUtterance(string: message)
|
||
// speechSynthesizer.speak(speechUtterance)
|
||
playDone()
|
||
finishedNavigationSuccessfully()
|
||
stepCounter = 0
|
||
previousDistanceToWaypoint = 1000
|
||
searchBar.isHidden = false
|
||
// locationManager.monitoredRegions.forEach({ self.locationManager.stopMonitoring(for: $0) })
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
func finishedNavigationSuccessfully(){
|
||
self.stepCounter = 0
|
||
self.startedNavigation = false
|
||
self.appDelegate.naviStarted = false
|
||
mapView.removeOverlays(mapView.overlays)
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1){
|
||
UIView.animate(withDuration: 0.7, animations: {
|
||
self.searchView.alpha = 1.0
|
||
self.cancelBtn.alpha = 0.0
|
||
self.persEtaView.alpha = 0.0})
|
||
}
|
||
UIView.animate(withDuration: 1, animations: {self.naviFinishedView.alpha = 1.0})
|
||
//Animate alpha doneview and deanimate it
|
||
|
||
}
|
||
|
||
func playStart(){
|
||
let path = Bundle.main.path(forResource: "guibe_begin", ofType : "mp3")!
|
||
self.debugLbl.text = String(path.suffix(15))
|
||
let url = URL(fileURLWithPath : path)
|
||
do {
|
||
player = try AVAudioPlayer(contentsOf: url)
|
||
player.delegate = self
|
||
player.play()
|
||
} catch {}
|
||
}
|
||
|
||
func playRight(){
|
||
// currentStepLbl.text = "COM: RIGHT"
|
||
|
||
let var1 = "guibe_left"
|
||
let var2 = "guibe_right"
|
||
|
||
var path: String = ""
|
||
|
||
if defaults.integer(forKey: "handSide") == 1{
|
||
if defaults.integer(forKey: "bigSide") == 1{
|
||
path = Bundle.main.path(forResource: var1, ofType : "mp3")!
|
||
}
|
||
else if defaults.integer(forKey: "bigSide") == 2 {
|
||
path = Bundle.main.path(forResource: var2, ofType : "mp3")!
|
||
}
|
||
else { errorMsg() }
|
||
}
|
||
else if defaults.integer(forKey: "handSide") == 2 {
|
||
if defaults.integer(forKey: "bigSide") == 1{
|
||
path = Bundle.main.path(forResource: var2, ofType : "mp3")!
|
||
}
|
||
else if defaults.integer(forKey: "bigSide") == 2 {
|
||
path = Bundle.main.path(forResource: var1, ofType : "mp3")!
|
||
}
|
||
else { errorMsg() }
|
||
}
|
||
else { errorMsg() }
|
||
|
||
self.debugLbl.text = String(path.suffix(15))
|
||
|
||
let url = URL(fileURLWithPath : path)
|
||
do {
|
||
player = try AVAudioPlayer(contentsOf: url)
|
||
player.delegate = self
|
||
player.play()
|
||
} catch { errorMsg() }
|
||
}
|
||
|
||
func playLeft(){
|
||
// currentStepLbl.text = "COM: LEFT"
|
||
|
||
let var1 = "guibe_right"
|
||
let var2 = "guibe_left"
|
||
|
||
var path: String = ""
|
||
|
||
if defaults.integer(forKey: "handSide") == 1{
|
||
if defaults.integer(forKey: "bigSide") == 1{
|
||
path = Bundle.main.path(forResource: var1, ofType : "mp3")!
|
||
}
|
||
else if defaults.integer(forKey: "bigSide") == 2 {
|
||
path = Bundle.main.path(forResource: var2, ofType : "mp3")!
|
||
}
|
||
else { errorMsg() }
|
||
}
|
||
else if defaults.integer(forKey: "handSide") == 2 {
|
||
if defaults.integer(forKey: "bigSide") == 1{
|
||
path = Bundle.main.path(forResource: var2, ofType : "mp3")!
|
||
}
|
||
else if defaults.integer(forKey: "bigSide") == 2 {
|
||
path = Bundle.main.path(forResource: var1, ofType : "mp3")!
|
||
}
|
||
else { errorMsg() }
|
||
}
|
||
else { errorMsg() }
|
||
|
||
self.debugLbl.text = String(path.suffix(15))
|
||
|
||
let url = URL(fileURLWithPath : path)
|
||
do {
|
||
player = try AVAudioPlayer(contentsOf: url)
|
||
player.delegate = self
|
||
player.play()
|
||
} catch { errorMsg() }
|
||
}
|
||
|
||
func playErr(){
|
||
// currentStepLbl.text = "COM: ERROR!!!"
|
||
return
|
||
}
|
||
|
||
func playTurnArd(){
|
||
// currentStepLbl.text = "COM: UTURN"
|
||
let path = Bundle.main.path(forResource: "guibe_wrongdirection", ofType : "mp3")!
|
||
self.debugLbl.text = String(path.suffix(15))
|
||
let url = URL(fileURLWithPath : path)
|
||
do {
|
||
player = try AVAudioPlayer(contentsOf: url)
|
||
player.delegate = self
|
||
player.play()
|
||
} catch {}
|
||
}
|
||
|
||
func playDone(){
|
||
// currentStepLbl.text = "COM: DONE"
|
||
let path = Bundle.main.path(forResource: "guibe_arrived", ofType : "mp3")!
|
||
self.debugLbl.text = String(path.suffix(15))
|
||
let url = URL(fileURLWithPath : path)
|
||
do {
|
||
player = try AVAudioPlayer(contentsOf: url)
|
||
player.delegate = self
|
||
player.play()
|
||
} catch {}
|
||
}
|
||
|
||
func errorMsg() {
|
||
let alert = UIAlertController(title: "Playback Error", message: "Error in either keyValues or filename", preferredStyle: .alert)
|
||
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
|
||
switch action.style{
|
||
case .default:
|
||
self.dismiss(animated: true, completion: nil)
|
||
|
||
case .cancel:
|
||
return
|
||
|
||
case .destructive:
|
||
return
|
||
}}))
|
||
self.present(alert, animated: true, completion: nil)
|
||
}
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
extension MainViewController: UISearchBarDelegate {
|
||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||
searchBar.endEditing(true) //HIDES LE KEYBOARD
|
||
self.keybDismissBtn.isHidden = true
|
||
|
||
let localSearchRequest = MKLocalSearch.Request()
|
||
localSearchRequest.naturalLanguageQuery = searchBar.text
|
||
let region = MKCoordinateRegion(center: currentCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
|
||
localSearchRequest.region = region
|
||
let localSearch = MKLocalSearch(request: localSearchRequest)
|
||
localSearch.start { (response, _) in
|
||
guard let response = response else { return }
|
||
|
||
|
||
|
||
let resultsViewController = self.storyboard?.instantiateViewController(withIdentifier: "resultsScreen") as! TableViewController
|
||
resultsViewController.searchResults = response.mapItems
|
||
resultsViewController.modalTransitionStyle = .coverVertical
|
||
resultsViewController.myProtocol = self
|
||
self.present(resultsViewController, animated: true, completion: nil)
|
||
}
|
||
}
|
||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||
self.keybDismissBtn.isHidden = false
|
||
}
|
||
}
|
||
extension MainViewController: MKMapViewDelegate {
|
||
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
||
if overlay is MKPolyline {
|
||
let renderer = MKPolylineRenderer(overlay: overlay)
|
||
renderer.strokeColor = .blue
|
||
renderer.alpha = 0.7
|
||
renderer.lineWidth = 10
|
||
return renderer
|
||
}
|
||
if overlay is MKCircle {
|
||
let renderer = MKCircleRenderer(overlay: overlay)
|
||
renderer.strokeColor = .red
|
||
renderer.fillColor = .red
|
||
renderer.alpha = 0.4
|
||
return renderer
|
||
}
|
||
return MKOverlayRenderer()
|
||
}
|
||
}
|