import Foundation import RealmSwift import DifferenceKit public class VehicleName: Object, Decodable, Cloneable { @objc public dynamic var original: String? @objc public dynamic var normalized: String? public required init(copy: VehicleName) { self.original = copy.original self.normalized = copy.normalized } required init() { } } public class VehicleBrand: Object, Decodable, Cloneable { @objc public dynamic var name: VehicleName? @objc public dynamic var logo: String? public required init(copy: VehicleBrand) { self.name = copy.name?.clone() self.logo = copy.logo } required init() { super.init() } } public class VehicleModel: Object, Decodable, Cloneable { @objc dynamic var name: VehicleName? public required init(copy: VehicleModel) { self.name = copy.name?.clone() } required init() { super.init() } } public class VehicleEngine: Object, Decodable, Cloneable { @objc public dynamic var number: String? public var volume: RealmOptional = RealmOptional(0) @objc public dynamic var powerHp: Float = 0 public var powerKw: RealmOptional = RealmOptional(0) @objc public dynamic var fuelType: String? enum CodingKeys: String, CodingKey { case number, volume, powerHp, powerKw, fuelType } required public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.number = try container.decodeIfPresent(String.self, forKey: .number) self.volume = RealmOptional(try container.decodeIfPresent(Int.self, forKey: .volume)) self.powerHp = try container.decode(Float.self, forKey: .powerHp) self.powerKw = RealmOptional(try container.decodeIfPresent(Float.self, forKey: .powerKw)) self.fuelType = try container.decodeIfPresent(String.self, forKey: .fuelType) } required init() { super.init() } public required init(copy: VehicleEngine) { self.number = copy.number self.volume = copy.volume self.powerHp = copy.powerHp self.powerKw = copy.powerKw self.fuelType = copy.fuelType } } public class VehiclePhoto: Object, Decodable, Cloneable { @objc public dynamic var brand: String? @objc public dynamic var model: String? @objc public dynamic var date: TimeInterval = 0 @objc public dynamic var url: String = "" public override var description: String { let formatter = DateFormatter() formatter.timeZone = TimeZone(identifier:"GMT") formatter.dateStyle = .medium formatter.timeStyle = .none let date = Date(timeIntervalSince1970: self.date/1000) let dateStr = formatter.string(from: date) return "\(self.brand ?? "") \(self.model ?? "") (\(dateStr))" } public required init(copy: VehiclePhoto) { self.brand = copy.brand self.model = copy.model self.date = copy.date self.url = copy.url } required init() { super.init() } } public enum OwnerType: String, CustomStringConvertible { case legal case individual public var description: String { switch self { case .legal: return NSLocalizedString("legal", comment: "Owner type") case .individual: return NSLocalizedString("individual", comment: "Owner type") } } } public enum SteeringWheelPosition: CustomStringConvertible { case left case right case unknown public var description: String { switch self { case .left: return "Left" case .right: return "Right" case .unknown: return "Unknown" } } } public class VehicleOwnershipPeriod: Object, Decodable, Cloneable { @objc public dynamic var lastOperation: String = "" @objc public dynamic var ownerType: String = "" @objc public dynamic var from: Int64 = 0 @objc public dynamic var to: Int64 = 0 @objc public dynamic var region: String? @objc public dynamic var registrationRegion: String? @objc public dynamic var locality: String? @objc public dynamic var code: String? @objc public dynamic var street: String? @objc public dynamic var building: String? @objc public dynamic var inn: String? required public init(copy: VehicleOwnershipPeriod) { self.lastOperation = copy.lastOperation self.ownerType = copy.ownerType self.from = copy.from self.to = copy.to self.region = copy.region self.registrationRegion = copy.registrationRegion self.locality = copy.locality self.code = copy.code self.street = copy.street self.building = copy.building self.inn = copy.inn } required init() { super.init() } } public final class Vehicle: Object, Decodable, Identifiable, Differentiable, Cloneable, Exportable { @objc public dynamic var brand: VehicleBrand? @objc public dynamic var model: VehicleModel? @objc public dynamic var color: String? @objc public dynamic var year: Int = 0 @objc public dynamic var category: String? @objc public dynamic var engine: VehicleEngine? @objc private dynamic var number: String = "" @objc public dynamic var currentNumber: String? @objc public dynamic var vin1: String? @objc public dynamic var vin2: String? @objc public dynamic var sts: String? @objc public dynamic var pts: String? public var isRightWheel = RealmOptional() public var isJapanese: RealmOptional = RealmOptional() @objc public dynamic var addedDate: TimeInterval = 0 @objc public dynamic var updatedDate: TimeInterval = 0 @objc public dynamic var addedBy: String = "" public var photos = List() public var ownershipPeriods = List() public var events = List() public var osagoContracts = List() public var ads = List() public var notes = List() @objc public dynamic var debugInfo: DebugInfo? @objc public dynamic var synchronized: Bool = true lazy var formatter: DateFormatter = { let f = DateFormatter() f.dateStyle = .medium f.timeStyle = .medium return f }() public var differenceIdentifier: String { self.number } public func isContentEqual(to source: Vehicle) -> Bool { return self.number == source.number && self.addedDate == source.addedDate && self.updatedDate == source.updatedDate } enum CodingKeys: String, CodingKey { case brand case model case color case year case category case engine case number case currentNumber case vin1 case vin2 case sts case pts case isRightWheel case isJapanese case addedDate case updatedDate case addedBy case photos case ownershipPeriods case events case osagoContracts case ads case notes case debugInfo } required public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.brand = try container.decodeIfPresent(VehicleBrand.self, forKey: .brand) self.model = try container.decodeIfPresent(VehicleModel.self, forKey: .model) self.color = try container.decodeIfPresent(String.self, forKey: .color) self.year = try container.decodeIfPresent(Int.self, forKey: .year) ?? 0 self.category = try container.decodeIfPresent(String.self, forKey: .category) self.number = try container.decode(String.self, forKey: .number) self.engine = try container.decodeIfPresent(VehicleEngine.self, forKey: .engine) self.currentNumber = try container.decodeIfPresent(String.self, forKey: .currentNumber) self.vin1 = try container.decodeIfPresent(String.self, forKey: .vin1) self.vin2 = try container.decodeIfPresent(String.self, forKey: .vin2) self.sts = try container.decodeIfPresent(String.self, forKey: .sts) self.pts = try container.decodeIfPresent(String.self, forKey: .pts) self.isRightWheel = RealmOptional(try container.decodeIfPresent(Bool.self, forKey: .isRightWheel)) self.isJapanese = RealmOptional(try container.decodeIfPresent(Bool.self, forKey: .isJapanese)) self.addedDate = (try container.decode(TimeInterval.self, forKey: .addedDate))/1000 self.addedBy = try container.decode(String.self, forKey: .addedBy) self.debugInfo = try container.decodeIfPresent(DebugInfo.self, forKey: .debugInfo) self.updatedDate = (try container.decode(TimeInterval.self, forKey: .updatedDate))/1000 if let photosArray = try container.decodeIfPresent([VehiclePhoto].self, forKey: .photos) { self.photos.append(objectsIn: photosArray) } if let ownersipsArray = try container.decodeIfPresent([VehicleOwnershipPeriod].self, forKey: .ownershipPeriods) { self.ownershipPeriods.append(objectsIn: ownersipsArray) } if let eventsArray = try container.decodeIfPresent([VehicleEvent].self, forKey: .events) { self.events.append(objectsIn: eventsArray.sorted { $0.date > $1.date }) } if let osago = try container.decodeIfPresent([Osago].self, forKey: .osagoContracts) { self.osagoContracts.append(objectsIn: osago) } if let ads = try container.decodeIfPresent([VehicleAd].self, forKey: .ads) { self.ads.append(objectsIn: ads) } if let notes = try container.decodeIfPresent([VehicleNote].self, forKey: .notes) { self.notes.append(objectsIn: notes) } // All vehicles received from API are synchronized by definition self.synchronized = true } required init() { super.init() } public init(_ number: String) { self.number = number self.addedDate = Date().timeIntervalSince1970 self.updatedDate = self.addedDate self.synchronized = false } public func getNumber() -> String { return self.number } public override static func primaryKey() -> String? { return "number" } public override class func ignoredProperties() -> [String] { return ["id", "identifier", "differenceIdentifier", "formatter"] } public var unrecognized: Bool { return self.brand == nil } public var outdated: Bool { if let current = self.currentNumber { return current != self.number } else { return false } } // MARK: - Cloneable required public init(copy: Vehicle) { self.brand = copy.brand self.model = copy.model self.color = copy.color self.year = copy.year self.category = copy.category self.engine = copy.engine self.number = copy.number self.currentNumber = copy.currentNumber self.vin1 = copy.vin1 self.vin2 = copy.vin2 self.sts = copy.sts self.pts = copy.pts self.isRightWheel = copy.isRightWheel self.isJapanese = copy.isJapanese self.addedDate = copy.addedDate self.addedBy = copy.addedBy self.updatedDate = copy.updatedDate let photos = List() photos.append(objectsIn: copy.photos.map { $0.clone() }) self.photos = photos let ownerships = List() ownerships.append(objectsIn: copy.ownershipPeriods.map { $0.clone() }) self.ownershipPeriods = ownerships let events = List() events.append(objectsIn: copy.events.map { $0.clone() }.sorted { $0.date > $1.date }) self.events = events let osago = List() osago.append(objectsIn: copy.osagoContracts.map { $0.clone() }) self.osagoContracts = osago let ads = List() ads.append(objectsIn: copy.ads.map { $0.clone() }) self.ads = ads let notes = List() notes.append(objectsIn: copy.notes.map { $0.clone() }) self.notes = notes self.synchronized = copy.synchronized } public func shallowClone() -> Vehicle { let vehicle = Vehicle() vehicle.number = self.number vehicle.currentNumber = self.currentNumber vehicle.addedDate = self.addedDate vehicle.updatedDate = self.updatedDate vehicle.brand = self.brand vehicle.synchronized = self.synchronized let notes = List() notes.append(objectsIn: self.notes.map { $0.clone() }) vehicle.notes = notes return vehicle } // MARK: - Exportable public static var csvHeader: String { return "Plate Number, Model, Color, Year, Power (HP), Events, Owners, VIN, STS, PTS, Added Date, Updated date, Locations, Notes" } public var csvLine: String { let model = self.brand?.name?.original ?? "" let added = self.formatter.string(from: Date(timeIntervalSince1970: self.addedDate)) let updated = self.formatter.string(from: Date(timeIntervalSince1970: self.updatedDate)) var eventsString = "" for event in events { let location = event.address ?? "lat: \(event.latitude), lon: \(event.longitude)" let date = formatter.string(from: Date(timeIntervalSince1970: event.date)) eventsString.append(location + "; " + date + "\r\n") } let notesString = self.notes.reduce("") { partialResult, note in partialResult + note.text + "\r\n" } let number = self.currentNumber == nil ? self.number : "\(self.number) (\(self.currentNumber ?? ""))" return String(format: "%@, \"%@\", %@, %d, %f, %d, %d, %@, %@, %@, \"%@\", \"%@\", \"%@\", \"%@\"", number, model, self.color ?? "", self.year, self.engine?.powerHp ?? 0.0, self.events.count, self.ownershipPeriods.count, self.vin1 ?? "", self.sts ?? "", self.pts ?? "", added, updated, eventsString, notesString) } }