Skip to content

Commit

Permalink
Refactored map out of POIViewController and added route overlays! Nee…
Browse files Browse the repository at this point in the history
…ds testing.
  • Loading branch information
elliottwilliams committed Jun 4, 2017
1 parent c7377c9 commit 491ba70
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 179 deletions.
4 changes: 4 additions & 0 deletions Proper.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
26C7FB561ED16DCE006B7101 /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C7FB551ED16DCE006B7101 /* ReactiveSwift.framework */; };
26CDF95B1D7E58CD00E53F82 /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CDF95A1D7E58CD00E53F82 /* TestError.swift */; };
26CDF95D1D7E59C700E53F82 /* Model+fixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CDF95C1D7E59C700E53F82 /* Model+fixture.swift */; };
26D6B1FD1EDD03C70033DAB9 /* POIMapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D6B1FC1EDD03C70033DAB9 /* POIMapViewController.swift */; };
26DFB67C1C35DFA5006A8AE0 /* StationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 26DFB67B1C35DFA5006A8AE0 /* StationTableViewCell.xib */; };
26E038561D8DE9ED0006B265 /* Curry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26E038501D8DE9ED0006B265 /* Curry.framework */; };
26E038571D8DE9ED0006B265 /* Dwifft.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26E038511D8DE9ED0006B265 /* Dwifft.framework */; };
Expand Down Expand Up @@ -235,6 +236,7 @@
26D3E2281D28E24B00F85712 /* Station.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Station.swift; sourceTree = "<group>"; };
26D3E22A1D28E25200F85712 /* Vehicle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = "<group>"; };
26D3E22E1D29BBA700F85712 /* MutableModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = MutableModel.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
26D6B1FC1EDD03C70033DAB9 /* POIMapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = POIMapViewController.swift; sourceTree = "<group>"; };
26DBCF481D3FF59A00B22562 /* EventLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventLog.swift; sourceTree = "<group>"; };
26DFB6751C35A7F7006A8AE0 /* ScheduleRail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduleRail.swift; sourceTree = "<group>"; };
26DFB67B1C35DFA5006A8AE0 /* StationTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = StationTableViewCell.xib; sourceTree = "<group>"; };
Expand Down Expand Up @@ -345,6 +347,7 @@
264782621C309F7A009D9AD0 /* Controllers */ = {
isa = PBXGroup;
children = (
26D6B1FC1EDD03C70033DAB9 /* POIMapViewController.swift */,
26E7B5441DAFC0AD00AE50E1 /* POIViewController.swift */,
263D36361DB2784700FC7FF5 /* POITableViewController.swift */,
26A424B51BD2C1320035754F /* StationViewController.swift */,
Expand Down Expand Up @@ -803,6 +806,7 @@
26C7FB3A1ED16D6F006B7101 /* MutableModel.swift in Sources */,
26C7FB511ED16D6F006B7101 /* ScheduleRail.swift in Sources */,
26C7FB1B1ED16D6F006B7101 /* TopicEvent.swift in Sources */,
26D6B1FD1EDD03C70033DAB9 /* POIMapViewController.swift in Sources */,
26C7FB3D1ED16D6F006B7101 /* Date.swift in Sources */,
26C7FB391ED16D6F006B7101 /* Model.swift in Sources */,
26C7FB351ED16D6F006B7101 /* MutableStation.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Proper/Controllers/ArrivalsTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class ArrivalsTableViewController: UITableViewController, ProperViewController {
// Bind vehicle attributes to a given cell
func arrivalCell(for indexPath: IndexPath) -> ArrivalTableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "arrivalCell", for: indexPath) as! ArrivalTableViewCell
let vehicle = diffCalculator.rows[indexPath.row]
_ = diffCalculator.rows[indexPath.row]

// cell.apply(vehicle)
return cell
Expand Down
235 changes: 235 additions & 0 deletions Proper/Controllers/POIMapViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
//
// POIMapViewController.swift
// Proper
//
// Created by Elliott Williams on 5/29/17.
// Copyright © 2017 Elliott Williams. All rights reserved.
//

import UIKit
import ReactiveSwift
import MapKit
import Result

class POIMapViewController: UIViewController, ProperViewController {
typealias DisposableType = ScopedDisposable<CompositeDisposable>

var map: MKMapView { return self.view as! MKMapView }
let onSelect: Action<MutableStation, (), NoError>
let routes: Property<Set<MutableRoute>>

// Mutable properties that can be set by the map to impact the POI search region.
let center: MutableProperty<Point>
let zoom: MutableProperty<CLLocationDistance>


let isUserLocation: Property<Bool>
var staticCenter: MKPointAnnotation? = nil

fileprivate var polylines = [MutableRoute: MKPolyline]()
fileprivate var routeForPolyline = [MKPolyline: MutableRoute]()

var connection: ConnectionType
var disposable = ScopedDisposable(CompositeDisposable())

init(center: MutableProperty<Point>,
zoom: MutableProperty<CLLocationDistance>,
routes: Property<Set<MutableRoute>>,
onSelect: Action<MutableStation, (), NoError>,
isUserLocation: Property<Bool>,
connection: ConnectionType = Connection.cachedInstance)
{
self.center = center
self.zoom = zoom
self.routes = routes
self.onSelect = onSelect
self.isUserLocation = isUserLocation
self.connection = connection
super.init(nibName: nil, bundle: nil)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: Lifecycle

override func loadView() {
view = MKMapView()
view.translatesAutoresizingMaskIntoConstraints = false
}

override func viewDidLoad() {
map.delegate = self
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

disposable += center.producer.combineLatest(with: zoom.producer).startWithValues { point, zoom in
// Update the map as the center and zoom level changes. Center is expected to change following device location.
let coordinate = CLLocationCoordinate2D(point: point)
self.map.setCenter(coordinate, animated: true)
let boundingRegion = MKCoordinateRegionMakeWithDistance(coordinate, zoom, zoom)
self.map.setRegion(self.map.regionThatFits(boundingRegion), animated: true)
self.staticCenter?.coordinate = coordinate
}

disposable += isUserLocation.producer.startWithValues { value in
// Depending on whether the map is tracking user location, show the appropriate annotation.
if value {
self.map.showsUserLocation = true
self.staticCenter = nil
} else {
// Clear current center annotation.
self.map.showsUserLocation = false
self.staticCenter.map(self.map.removeAnnotation)

// Create a new center annotation.
let point = MKPointAnnotation()
point.coordinate = CLLocationCoordinate2D(point: self.center.value)
self.map.addAnnotation(point)
self.staticCenter = point
}
}

disposable += routes.producer.flatMap(.latest, transform: { routes -> SignalProducer<Void, NoError> in
// Show route polyline annotations, updating as the routes change. The parent POIViewController will only
// provide routes inside the current search region.
let annotationProducers = routes.map(self.polyline(for:))
return SignalProducer(annotationProducers).flatten(.merge)
}).start()
}

override func viewDidDisappear(_ animated: Bool) {
disposable.dispose()
disposable = ScopedDisposable(CompositeDisposable())
super.viewDidDisappear(animated)
}

// MARK: Map annotations

func polyline(for route: MutableRoute) -> SignalProducer<Void, NoError> {
let producer = route.canonical.producer.skipNil().map({ route -> MKPolyline in
let points = route.stations.map({ stop in stop.station.position.value })
.flatMap({ $0 })
.map({ MKMapPoint(point: $0) })
return MKPolyline(points: UnsafePointer(points), count: points.count)
}).map(Optional.some)

return producer.skipRepeats(==).combinePrevious(nil).on(value: { prev, next in
if let prev = prev {
self.map.removeAnnotation(prev)
self.routeForPolyline[prev] = nil
}
if let next = next {
self.map.addAnnotation(next)
self.routeForPolyline[next] = route
}
}).map({ _, _ in () })
}

func annotations(for station: MutableStation) -> [POIStationAnnotation] {
return self.map.annotations.flatMap({ $0 as? POIStationAnnotation })
.filter({ $0.station == station })
}
func stations(within range: CountableClosedRange<Int>) -> [POIStationAnnotation] {
return map.annotations.flatMap({ ($0 as? POIStationAnnotation) })
.filter({ range.contains($0.index) })
}
func stations(from idx: Int) -> [POIStationAnnotation] {
return map.annotations.flatMap({ ($0 as? POIStationAnnotation) })
.filter({ $0.index >= idx })
}

func addAnnotation(for station: MutableStation, at idx: Int) {
guard let position = station.position.value else {
return
}
let distanceString = POIViewModel.distanceString(self.center.producer.map({ ($0, position) }))
let annotation = POIStationAnnotation(station: station,
locatedAt: position,
index: idx,
distance: distanceString)
stations(from: idx).forEach { $0.index += 1 }
map.addAnnotation(annotation)
}

func deleteAnnotations(for station: MutableStation) {
let annotations = self.annotations(for: station)
let idx = annotations.min(by: { $0.index < $1.index }).map({ $0.index })!
map.removeAnnotations(annotations)
self.stations(from: idx+1).forEach { $0.index -= 1 }
}

func reorderAnnotations(withIndex fi: Int, to ti: Int) {
if fi < ti {
self.stations(within: fi...ti).forEach { annotation in
switch annotation.index {
case fi: annotation.index = ti
case _: annotation.index -= 1
}
}
} else {
self.stations(within: ti...fi).forEach { annotation in
switch annotation.index {
case fi: annotation.index = ti
case _: annotation.index += 1
}
}
}
}
}

// MARK: - Map view delegate
extension POIMapViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let annotation = annotation as? POIStationAnnotation {
let view =
mapView.dequeueReusableAnnotationView(withIdentifier: "stationAnnotation") as? POIStationAnnotationView
?? POIStationAnnotationView(annotation: annotation, reuseIdentifier: "stationAnnotation")
view.apply(annotation: annotation)
return view
}

// Returning nil causes the map to use a default annotation.
return nil
}

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
// Render a route on the map.
if let line = overlay as? MKPolyline, let route = routeForPolyline[line] {
let renderer = MKPolylineRenderer()
disposable += route.color.producer.startWithValues { renderer.strokeColor = $0 }
renderer.lineWidth = 5.0
return renderer
}

// The default:
return MKOverlayRenderer()
}

func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
if let station = ((view as? POIStationAnnotationView)?.annotation as? POIStationAnnotation)?.station {
self.parent?.performSegue(withIdentifier: "showStation", sender: station)
}
}

func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {

if let annotation = (view as? POIStationAnnotationView)?.annotation as? POIStationAnnotation {
disposable += onSelect.apply(annotation.station).start()
// let section = table.dataSource.index(of: annotation.station)
// let row = (table.dataSource.arrivals[section].isEmpty) ? NSNotFound : 0
// table.tableView.scrollToRow(at: IndexPath(row: row, section: section), at: .top,
// animated: true)
}
}

func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
// Update the search region by modifying `center` and `zoom`.
// Called repeatedly as the map is being scrolled. We'll need to throttle the search itself or the property updates
// to prevent from overwhelming the system with search requests.
// map.visibleMapRect
}
}
Loading

0 comments on commit 491ba70

Please sign in to comment.