import Foundation import RealmSwift public class VehicleName: Object, Decodable, Cloneable { @Persisted public var original: String? @Persisted public var normalized: String? public required init(copy: VehicleName) { self.original = copy.original self.normalized = copy.normalized } required override init() { } } public class VehicleBrand: Object, Decodable, Cloneable { @Persisted public var name: VehicleName? @Persisted public var logo: String? public required init(copy: VehicleBrand) { self.name = copy.name?.clone() self.logo = copy.logo } required override init() { super.init() } } public class VehicleModel: Object, Decodable, Cloneable { @Persisted var name: VehicleName? public required init(copy: VehicleModel) { self.name = copy.name?.clone() } required override init() { super.init() } } public class VehicleEngine: Object, Decodable, Cloneable { @Persisted public var number: String? @Persisted public var volume: Int? = 0 @Persisted public var powerHp: Float? = 0 @Persisted public var powerKw: Float? = 0 @Persisted public 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 = try container.decodeIfPresent(Int.self, forKey: .volume) self.powerHp = try container.decodeIfPresent(Float.self, forKey: .powerHp) self.powerKw = try container.decodeIfPresent(Float.self, forKey: .powerKw) self.fuelType = try container.decodeIfPresent(String.self, forKey: .fuelType) } required override 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 { @Persisted public var brand: String? @Persisted public var model: String? @Persisted public var date: TimeInterval = 0 @Persisted public 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 override 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 { @Persisted public var lastOperation: String = "" @Persisted public var ownerType: String = "" @Persisted public var from: Int64 = 0 @Persisted public var to: Int64 = 0 @Persisted public var region: String? @Persisted public var registrationRegion: String? @Persisted public var locality: String? @Persisted public var code: String? @Persisted public var street: String? @Persisted public var building: String? @Persisted public 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 override init() { super.init() } } public final class Vehicle: Object, Decodable, Identifiable, Cloneable, Exportable { @Persisted public var brand: VehicleBrand? @Persisted public var model: VehicleModel? @Persisted public var color: String? @Persisted public var year: Int = 0 @Persisted public var category: String? @Persisted public var engine: VehicleEngine? @Persisted private var number: String = "" @Persisted public var currentNumber: String? @Persisted public var vin1: String? @Persisted public var vin2: String? @Persisted public var sts: String? @Persisted public var pts: String? @Persisted public var isRightWheel: Bool? @Persisted public var isJapanese: Bool? @Persisted public var addedDate: TimeInterval = 0 @Persisted public var updatedDate: TimeInterval = 0 @Persisted public var addedBy: String = "" @Persisted public var photos: List @Persisted public var ownershipPeriods: List @Persisted public var events: List @Persisted public var osagoContracts: List @Persisted public var ads: List @Persisted public var notes: List @Persisted public var debugInfo: DebugInfo? @Persisted public var synchronized: Bool = true lazy var formatter: DateFormatter = { let f = DateFormatter() f.dateStyle = .medium f.timeStyle = .medium return f }() 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 { super.init() 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 = try container.decodeIfPresent(Bool.self, forKey: .isRightWheel) self.isJapanese = 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 override init() { super.init() } public init(_ number: String) { super.init() 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 let events = List() events.append(objectsIn: self.events.map { $0.clone() }.sorted { $0.date > $1.date }) vehicle.events = events return vehicle } // MARK: - Exportable public static var csvHeader: String { return "Plate Number, Model, Color, Year, Power (HP), Events, Owners, VIN, STS, PTS, Engine number, 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 ?? "", self.engine?.number ?? "", added, updated, eventsString, notesString) } }