diff --git a/README.md b/README.md index 1dfa21a22..d259c7daa 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,11 @@ Version 2 alpha at https://v2.airline-club.com 11. Open another terminal, navigate to `airline-web`, run the web server by `activator run` 12. The application should be accessible at `localhost:9000` +## Banners +Self notes, too much trouble for other people to set it up right now. Just do NOT enable the banner. (disabled by default, to enable change `bannerEnabled` in `application.conf` + +For the banners to work properly, need to setup google photo API. Download the oauth json and put it under airline-web/conf. Then run the app, the log should show an oauth url, use it, then it should generate a token under airline-web/google-tokens. Now for server deployment, copy the oauth json `google-oauth-credentials.json` to `conf` AND the google-tokens (as folder) to the root of `airline-web`. + ## Attribution Some icons by [Yusuke Kamiyamane](http://p.yusukekamiyamane.com/). Licensed under a [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/) diff --git a/airline-data/db_scripts/issue485_patch.sql b/airline-data/db_scripts/issue485_patch.sql new file mode 100644 index 000000000..91fc04f6c --- /dev/null +++ b/airline-data/db_scripts/issue485_patch.sql @@ -0,0 +1,4 @@ +DELETE FROM link WHERE transport_type <> 0; + +ALTER TABLE `airline_v2`.`link` +DROP FOREIGN KEY `link_ibfk_3`; diff --git a/airline-data/db_scripts/patch_airline_modifer.sql b/airline-data/db_scripts/patch_airline_modifer.sql new file mode 100644 index 000000000..d3253b730 --- /dev/null +++ b/airline-data/db_scripts/patch_airline_modifer.sql @@ -0,0 +1,8 @@ +ALTER TABLE `airline_modifier` ADD INDEX `modifier_airline` (`airline` ASC) VISIBLE; + +ALTER TABLE `airline_modifier` DROP INDEX `PRIMARY`; +ALTER TABLE `airline_modifier` ADD `id` INT PRIMARY KEY NOT NULL AUTO_INCREMENT FIRST; + + +REPLACE INTO `airline_modifier_property`(id, name, value) SELECT o.id, "DURATION", 52 FROM `airline_modifier` o WHERE o.modifier_name = "DELEGATE_BOOST"; +REPLACE INTO `airline_modifier_property`(id, name, value) SELECT o.id, "STRENGTH", 3 FROM `airline_modifier` o WHERE o.modifier_name = "DELEGATE_BOOST"; \ No newline at end of file diff --git a/airline-data/db_scripts/patch_link_consumption_transport_type.sql b/airline-data/db_scripts/patch_link_consumption_transport_type.sql new file mode 100644 index 000000000..b4a05403a --- /dev/null +++ b/airline-data/db_scripts/patch_link_consumption_transport_type.sql @@ -0,0 +1,2 @@ +ALTER TABLE `airline_v2`.`link_consumption` +ADD COLUMN `transport_type` TINYINT NULL AFTER `duration`; \ No newline at end of file diff --git a/airline-data/db_scripts/patch_log.sql b/airline-data/db_scripts/patch_log.sql new file mode 100644 index 000000000..498d2930c --- /dev/null +++ b/airline-data/db_scripts/patch_log.sql @@ -0,0 +1,2 @@ +ALTER TABLE `log` ADD `id` INT PRIMARY KEY NOT NULL AUTO_INCREMENT FIRST; + diff --git a/airline-data/src/main/scala/com/patson/AirlineSimulation.scala b/airline-data/src/main/scala/com/patson/AirlineSimulation.scala index 0abed1c82..f54a296fc 100644 --- a/airline-data/src/main/scala/com/patson/AirlineSimulation.scala +++ b/airline-data/src/main/scala/com/patson/AirlineSimulation.scala @@ -26,7 +26,7 @@ object AirlineSimulation { val allAirlines = AirlineSource.loadAllAirlines(true) val allLinks = LinkSource.loadAllLinks(LinkSource.FULL_LOAD) val allFlightLinksByAirlineId = allLinks.filter(_.transportType == TransportType.FLIGHT).map(_.asInstanceOf[Link]).groupBy(_.airline.id) - val allShuttleLinksByAirlineId = allLinks.filter(_.transportType == TransportType.SHUTTLE).map(_.asInstanceOf[Shuttle]).groupBy(_.airline.id) + //val allShuttleLinksByAirlineId = allLinks.filter(_.transportType == TransportType.SHUTTLE).map(_.asInstanceOf[Shuttle]).groupBy(_.airline.id) val allTransactions = AirlineSource.loadTransactions(cycle).groupBy { _.airlineId } val allTransactionalCashFlowItems: scala.collection.immutable.Map[Int, List[AirlineCashFlowItem]] = AirlineSource.loadCashFlowItems(cycle).groupBy { _.airlineId } //purge the older transactions @@ -44,7 +44,7 @@ object AirlineSimulation { loungesByAirlineId.getOrElseUpdate(lounge.airline.id, ListBuffer[Lounge]()) += lounge ) - val shuttleServicesByAirlineId = AirlineSource.loadShuttleServiceByCriteria(List.empty).groupBy(_.airline.id) + //val shuttleServicesByAirlineId = AirlineSource.loadShuttleServiceByCriteria(List.empty).groupBy(_.airline.id) val allIncomes = ListBuffer[AirlineIncome]() val allCashFlows = ListBuffer[AirlineCashFlow]() //cash flow for accounting purpose @@ -191,14 +191,14 @@ object AirlineSimulation { othersSummary.put(OtherIncomeItemType.LOUNGE_INCOME, loungeIncome) - var shuttleCost = 0L - allShuttleLinksByAirlineId.get(airline.id).foreach { links => - shuttleCost = links.map(_.upkeep).sum - } - shuttleCost += shuttleServicesByAirlineId.getOrElse(airline.id, List.empty).map(_.basicUpkeep).sum - othersSummary.put(OtherIncomeItemType.SHUTTLE_COST, -1 * shuttleCost) +// var shuttleCost = 0L +// allShuttleLinksByAirlineId.get(airline.id).foreach { links => +// shuttleCost = links.map(_.upkeep).sum +// } +// shuttleCost += shuttleServicesByAirlineId.getOrElse(airline.id, List.empty).map(_.basicUpkeep).sum +// othersSummary.put(OtherIncomeItemType.SHUTTLE_COST, -1 * shuttleCost) - totalCashExpense += loungeUpkeep + loungeCost + shuttleCost + totalCashExpense += loungeUpkeep + loungeCost totalCashRevenue += loungeIncome //calculate extra cash flow due to difference in fuel cost diff --git a/airline-data/src/main/scala/com/patson/AirplaneSimulation.scala b/airline-data/src/main/scala/com/patson/AirplaneSimulation.scala index cee1b2d2d..bd6b34de9 100644 --- a/airline-data/src/main/scala/com/patson/AirplaneSimulation.scala +++ b/airline-data/src/main/scala/com/patson/AirplaneSimulation.scala @@ -103,9 +103,6 @@ object AirplaneSimulation { val secondHandAirplanes = ListBuffer[Airplane]() val fundsExhaustedAirlineIds = mutable.HashSet[Int]() - val discountsByAirlineId = ModelSource.loadAllAirlineDiscounts().view.mapValues(_.groupBy(_.modelId)) - val discountsByModelId = ModelSource.loadAllModelDiscounts().groupBy(_.modelId) - val updatingAirplanes = airplanes //this contains airplanes from all airlines .sortBy(_.condition) //lowest conditional airplane gets renewal first .map { airplane => @@ -120,17 +117,7 @@ object AirplaneSimulation { val originalModel = airplane.model - val discounts = ListBuffer[ModelDiscount]() - discountsByAirlineId.get(airlineId).foreach { airlineDiscountsByModelId => //airline specific discounts - airlineDiscountsByModelId.get(originalModel.id).foreach { airlineDiscounts => - discounts.appendAll(airlineDiscounts) - } - } - discountsByModelId.get(originalModel.id).foreach { modelDiscounts => - discounts.appendAll(modelDiscounts) - } - - val adjustedModel = originalModel.applyDiscount(discounts.toList) + val adjustedModel = originalModel.applyDiscount(ModelDiscount.getCombinedDiscountsByModelId(airlineId, originalModel.id)) val renewCost = adjustedModel.price - sellValue val newCost = existingCost + renewCost val newBuyPlane = existingBuyPlane + adjustedModel.price diff --git a/airline-data/src/main/scala/com/patson/AirportSimulation.scala b/airline-data/src/main/scala/com/patson/AirportSimulation.scala index c0f69aa4a..a0a129e72 100644 --- a/airline-data/src/main/scala/com/patson/AirportSimulation.scala +++ b/airline-data/src/main/scala/com/patson/AirportSimulation.scala @@ -3,10 +3,11 @@ package com.patson import java.util.Random import com.patson.data._ import com.patson.model._ -import com.patson.util.{AirlineCache, AirportCache, ChampionUtil} +import com.patson.util.{AirlineCache, AirportCache, AirportChampionInfo, ChampionUtil} import scala.collection.{MapView, immutable, mutable} import scala.collection.mutable.{ListBuffer, Map, Set} +import scala.math.BigDecimal.RoundingMode object AirportSimulation { val AWARENESS_DECAY = 0.1 @@ -122,6 +123,56 @@ object AirportSimulation { baseCycle + delta * LOYALIST_HISTORY_SAVE_INTERVAL } + def processChampionInfoChanges(previousInfo : List[AirportChampionInfo], newInfo : List[AirportChampionInfo], currentCycle : Int) = { + val previousInfoByAirlineId : Predef.Map[Int, List[AirportChampionInfo]] = previousInfo.groupBy(_.loyalist.airline.id) + val newInfoByAirlineId : Predef.Map[Int, List[AirportChampionInfo]] = newInfo.groupBy(_.loyalist.airline.id) + + val airlineIds = previousInfoByAirlineId.keySet ++ newInfoByAirlineId.keySet + val logs = ListBuffer[Log]() + airlineIds.foreach { airlineId => + val changes = ListBuffer[ChampionInfoChange]() + previousInfoByAirlineId.get(airlineId) match { + case Some(previousRanks) => + newInfoByAirlineId.get(airlineId) match { + case Some(newRanks) => //go from airport to airport + val previousInfoByAirport = previousRanks.groupBy(_.loyalist.airport).view.mapValues(_(0)) //should be exactly one entry + val newInfoByAirport = newRanks.groupBy(_.loyalist.airport).view.mapValues(_(0)) //should be exactly one entry + val airportIds = previousInfoByAirport.keySet ++ newInfoByAirport.keySet + airportIds.foreach { airportId => + if (previousInfoByAirport.get(airportId).map(_.ranking).getOrElse(0) != newInfoByAirport.get(airportId).map(_.ranking).getOrElse(0)) { + changes.append(ChampionInfoChange(previousInfoByAirport.get(airportId), newInfoByAirport.get(airportId))) + } + } + case None => changes.appendAll(previousRanks.map(info => ChampionInfoChange(Some(info), None))) //lost all ranks + } + case None => changes.appendAll(newInfoByAirlineId(airlineId).map(info => ChampionInfoChange(None, Some(info)))) //all ranks are new + } + val airline = AirlineCache.getAirline(airlineId, false).getOrElse(Airline.fromId(airlineId)) + + logs.appendAll(changes.map { + case ChampionInfoChange(Some(previousRank), Some(newRank)) => + val reputationChange = BigDecimal.valueOf(newRank.reputationBoost - previousRank.reputationBoost).setScale(2, RoundingMode.HALF_UP) + val displayChange = + if (reputationChange >= 0) { + "+" + reputationChange + } else { + reputationChange.toString + } + Log(airline, s"${newRank.loyalist.airport.displayText} ranking ${previousRank.ranking} -> ${newRank.ranking}. Reputation change $displayChange", LogCategory.AIRPORT_RANK_CHANGE, LogSeverity.INFO, currentCycle, immutable.Map("airportId" -> newRank.loyalist.airport.id.toString)) + case ChampionInfoChange(None, Some(newRank)) => + Log(airline, s"${newRank.loyalist.airport.displayText} new ranking ${newRank.ranking}. Reputation change +${BigDecimal.valueOf(newRank.reputationBoost).setScale(2, RoundingMode.HALF_UP)}", LogCategory.AIRPORT_RANK_CHANGE, LogSeverity.INFO, currentCycle, immutable.Map("airportId" -> newRank.loyalist.airport.id.toString)) + case ChampionInfoChange(Some(previousRank), None) => + Log(airline, s"${previousRank.loyalist.airport.displayText} lost ranking ${previousRank.ranking}. Reputation change -${BigDecimal.valueOf(previousRank.reputationBoost).setScale(2, RoundingMode.HALF_UP)}", LogCategory.AIRPORT_RANK_CHANGE, LogSeverity.INFO, currentCycle, immutable.Map("airportId" -> previousRank.loyalist.airport.id.toString)) + case _ => //should not happen + Log(airline, s"Unknown rank change", LogCategory.AIRPORT_RANK_CHANGE, LogSeverity.INFO, currentCycle) + }) + } + println(s"Ranking changes count : ${logs.size}") + LogSource.insertLogs(logs.toList) + } + case class ChampionInfoChange(previousRank : Option[AirportChampionInfo], newRank : Option[AirportChampionInfo]) + + def simulateLoyalists(allAirports : List[Airport], linkRidershipDetails : immutable.Map[(PassengerGroup, Airport, Route), Int], cycle : Int) = { val existingLoyalistByAirportId : immutable.Map[Int, List[Loyalist]] = LoyalistSource.loadLoyalistsByCriteria(List.empty).groupBy(_.airport.id) val (updatingLoyalists,deletingLoyalists) = computeLoyalists(allAirports, linkRidershipDetails, existingLoyalistByAirportId) @@ -135,7 +186,12 @@ object AirportSimulation { println(s"Computing loyalist info with ${allLoyalists.length} entries") //compute champion info - ChampionUtil.updateAirportChampionInfo(allLoyalists) + val previousInfo = ChampionUtil.loadAirportChampionInfo() + val newInfo = ChampionUtil.computeAirportChampionInfo(allLoyalists) + processChampionInfoChanges(previousInfo, newInfo, cycle) + AirportSource.updateChampionInfo(newInfo) + + println("Done computing champions") if (cycle % LOYALIST_HISTORY_SAVE_INTERVAL == 0) { @@ -278,7 +334,7 @@ object AirportSimulation { println("Checking lounge status") val passengersByAirport : MapView[Airport, MapView[Airline, Int]] = linkRidershipDetails.toList.flatMap { case ((passengerGroup, airport, route), count) => - route.links.flatMap { linkConsideration => + route.links.filter(_.link.transportType == TransportType.FLIGHT).flatMap { linkConsideration => List((linkConsideration.link.airline, linkConsideration.from, count), (linkConsideration.link.airline, linkConsideration.to, count)) } diff --git a/airline-data/src/main/scala/com/patson/DemandGenerator.scala b/airline-data/src/main/scala/com/patson/DemandGenerator.scala index 94a1df61c..d13b66571 100644 --- a/airline-data/src/main/scala/com/patson/DemandGenerator.scala +++ b/airline-data/src/main/scala/com/patson/DemandGenerator.scala @@ -207,7 +207,7 @@ object DemandGenerator { val income = fromAirport.income val firstClassPercentage : Double = - if (flightType == LONG_HAUL_INTERNATIONAL || flightType == MEDIUM_HAUL_INTERCONTINENTAL || flightType == LONG_HAUL_INTERCONTINENTAL || flightType == ULTRA_LONG_HAUL_INTERCONTINENTAL || flightType == MEDIUM_HAUL_DOMESTIC || flightType == LONG_HAUL_DOMESTIC || flightType == SHORT_HAUL_INTERNATIONAL || flightType == MEDIUM_HAUL_INTERNATIONAL) { + if (flightType == LONG_HAUL_INTERNATIONAL || flightType == MEDIUM_HAUL_INTERCONTINENTAL || flightType == SHORT_HAUL_INTERCONTINENTAL || flightType == LONG_HAUL_INTERCONTINENTAL || flightType == ULTRA_LONG_HAUL_INTERCONTINENTAL || flightType == MEDIUM_HAUL_DOMESTIC || flightType == LONG_HAUL_DOMESTIC || flightType == SHORT_HAUL_INTERNATIONAL || flightType == MEDIUM_HAUL_INTERNATIONAL) { if (income <= FIRST_CLASS_INCOME_MIN) { 0 } else if (income >= FIRST_CLASS_INCOME_MAX) { diff --git a/airline-data/src/main/scala/com/patson/LinkSimulation.scala b/airline-data/src/main/scala/com/patson/LinkSimulation.scala index c906dd6ba..41a0c9508 100644 --- a/airline-data/src/main/scala/com/patson/LinkSimulation.scala +++ b/airline-data/src/main/scala/com/patson/LinkSimulation.scala @@ -41,12 +41,12 @@ object LinkSimulation { val (consumptionResult: scala.collection.immutable.Map[(PassengerGroup, Airport, Route), Int], missedPassengerResult : immutable.Map[(PassengerGroup, Airport), Int])= PassengerSimulation.passengerConsume(demand, links) //generate statistic - println("Generating stats") - val linkStatistics = generateLinkStatistics(consumptionResult, cycle) + println("Generating flight stats") + val linkStatistics = generateFlightStatistics(consumptionResult, cycle) println("Saving generated stats to DB") LinkStatisticsSource.deleteLinkStatisticsBeforeCycle(cycle - 5) LinkStatisticsSource.saveLinkStatistics(linkStatistics) - + //generate country market share println("Generating country market share") val countryMarketShares = generateCountryMarketShares(consumptionResult) @@ -74,7 +74,7 @@ object LinkSimulation { } - + //save all consumptions var startTime = System.currentTimeMillis() println("Saving " + consumptionResult.size + " consumptions") @@ -88,12 +88,12 @@ object LinkSimulation { // val linkHistory = generateLinkHistory(consumptionResult) // println("Saving " + linkHistory.size + " generated history to DB") // LinkHistorySource.updateLinkHistory(linkHistory) - + // println("Generating VIP") // val vipRoutes = generateVipRoutes(consumptionResult) // RouteHistorySource.deleteVipRouteBeforeCycle(cycle) // RouteHistorySource.saveVipRoutes(vipRoutes, cycle) - + println("Calculating profits by links") startTime = System.currentTimeMillis() val linkConsumptionDetails = ListBuffer[LinkConsumptionDetails]() @@ -107,12 +107,15 @@ object LinkSimulation { } } - links.foreach { link => - if (link.capacity.total > 0) { - val (linkResult, loungeResult) = computeLinkAndLoungeConsumptionDetail(link, cycle, allAirplaneAssignments, costByLink.getOrElse(link, List.empty).toList) - linkConsumptionDetails += linkResult - loungeConsumptionDetails ++= loungeResult - } + links.foreach { + case flightLink : Link => + if (flightLink.capacity.total > 0) { + val (linkResult, loungeResult) = computeLinkAndLoungeConsumptionDetail(flightLink, cycle, allAirplaneAssignments, costByLink.getOrElse(flightLink, List.empty).toList) + linkConsumptionDetails += linkResult + loungeConsumptionDetails ++= loungeResult + } + case nonFlightLink => //only compute for flights (class Link) + linkConsumptionDetails += LinkConsumptionDetails(nonFlightLink, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, cycle) } endTime = System.currentTimeMillis() @@ -220,125 +223,119 @@ object LinkSimulation { computeLinkAndLoungeConsumptionDetail(link, cycle, assignmentsToThis, List.empty)._1 } - def computeLinkAndLoungeConsumptionDetail(link : Transport, cycle : Int, allAirplaneAssignments : immutable.Map[Int, LinkAssignments], passengerCostEntries : List[PassengerCost]) : (LinkConsumptionDetails, List[LoungeConsumptionDetails]) = { - link.transportType match { - case TransportType.FLIGHT => - val flightLink = link.asInstanceOf[Link] - val loadFactor = flightLink.getTotalSoldSeats.toDouble / flightLink.getTotalCapacity - - //val totalFuelBurn = link //fuel burn actually similar to crew cost - val fuelCost = flightLink.getAssignedModel() match { - case Some(model) => - (if (flightLink.duration <= 90) { - val ascendTime, descendTime = (flightLink.duration / 2) - (model.fuelBurn * 10 * ascendTime + model.fuelBurn * descendTime) * FUEL_UNIT_COST * (flightLink.frequency - flightLink.cancellationCount) - } else { - (model.fuelBurn * 10 * 45 + model.fuelBurn * (flightLink.duration - 45)) * FUEL_UNIT_COST * (flightLink.frequency - flightLink.cancellationCount) //first 45 minutes huge burn, then cruising at 1/10 the cost - } * (0.7 + 0.3 * loadFactor)).toInt //at 0 LF, 70% fuel cost - case None => 0 - } + def computeLinkAndLoungeConsumptionDetail(link : Link, cycle : Int, allAirplaneAssignments : immutable.Map[Int, LinkAssignments], passengerCostEntries : List[PassengerCost]) : (LinkConsumptionDetails, List[LoungeConsumptionDetails]) = { + val flightLink = link.asInstanceOf[Link] + val loadFactor = flightLink.getTotalSoldSeats.toDouble / flightLink.getTotalCapacity + + //val totalFuelBurn = link //fuel burn actually similar to crew cost + val fuelCost = flightLink.getAssignedModel() match { + case Some(model) => + (if (flightLink.duration <= 90) { + val ascendTime, descendTime = (flightLink.duration / 2) + (model.fuelBurn * 10 * ascendTime + model.fuelBurn * descendTime) * FUEL_UNIT_COST * (flightLink.frequency - flightLink.cancellationCount) + } else { + (model.fuelBurn * 10 * 45 + model.fuelBurn * (flightLink.duration - 45)) * FUEL_UNIT_COST * (flightLink.frequency - flightLink.cancellationCount) //first 45 minutes huge burn, then cruising at 1/10 the cost + } * (0.7 + 0.3 * loadFactor)).toInt //at 0 LF, 70% fuel cost + case None => 0 + } - val inServiceAssignedAirplanes = flightLink.getAssignedAirplanes().filter(_._1.isReady) - //the % of time spent on this link for each airplane - val assignmentWeights : immutable.Map[Airplane, Double] = { //0 to 1 - inServiceAssignedAirplanes.view.map { - case(airplane, assignment) => - allAirplaneAssignments.get(airplane.id) match { - case Some(linkAssignmentsToThisAirplane) => - val weight : Double = assignment.flightMinutes.toDouble / linkAssignmentsToThisAirplane.assignments.values.map(_.flightMinutes).sum - (airplane, weight) - case None => (airplane, 1.0) //100% - } //it shouldn't be else...but just to play safe, if it's not found in "all" table, assume this is the only link assigned - }.toMap - } - var maintenanceCost = 0 - inServiceAssignedAirplanes.foreach { - case(airplane, _) => - //val maintenanceCost = (link.getAssignedAirplanes.toList.map(_._1).foldLeft(0)(_ + _.model.maintenanceCost) * link.airline.getMaintenanceQuality() / Airline.MAX_MAINTENANCE_QUALITY).toInt - maintenanceCost += (airplane.model.maintenanceCost * assignmentWeights(airplane) * flightLink.airline.getMaintenanceQuality() / Airline.MAX_MAINTENANCE_QUALITY).toInt - } + val inServiceAssignedAirplanes = flightLink.getAssignedAirplanes().filter(_._1.isReady) + //the % of time spent on this link for each airplane + val assignmentWeights : immutable.Map[Airplane, Double] = { //0 to 1 + inServiceAssignedAirplanes.view.map { + case(airplane, assignment) => + allAirplaneAssignments.get(airplane.id) match { + case Some(linkAssignmentsToThisAirplane) => + val weight : Double = assignment.flightMinutes.toDouble / linkAssignmentsToThisAirplane.assignments.values.map(_.flightMinutes).sum + (airplane, weight) + case None => (airplane, 1.0) //100% + } //it shouldn't be else...but just to play safe, if it's not found in "all" table, assume this is the only link assigned + }.toMap + } + var maintenanceCost = 0 + inServiceAssignedAirplanes.foreach { + case(airplane, _) => + //val maintenanceCost = (link.getAssignedAirplanes.toList.map(_._1).foldLeft(0)(_ + _.model.maintenanceCost) * link.airline.getMaintenanceQuality() / Airline.MAX_MAINTENANCE_QUALITY).toInt + maintenanceCost += (airplane.model.maintenanceCost * assignmentWeights(airplane) * flightLink.airline.getMaintenanceQuality() / Airline.MAX_MAINTENANCE_QUALITY).toInt + } - val airportFees = flightLink.getAssignedModel() match { - case Some(model) => - val airline = flightLink.airline - (flightLink.from.slotFee(model, airline) + flightLink.to.slotFee(model, airline) + flightLink.from.landingFee(model) + flightLink.to.landingFee(model)) * flightLink.frequency - case None => 0 - } + val airportFees = flightLink.getAssignedModel() match { + case Some(model) => + val airline = flightLink.airline + (flightLink.from.slotFee(model, airline) + flightLink.to.slotFee(model, airline) + flightLink.from.landingFee(model) + flightLink.to.landingFee(model)) * flightLink.frequency + case None => 0 + } - var depreciation = 0 - inServiceAssignedAirplanes.foreach { - case(airplane, _) => - //link.getAssignedAirplanes().toList.map(_._1).foldLeft(0)(_ + _.depreciationRate) - depreciation += (airplane.depreciationRate * assignmentWeights(airplane)).toInt - } + var depreciation = 0 + inServiceAssignedAirplanes.foreach { + case(airplane, _) => + //link.getAssignedAirplanes().toList.map(_._1).foldLeft(0)(_ + _.depreciationRate) + depreciation += (airplane.depreciationRate * assignmentWeights(airplane)).toInt + } - var inflightCost, crewCost, revenue = 0 - LinkClass.values.foreach { linkClass => - val capacity = flightLink.capacity(linkClass) - val soldSeats = flightLink.soldSeats(linkClass) + var inflightCost, crewCost, revenue = 0 + LinkClass.values.foreach { linkClass => + val capacity = flightLink.capacity(linkClass) + val soldSeats = flightLink.soldSeats(linkClass) - inflightCost += computeInflightCost(linkClass, flightLink, soldSeats) - crewCost += (linkClass.resourceMultiplier * capacity * flightLink.duration / 60 * CREW_UNIT_COST).toInt - revenue += soldSeats * flightLink.price(linkClass) - } + inflightCost += computeInflightCost(linkClass, flightLink, soldSeats) + crewCost += (linkClass.resourceMultiplier * capacity * flightLink.duration / 60 * CREW_UNIT_COST).toInt + revenue += soldSeats * flightLink.price(linkClass) + } - // delays incur extra cost - var delayCompensation = Computation.computeCompensation(flightLink) - - // lounge cost - val fromLounge = flightLink.from.getLounge(flightLink.airline.id, flightLink.airline.getAllianceId(), activeOnly = true) - val toLounge = flightLink.to.getLounge(flightLink.airline.id, flightLink.airline.getAllianceId(), activeOnly = true) - var loungeCost = 0 - val loungeConsumptionDetails = ListBuffer[LoungeConsumptionDetails]() - if (fromLounge.isDefined || toLounge.isDefined) { - val visitorCount = flightLink.soldSeats(BUSINESS) + flightLink.soldSeats(FIRST) - if (fromLounge.isDefined) { - loungeCost += visitorCount * Lounge.PER_VISITOR_CHARGE - loungeConsumptionDetails += ( - if (fromLounge.get.airline.id == flightLink.airline.id) { - LoungeConsumptionDetails(fromLounge.get, selfVisitors = visitorCount, allianceVisitors = 0, cycle) - } else { - LoungeConsumptionDetails(fromLounge.get, selfVisitors = 0, allianceVisitors = visitorCount, cycle) - }) - } - if (toLounge.isDefined) { - loungeCost += visitorCount * Lounge.PER_VISITOR_CHARGE - loungeConsumptionDetails += ( - if (toLounge.get.airline.id == flightLink.airline.id) { - LoungeConsumptionDetails(toLounge.get, selfVisitors = visitorCount, allianceVisitors = 0, cycle) - } else { - LoungeConsumptionDetails(toLounge.get, selfVisitors = 0, allianceVisitors = visitorCount, cycle) - }) - } + // delays incur extra cost + var delayCompensation = Computation.computeCompensation(flightLink) - } + // lounge cost + val fromLounge = flightLink.from.getLounge(flightLink.airline.id, flightLink.airline.getAllianceId(), activeOnly = true) + val toLounge = flightLink.to.getLounge(flightLink.airline.id, flightLink.airline.getAllianceId(), activeOnly = true) + var loungeCost = 0 + val loungeConsumptionDetails = ListBuffer[LoungeConsumptionDetails]() + if (fromLounge.isDefined || toLounge.isDefined) { + val visitorCount = flightLink.soldSeats(BUSINESS) + flightLink.soldSeats(FIRST) + if (fromLounge.isDefined) { + loungeCost += visitorCount * Lounge.PER_VISITOR_CHARGE + loungeConsumptionDetails += ( + if (fromLounge.get.airline.id == flightLink.airline.id) { + LoungeConsumptionDetails(fromLounge.get, selfVisitors = visitorCount, allianceVisitors = 0, cycle) + } else { + LoungeConsumptionDetails(fromLounge.get, selfVisitors = 0, allianceVisitors = visitorCount, cycle) + }) + } + if (toLounge.isDefined) { + loungeCost += visitorCount * Lounge.PER_VISITOR_CHARGE + loungeConsumptionDetails += ( + if (toLounge.get.airline.id == flightLink.airline.id) { + LoungeConsumptionDetails(toLounge.get, selfVisitors = visitorCount, allianceVisitors = 0, cycle) + } else { + LoungeConsumptionDetails(toLounge.get, selfVisitors = 0, allianceVisitors = visitorCount, cycle) + }) + } - val profit = revenue - fuelCost - maintenanceCost - crewCost - airportFees - inflightCost - delayCompensation - depreciation - loungeCost - - //calculation overall satisifaction - var satisfactionTotalValue : Double = 0 - var totalPassengerCount = 0 - passengerCostEntries.foreach { - case PassengerCost(passengerGroup, passengerCount, cost) => - val preferredLinkClass = passengerGroup.preference.preferredLinkClass - val standardPrice = flightLink.standardPrice(preferredLinkClass) - val satisfaction = Computation.computePassengerSatisfaction(cost, standardPrice) - satisfactionTotalValue += satisfaction * passengerCount - totalPassengerCount += passengerCount - } - val overallSatisfaction = if (totalPassengerCount == 0) 0 else satisfactionTotalValue / totalPassengerCount - - //val result = LinkConsumptionDetails(link.id, link.price, link.capacity, link.soldSeats, link.computedQuality, fuelCost, crewCost, airportFees, inflightCost, delayCompensation = delayCompensation, maintenanceCost, depreciation = depreciation, revenue, profit, link.cancellationCount, linklink.from.id, link.to.id, link.airline.id, link.distance, cycle) - val result = LinkConsumptionDetails(flightLink, fuelCost, crewCost, airportFees, inflightCost, delayCompensation = delayCompensation, maintenanceCost, depreciation = depreciation, loungeCost = loungeCost, revenue, profit, overallSatisfaction, cycle) - //println("model : " + link.getAssignedModel().get + " profit : " + result.profit + " result: " + result) - (result, loungeConsumptionDetails.toList) - case TransportType.SHUTTLE => - (LinkConsumptionDetails(link, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, cycle), List.empty) } + val profit = revenue - fuelCost - maintenanceCost - crewCost - airportFees - inflightCost - delayCompensation - depreciation - loungeCost + + //calculation overall satisifaction + var satisfactionTotalValue : Double = 0 + var totalPassengerCount = 0 + passengerCostEntries.foreach { + case PassengerCost(passengerGroup, passengerCount, cost) => + val preferredLinkClass = passengerGroup.preference.preferredLinkClass + val standardPrice = flightLink.standardPrice(preferredLinkClass) + val satisfaction = Computation.computePassengerSatisfaction(cost, standardPrice) + satisfactionTotalValue += satisfaction * passengerCount + totalPassengerCount += passengerCount + } + val overallSatisfaction = if (totalPassengerCount == 0) 0 else satisfactionTotalValue / totalPassengerCount + + //val result = LinkConsumptionDetails(link.id, link.price, link.capacity, link.soldSeats, link.computedQuality, fuelCost, crewCost, airportFees, inflightCost, delayCompensation = delayCompensation, maintenanceCost, depreciation = depreciation, revenue, profit, link.cancellationCount, linklink.from.id, link.to.id, link.airline.id, link.distance, cycle) + val result = LinkConsumptionDetails(flightLink, fuelCost, crewCost, airportFees, inflightCost, delayCompensation = delayCompensation, maintenanceCost, depreciation = depreciation, loungeCost = loungeCost, revenue, profit, overallSatisfaction, cycle) + //println("model : " + link.getAssignedModel().get + " profit : " + result.profit + " result: " + result) + (result, loungeConsumptionDetails.toList) } val BASE_INFLIGHT_COST = 20 @@ -361,7 +358,7 @@ object LinkSimulation { val costPerPassenger = BASE_INFLIGHT_COST + durationCostPerHour * link.duration.toDouble / 60 (costPerPassenger * soldSeats * 2).toInt //Roundtrip X 2 } - + val LOAD_FACTOR_ALERT_LINK_COUNT_THRESHOLD = 3 //how many airlines before load factor is checked val LOAD_FACTOR_ALERT_THRESHOLD = 0.5 //LF threshold val LOAD_FACTOR_ALERT_DURATION = 52 @@ -436,7 +433,7 @@ object LinkSimulation { } } - + deletingLinks.foreach { link => println("Revoked link: " + link) LinkSource.deleteLink(link.id) @@ -444,31 +441,33 @@ object LinkSimulation { AlertSource.updateAlerts(updatingAlerts.toList) AlertSource.insertAlerts(newAlerts.toList) AlertSource.deleteAlerts(deletingAlerts.toList) - + LogSource.insertLogs(newLogs.toList) } - - def generateLinkStatistics(consumptionResult: scala.collection.immutable.Map[(PassengerGroup, Airport, Route), Int], cycle : Int) : List[LinkStatistics] = { + + def generateFlightStatistics(consumptionResult: scala.collection.immutable.Map[(PassengerGroup, Airport, Route), Int], cycle : Int) : List[LinkStatistics] = { val statistics = Map[LinkStatisticsKey, Int]() consumptionResult.foreach { case ((_, _, route), passengerCount) => for (i <- 0 until route.links.size) { - val link = route.links(i) - val airline = link.link.airline - val key = - if (i == 0) { - if (route.links.size == 1) { - LinkStatisticsKey(link.from, link.to, true, true, airline) - } else { - LinkStatisticsKey(link.from, link.to, true, false, airline) + val link = route.links(i) + if (link.link.transportType == TransportType.FLIGHT) { //only do stats on flights here + val airline = link.link.airline + val key = + if (i == 0) { + if (route.links.size == 1) { + LinkStatisticsKey(link.from, link.to, true, true, airline) + } else { + LinkStatisticsKey(link.from, link.to, true, false, airline) + } + } else if (i == route.links.size - 1) { //last one in list + LinkStatisticsKey(link.from, link.to, false, true, airline) + } else { //in the middle + LinkStatisticsKey(link.from, link.to, false, false, airline) } - } else if (i == route.links.size -1) { //last one in list - LinkStatisticsKey(link.from, link.to, false, true, airline) - } else { //in the middle - LinkStatisticsKey(link.from, link.to, false, false, airline) - } - val newPassengerCount = statistics.getOrElse(key, 0) + passengerCount - statistics.put(key, newPassengerCount) + val newPassengerCount = statistics.getOrElse(key, 0) + passengerCount + statistics.put(key, newPassengerCount) + } } } @@ -557,8 +556,10 @@ object LinkSimulation { olympicsConsumptions.foreach { case ((_, _, Route(links, _, _)), passengerCount) => links.foreach { link => - val existingScore : BigDecimal = scoresByAirline.getOrElse(link.link.airline, 0) - scoresByAirline.put(link.link.airline, existingScore + passengerCount.toDouble / links.size) + if (link.link.transportType == TransportType.FLIGHT) { + val existingScore : BigDecimal = scoresByAirline.getOrElse(link.link.airline, 0) + scoresByAirline.put(link.link.airline, existingScore + passengerCount.toDouble / links.size) + } } } diff --git a/airline-data/src/main/scala/com/patson/PassengerSimulation.scala b/airline-data/src/main/scala/com/patson/PassengerSimulation.scala index 6c5b2bf78..2774e91ec 100644 --- a/airline-data/src/main/scala/com/patson/PassengerSimulation.scala +++ b/airline-data/src/main/scala/com/patson/PassengerSimulation.scala @@ -476,13 +476,20 @@ object PassengerSimulation { //see if there are any seats for that class (or lower) left link.availableSeatsAtOrBelowClass(preferredLinkClass).foreach { case(matchingLinkClass, seatsLeft) => - //from the perspective of the passenger group, how well does it know each link - val airlineAwarenessFromCity = passengerGroup.fromAirport.getAirlineAwareness(link.airline.id) - val airlineAwarenessFromReputation = if (link.airline.getReputation() >= AirlineGrade.CONTINENTAL.reputationCeiling) AirlineAppeal.MAX_AWARENESS else link.airline.getReputation() * 2 //if reputation is 50+ then everyone will see it, otherwise reputation * 2 - //println("Awareness from reputation " + airlineAwarenessFromReputation) - val airlineAwareness = Math.max(airlineAwarenessFromCity, airlineAwarenessFromReputation) + val isAwareOfService = + if (link.transportType == TransportType.GENERIC_TRANSIT) { + true + } else { + //from the perspective of the passenger group, how well does it know each link + val airlineAwarenessFromCity = passengerGroup.fromAirport.getAirlineAwareness(link.airline.id) + val airlineAwarenessFromReputation = if (link.airline.getReputation() >= AirlineGrade.CONTINENTAL.reputationCeiling) AirlineAppeal.MAX_AWARENESS else link.airline.getReputation() * 2 //if reputation is 50+ then everyone will see it, otherwise reputation * 2 + //println("Awareness from reputation " + airlineAwarenessFromReputation) + val airlineAwareness = Math.max(airlineAwarenessFromCity, airlineAwarenessFromReputation) + airlineAwareness > Random.nextInt(AirlineAppeal.MAX_AWARENESS) + } + - if (airlineAwareness > Random.nextInt(AirlineAppeal.MAX_AWARENESS)) { + if (isAwareOfService) { // var cost = passengerGroup.preference.computeCost(link, matchingLinkClass) // // if (airlineCostModifiers.contains(link.airline.id)) { @@ -683,18 +690,8 @@ object PassengerSimulation { if (linkConsideration.link.id == predecessorLink.id) { //going back and forth on the same link isValid = false - } else if (predecessorLink.transportType == TransportType.SHUTTLE || linkConsideration.link.transportType == TransportType.SHUTTLE) { - if (previousLinkAirlineId == currentLinkAirlineId || - (allianceIdByAirlineId.containsKey(previousLinkAirlineId) && - allianceIdByAirlineId.get(previousLinkAirlineId) == allianceIdByAirlineId.get(currentLinkAirlineId))) { //same airline or same alliance - shuttle okay - connectionCost = 25 - } else { - isValid = false //shuttle only allows same network - } - - //THIS ONLY WORKS since the shuttle distance is less than min flight distance, if we introduce shuttle that overlaps flight distance, it will have issues - //for example airport A -> B , 100 km , if covered by a long range shuttle, vertex B will have the shuttle as edge, but then it forbids all other airlines heading out from B - //a more "correct" way would be to create shuttle assisted "flight" that is a Link combining shuttle and the actual link. Though this would require quite a bit of changes + } else if (predecessorLink.transportType == TransportType.GENERIC_TRANSIT || linkConsideration.link.transportType == TransportType.GENERIC_TRANSIT) { + connectionCost = 25 } else { connectionCost += 25 //base cost for connection //now look at the frequency of the link arriving at this FromAirport and the link (current link) leaving this FromAirport. check frequency @@ -712,7 +709,8 @@ object PassengerSimulation { } if (isValid) { - val cost = linkConsideration.cost + connectionCost + val cost = Math.max(0, linkConsideration.cost + connectionCost) //just to avoid loop in graph + val fromCost = distanceMap.get(linkConsideration.from.id) val newCost = fromCost + cost @@ -730,15 +728,15 @@ object PassengerSimulation { } val resultMap : scala.collection.mutable.Map[Airport, Route] = scala.collection.mutable.Map[Airport, Route]() - val maxHop = maxIteration * (maxIteration + 1) / 2 - toAirports.foreach{ to => + + toAirports.foreach{ to => var walker = to.id var noSolution = false; var foundSolution = false var hasFlight = false var route = ListBuffer[LinkConsideration]() var hopCounter = 0 - while (!foundSolution && !noSolution && hopCounter < maxIteration) { + while (!foundSolution && !noSolution) { val link = predecessorMap.get(walker) if (link != null) { route.prepend(link) @@ -801,4 +799,4 @@ object PassengerSimulation { // } -} \ No newline at end of file +} diff --git a/airline-data/src/main/scala/com/patson/data/AirlineSource.scala b/airline-data/src/main/scala/com/patson/data/AirlineSource.scala index 3938fc85f..5c17bad0b 100644 --- a/airline-data/src/main/scala/com/patson/data/AirlineSource.scala +++ b/airline-data/src/main/scala/com/patson/data/AirlineSource.scala @@ -2,6 +2,7 @@ package com.patson.data import scala.collection.mutable.ListBuffer import scala.collection.mutable.Map +import scala.collection.immutable import com.patson.data.Constants._ import com.patson.model._ import com.patson.MainSimulation @@ -654,129 +655,6 @@ object AirlineSource { } - def loadShuttleServiceByAirlineAndAirport(airlineId : Int, airportId : Int) : Option[ShuttleService] = { - val result = loadShuttleServiceByCriteria(List(("airline", airlineId), ("airport", airportId))) - if (result.isEmpty) { - None - } else { - Some(result(0)) - } - } - - def loadShuttleServiceByCriteria(criteria : List[(String, Any)], airports : Map[Int, Airport] = Map[Int, Airport]()) : List[ShuttleService] = { - //open the hsqldb - val connection = Meta.getConnection() - try { - var queryString = "SELECT * FROM " + SHUTTLE_SERVICE_TABLE - - if (!criteria.isEmpty) { - queryString += " WHERE " - for (i <- 0 until criteria.size - 1) { - queryString += criteria(i)._1 + " = ? AND " - } - queryString += criteria.last._1 + " = ?" - } - - val preparedStatement = connection.prepareStatement(queryString) - - for (i <- 0 until criteria.size) { - preparedStatement.setObject(i + 1, criteria(i)._2) - } - - - val resultSet = preparedStatement.executeQuery() - - val services = new ListBuffer[ShuttleService]() - - val airlines = Map[Int, Airline]() - while (resultSet.next()) { - val airlineId = resultSet.getInt("airline") - val airline = airlines.getOrElseUpdate(airlineId, AirlineCache.getAirline(airlineId, false).getOrElse(Airline.fromId(airlineId))) - AllianceSource.loadAllianceMemberByAirline(airline).foreach { member => - if (member.role != AllianceRole.APPLICANT) { - airline.setAllianceId(member.allianceId) - } - } - - //val airport = Airport.fromId(resultSet.getInt("airport")) - val airportId = resultSet.getInt("airport") - val airport = airports.getOrElseUpdate(airportId, AirportCache.getAirport(airportId, false).get) - val name = resultSet.getString("name") - val level = resultSet.getInt("level") - val foundedCycle = resultSet.getInt("founded_cycle") - - - services += ShuttleService(airline, airline.getAllianceId(), airport, name, level, foundedCycle) - } - - resultSet.close() - preparedStatement.close() - - services.toList - } finally { - connection.close() - } - } - - //case class AirlineBase(airline : Airline, airport : Airport, scale : Int, headQuarter : Boolean = false, var id : Int = 0) extends IdObject - def saveShuttleService(shuttleService : ShuttleService) = { - val connection = Meta.getConnection() - try { - val preparedStatement = connection.prepareStatement("REPLACE INTO " + SHUTTLE_SERVICE_TABLE + "(airline, airport, name, level, founded_cycle) VALUES(?, ?, ?, ?, ?)") - - preparedStatement.setInt(1, shuttleService.airline.id) - preparedStatement.setInt(2, shuttleService.airport.id) - preparedStatement.setString(3, shuttleService.name) - preparedStatement.setInt(4, shuttleService.level) - preparedStatement.setInt(5, shuttleService.foundedCycle) - preparedStatement.executeUpdate() - preparedStatement.close() - - AirlineCache.invalidateAirline(shuttleService.airline.id) - AirportCache.invalidateAirport(shuttleService.airport.id) - } finally { - connection.close() - } - } - - def deleteShuttleService(shuttleService : ShuttleService) = { - deleteShuttleServiceByCriteria(List(("airline", shuttleService.airline.id), ("airport", shuttleService.airport.id))) - AirlineCache.invalidateAirline(shuttleService.airline.id) - AirportCache.invalidateAirport(shuttleService.airport.id) - } - - def deleteShuttleServiceByCriteria(criteria : List[(String, Any)]) = { - //open the hsqldb - val connection = Meta.getConnection() - try { - var queryString = "DELETE FROM " + SHUTTLE_SERVICE_TABLE - - if (!criteria.isEmpty) { - queryString += " WHERE " - for (i <- 0 until criteria.size - 1) { - queryString += criteria(i)._1 + " = ? AND " - } - queryString += criteria.last._1 + " = ?" - } - - val preparedStatement = connection.prepareStatement(queryString) - - for (i <- 0 until criteria.size) { - preparedStatement.setObject(i + 1, criteria(i)._2) - } - - val deletedCount = preparedStatement.executeUpdate() - - preparedStatement.close() - println("Deleted " + deletedCount + " shuttleService records") - deletedCount - - } finally { - connection.close() - } - - } - def saveTransaction(transaction : AirlineTransaction) = { val connection = Meta.getConnection() @@ -1225,7 +1103,8 @@ object AirlineSource { def saveAirlineModifier(airlineId : Int, modifier : AirlineModifier) = { val connection = Meta.getConnection() try { - val preparedStatement = connection.prepareStatement(s"REPLACE INTO $AIRLINE_MODIFIER_TABLE (airline, modifier_name, creation, expiry) VALUES(?, ?, ?, ?)") + val preparedStatement = connection.prepareStatement(s"INSERT INTO $AIRLINE_MODIFIER_TABLE (airline, modifier_name, creation, expiry) VALUES(?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS) + preparedStatement.setInt(1, airlineId) preparedStatement.setString(2, modifier.modifierType.toString) preparedStatement.setInt(3, modifier.creationCycle) @@ -1234,29 +1113,86 @@ object AirlineSource { case None => preparedStatement.setNull(4, java.sql.Types.INTEGER) } preparedStatement.executeUpdate() - + val generatedKeys = preparedStatement.getGeneratedKeys + if (generatedKeys.next()) { + val generatedId = generatedKeys.getInt(1) + modifier.id = generatedId + } preparedStatement.close() + + saveAirlineModifierProperties(modifier) + AirlineCache.invalidateAirline(airlineId) } finally { connection.close() } } + private[this] def saveAirlineModifierProperties(modifier : AirlineModifier) = { + val connection = Meta.getConnection() + try { + val preparedStatement = connection.prepareStatement(s"REPLACE INTO $AIRLINE_MODIFIER_PROPERTY_TABLE (id, name, value) VALUES(?, ?, ?)") + + modifier.properties.foreach { + case (propertyType, value) => + preparedStatement.setInt(1, modifier.id) + preparedStatement.setString(2, propertyType.toString) + preparedStatement.setLong(3, value) + preparedStatement.executeUpdate() + } + preparedStatement.close() + } finally { + connection.close() + } + } + + def loadAirlineModifierProperties() = { + val connection = Meta.getConnection() + try { + val preparedStatement = connection.prepareStatement("SELECT * FROM " + AIRLINE_MODIFIER_PROPERTY_TABLE) + + val resultSet = preparedStatement.executeQuery() + + val result : Map[Int, Map[AirlineModifierPropertyType.Value, Long]] = Map() + while (resultSet.next()) { + val propertyType = AirlineModifierPropertyType.withName(resultSet.getString("name")) + val value = resultSet.getLong("value") + val id = resultSet.getInt("id") + val map = result.getOrElseUpdate(id, Map()) + map.put(propertyType, value) + } + + resultSet.close() + preparedStatement.close() + + result.view.mapValues(_.toMap).toMap + } finally { + connection.close() + } + } + def loadAirlineModifiers() : List[(Int, AirlineModifier)] = { //_1 is airline Id val connection = Meta.getConnection() try { + val propertiesById = loadAirlineModifierProperties() + val preparedStatement = connection.prepareStatement("SELECT * FROM " + AIRLINE_MODIFIER_TABLE) val resultSet = preparedStatement.executeQuery() val result : ListBuffer[(Int, AirlineModifier)] = ListBuffer[(Int, AirlineModifier)]() while (resultSet.next()) { val expiryObject = resultSet.getObject("expiry") + val id = resultSet.getInt("id") val airlineModifier = AirlineModifier.fromValues( AirlineModifierType.withName(resultSet.getString("modifier_name")), resultSet.getInt("creation"), - if (expiryObject == null) None else Some(expiryObject.asInstanceOf[Int]) + if (expiryObject == null) None else Some(expiryObject.asInstanceOf[Int]), + propertiesById.get(id).getOrElse(immutable.Map.empty) ) + airlineModifier.id = id + + result.append((resultSet.getInt("airline"), airlineModifier)) } @@ -1282,7 +1218,8 @@ object AirlineSource { val airlineModifier = AirlineModifier.fromValues( AirlineModifierType.withName(resultSet.getString("modifier_name")), resultSet.getInt("creation"), - if (expiryObject == null) None else Some(expiryObject.asInstanceOf[Int]) + if (expiryObject == null) None else Some(expiryObject.asInstanceOf[Int]), + loadAirlineModifierPropertiesById(resultSet.getInt("id")) ) result.append(airlineModifier) } @@ -1295,4 +1232,27 @@ object AirlineSource { connection.close() } } + + def loadAirlineModifierPropertiesById(id : Int) = { + val connection = Meta.getConnection() + try { + val preparedStatement = connection.prepareStatement("SELECT * FROM " + AIRLINE_MODIFIER_PROPERTY_TABLE + " WHERE id = ?") + preparedStatement.setInt(1, id) + + val resultSet = preparedStatement.executeQuery() + val result = Map[AirlineModifierPropertyType.Value, Long]() + while (resultSet.next()) { + val propertyType = AirlineModifierPropertyType.withName(resultSet.getString("name")) + val value = resultSet.getLong("value") + result.put(propertyType, value) + } + + resultSet.close() + preparedStatement.close() + + result.toMap + } finally { + connection.close() + } + } } \ No newline at end of file diff --git a/airline-data/src/main/scala/com/patson/data/AllianceSource.scala b/airline-data/src/main/scala/com/patson/data/AllianceSource.scala index 910005891..860691ed7 100644 --- a/airline-data/src/main/scala/com/patson/data/AllianceSource.scala +++ b/airline-data/src/main/scala/com/patson/data/AllianceSource.scala @@ -307,8 +307,13 @@ object AllianceSource { } val resultSet = queryStatement.executeQuery() + while (resultSet.next()) { - AllianceCache.invalidateAlliance(resultSet.getInt("id")) + val allianceId = resultSet.getInt("id") + AllianceCache.invalidateAlliance(allianceId) + loadAllianceMembersByAllianceId(allianceId, false).foreach { member => + AirlineCache.invalidateAirline(member.airline.id) + } } resultSet.close() queryStatement.close() diff --git a/airline-data/src/main/scala/com/patson/data/ColorSource.scala b/airline-data/src/main/scala/com/patson/data/ColorSource.scala new file mode 100644 index 000000000..208357972 --- /dev/null +++ b/airline-data/src/main/scala/com/patson/data/ColorSource.scala @@ -0,0 +1,95 @@ +package com.patson.data + +import com.patson.data.Constants._ +import com.patson.model._ +import com.patson.model.campaign.Campaign +import com.patson.util.{AirlineCache, AirportCache, CountryCache} + +import java.sql.{Statement, Types} +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + + +object ColorSource { + + def loadAllianceLabelColorsFromAlliance(currentAllianceId : Int) : Map[Int, String] = { + loadAllianceLabelColorsFromId("alliance", currentAllianceId, ALLIANCE_LABEL_COLOR_BY_ALLIANCE_TABLE) + } + + def loadAllianceLabelColorsFromAirline(currentAirlineId : Int) : Map[Int, String] = { + loadAllianceLabelColorsFromId("airline", currentAirlineId, ALLIANCE_LABEL_COLOR_BY_AIRLINE_TABLE) + } + + def loadAllianceLabelColorsFromId(idColumn : String, id : Int, table : String) : Map[Int, String] = { + val connection = Meta.getConnection() + try { + val preparedStatement = connection.prepareStatement(s"SELECT * FROM $table WHERE $idColumn = ?") + + preparedStatement.setInt(1, id) + + val resultSet = preparedStatement.executeQuery() + + val result = mutable.Map[Int, String]() //key: Alliance id + + + while (resultSet.next()) { + val targetAllianceId = resultSet.getInt("target_alliance") + val color = resultSet.getString("color") + result.put(targetAllianceId, color) + } + + resultSet.close() + preparedStatement.close() + + result.toMap + } finally { + connection.close() + } + } + + def saveAllianceLabelColorFromAlliance(currentAllianceId : Int, targetAllianceId : Int, color : String) = { + saveAllianceLabelColor("alliance", currentAllianceId, ALLIANCE_LABEL_COLOR_BY_ALLIANCE_TABLE, targetAllianceId, color) + } + + def saveAllianceLabelColorFromAirline(currentAirlineId : Int, targetAllianceId : Int, color : String) = { + saveAllianceLabelColor("airline", currentAirlineId, ALLIANCE_LABEL_COLOR_BY_AIRLINE_TABLE, targetAllianceId, color) + } + + + def saveAllianceLabelColor(idColumn : String, id : Int, table : String, targetAllianceId : Int, color : String) = { + val connection = Meta.getConnection() + val preparedStatement = connection.prepareStatement(s"REPLACE INTO $table($idColumn, target_alliance, color) VALUES(?,?,?)") + try { + preparedStatement.setInt(1, id) + preparedStatement.setInt(2, targetAllianceId) + preparedStatement.setString(3, color) + preparedStatement.executeUpdate() + } finally { + preparedStatement.close() + connection.close() + } + } + + def deleteAllianceLabelColorFromAlliance(currentAllianceId : Int, targetAllianceId : Int) = { + deleteAllianceLabelColor("alliance", currentAllianceId, ALLIANCE_LABEL_COLOR_BY_ALLIANCE_TABLE, targetAllianceId) + } + + def deleteAllianceLabelColorFromAirline(currentAirlineId : Int, targetAllianceId : Int) = { + deleteAllianceLabelColor("airline", currentAirlineId, ALLIANCE_LABEL_COLOR_BY_AIRLINE_TABLE, targetAllianceId) + } + + def deleteAllianceLabelColor(idColumn : String, id : Int, table : String, targetAllianceId : Int) = { + val connection = Meta.getConnection() + val preparedStatement = connection.prepareStatement(s"DELETE FROM $table WHERE $idColumn = ? AND target_alliance = ?") + try { + preparedStatement.setInt(1, id) + preparedStatement.setInt(2, targetAllianceId) + + preparedStatement.executeUpdate() + } finally { + preparedStatement.close() + connection.close() + } + } + +} \ No newline at end of file diff --git a/airline-data/src/main/scala/com/patson/data/Constants.scala b/airline-data/src/main/scala/com/patson/data/Constants.scala index e21a1d8c2..8a0d56990 100644 --- a/airline-data/src/main/scala/com/patson/data/Constants.scala +++ b/airline-data/src/main/scala/com/patson/data/Constants.scala @@ -66,6 +66,8 @@ object Constants { val AIRLINE_BASE_SPECIALIZATION_LAST_UPDATE_TABLE = "airline_base_specialization_last_update" val AIRLINE_REPUTATION_BREAKDOWN = "airline_reputation_breakdown" val AIRLINE_MODIFIER_TABLE = "airline_modifier" + val AIRLINE_MODIFIER_INDEX_PREFIX = "airline_modifier_index_" + val AIRLINE_MODIFIER_PROPERTY_TABLE = "airline_modifier_property" val INCOME_TABLE = "income" @@ -116,6 +118,7 @@ object Constants { val OIL_INVENTORY_POLICY_TABLE = "oil_inventory_policy" val LOAN_INTEREST_RATE_TABLE = "loan_interest_rate" val LOG_TABLE = "log" + val LOG_PROPERTY_TABLE = "log_property" val LAST_CHAT_ID_TABLE = "last_chat_id" val CHAT_MESSAGE_TABLE = "chat_message" val LOG_INDEX_1 = "log_index_1" @@ -151,6 +154,9 @@ object Constants { val LINK_NEGOTIATION_DISCOUNT_TABLE = "link_negotiation_discount" + val ALLIANCE_LABEL_COLOR_BY_ALLIANCE_TABLE = "alliance_label_color_by_alliance" + val ALLIANCE_LABEL_COLOR_BY_AIRLINE_TABLE = "alliance_label_color_by_airline" + //Christmas Event val SANTA_CLAUS_INFO_TABLE = "santa_claus_info" val SANTA_CLAUS_GUESS_TABLE = "santa_claus_guess" @@ -159,12 +165,15 @@ object Constants { // val DB_DRIVER = "org.sqlite.JDBC" val configFactory = ConfigFactory.load() val DB_HOST = if (configFactory.hasPath("mysqldb.host")) configFactory.getString("mysqldb.host") else "localhost:3306" + val dbParams = if (configFactory.hasPath("mysqldb.dbParams")) configFactory.getString("mysqldb.dbParams") else "" println("!!!!!!!!!!!!!!!DB HOST IS " + DB_HOST) //val DATABASE_CONNECTION = "jdbc:mysql://" + DB_HOST + "/airline?rewriteBatchedStatements=true&useSSL=false&autoReconnect=true&useUnicode=true&characterEncoding=utf-8" - val DATABASE_CONNECTION = "jdbc:mysql://" + DB_HOST + "/airline_v2?rewriteBatchedStatements=true&useSSL=false&autoReconnect=true&useUnicode=true&characterEncoding=utf-8" + val DATABASE_CONNECTION = "jdbc:mysql://" + DB_HOST + "/airline_v2?rewriteBatchedStatements=true&useSSL=false&autoReconnect=true&useUnicode=true&characterEncoding=utf-8" + dbParams val DB_DRIVER = "com.mysql.jdbc.Driver" - val DATABASE_USER = "sa" - val DATABASE_PASSWORD = "admin" + val DATABASE_USER = if (configFactory.hasPath("mysqldb.user")) configFactory.getString("mysqldb.user") else "sa" + val DATABASE_PASSWORD = if (configFactory.hasPath("mysqldb.password")) configFactory.getString("mysqldb.password") else "admin" + + println(s"!!!!!!!!!!!!!!!FINAL DB str $DATABASE_CONNECTION with user $DATABASE_USER") } \ No newline at end of file diff --git a/airline-data/src/main/scala/com/patson/data/LinkSource.scala b/airline-data/src/main/scala/com/patson/data/LinkSource.scala index 33d457b5e..66173327b 100644 --- a/airline-data/src/main/scala/com/patson/data/LinkSource.scala +++ b/airline-data/src/main/scala/com/patson/data/LinkSource.scala @@ -105,7 +105,7 @@ object LinkSource { val fromAirport = airportCache.get(fromAirportId) //Do not use AirportCache as fullLoad will be slow val toAirport = airportCache.get(toAirportId) //Do not use AirportCache as fullLoad will be slow val airline = loadDetails.get(DetailType.AIRLINE) match { - case Some(fullLoad) => AirlineCache.getAirline(airlineId, fullLoad) + case Some(fullLoad) => AirlineCache.getAirline(airlineId, fullLoad).orElse(Some(Airline.fromId(airlineId))) case None => Some(Airline.fromId(airlineId)) } @@ -127,12 +127,11 @@ object LinkSource { resultSet.getInt("frequency"), FlightType(resultSet.getInt("flight_type")), resultSet.getInt("flight_number")) - case SHUTTLE => + case GENERIC_TRANSIT => //from : Airport, to : Airport, airline: Airline, distance : Int, var capacity: LinkClassValues, duration : Int, var frequency : Int, var id : Int = 0 - Shuttle( + GenericTransit( fromAirport.get, toAirport.get, - airline.get, resultSet.getInt("distance"), LinkClassValues.getInstance(resultSet.getInt("capacity_economy"), resultSet.getInt("capacity_business"), resultSet.getInt("capacity_first")), resultSet.getInt("duration") @@ -346,9 +345,9 @@ object LinkSource { case TransportType.FLIGHT => val flightLink = link.asInstanceOf[Link] (flightLink.from.id, flightLink.to.id, flightLink.airline.id, flightLink.price, flightLink.distance, flightLink.capacity, flightLink.rawQuality, flightLink.duration, flightLink.frequency, flightLink.flightType, flightLink.flightNumber, flightLink.getAssignedAirplanes) - case TransportType.SHUTTLE => - val shuttle = link.asInstanceOf[Shuttle] - (shuttle.from.id, shuttle.to.id, shuttle.airline.id, shuttle.price, shuttle.distance, shuttle.capacity, Shuttle.QUALITY, shuttle.duration, shuttle.frequency, shuttle.flightType, 0, Map.empty) + case TransportType.GENERIC_TRANSIT => + val genericTransit = link.asInstanceOf[GenericTransit] + (genericTransit.from.id, genericTransit.to.id, 0, genericTransit.price, genericTransit.distance, genericTransit.capacity, GenericTransit.QUALITY, genericTransit.duration, genericTransit.frequency, genericTransit.flightType, 0, Map.empty) } @@ -769,7 +768,7 @@ object LinkSource { def saveLinkConsumptions(linkConsumptions: List[LinkConsumptionDetails]) = { //open the hsqldb val connection = Meta.getConnection() - val preparedStatement = connection.prepareStatement("REPLACE INTO " + LINK_CONSUMPTION_TABLE + "(link, price_economy, price_business, price_first, capacity_economy, capacity_business, capacity_first, sold_seats_economy, sold_seats_business, sold_seats_first, quality, fuel_cost, crew_cost, airport_fees, inflight_cost, delay_compensation, maintenance_cost, lounge_cost, depreciation, revenue, profit, minor_delay_count, major_delay_count, cancellation_count, from_airport, to_airport, airline, distance, frequency, duration, flight_type, flight_number, airplane_model, raw_quality, satisfaction, cycle) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)") + val preparedStatement = connection.prepareStatement("REPLACE INTO " + LINK_CONSUMPTION_TABLE + "(link, price_economy, price_business, price_first, capacity_economy, capacity_business, capacity_first, sold_seats_economy, sold_seats_business, sold_seats_first, quality, fuel_cost, crew_cost, airport_fees, inflight_cost, delay_compensation, maintenance_cost, lounge_cost, depreciation, revenue, profit, minor_delay_count, major_delay_count, cancellation_count, from_airport, to_airport, airline, distance, frequency, duration, transport_type, flight_type, flight_number, airplane_model, raw_quality, satisfaction, cycle) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)") try { connection.setAutoCommit(false) @@ -804,18 +803,19 @@ object LinkSource { preparedStatement.setInt(28, linkConsumption.link.distance) preparedStatement.setInt(29, linkConsumption.link.frequency) preparedStatement.setInt(30, linkConsumption.link.duration) - preparedStatement.setInt(31, linkConsumption.link.flightType.id) + preparedStatement.setInt(31, linkConsumption.link.transportType.id) + preparedStatement.setInt(32, linkConsumption.link.flightType.id) if (linkConsumption.link.isInstanceOf[Link]) { - preparedStatement.setInt(32, linkConsumption.link.asInstanceOf[Link].flightNumber) - preparedStatement.setInt(33, linkConsumption.link.asInstanceOf[Link].getAssignedModel().map(_.id).getOrElse(0)) - preparedStatement.setInt(34, linkConsumption.link.asInstanceOf[Link].rawQuality) + preparedStatement.setInt(33, linkConsumption.link.asInstanceOf[Link].flightNumber) + preparedStatement.setInt(34, linkConsumption.link.asInstanceOf[Link].getAssignedModel().map(_.id).getOrElse(0)) + preparedStatement.setInt(35, linkConsumption.link.asInstanceOf[Link].rawQuality) } else { - preparedStatement.setNull(32, Types.INTEGER) preparedStatement.setNull(33, Types.INTEGER) preparedStatement.setNull(34, Types.INTEGER) + preparedStatement.setNull(35, Types.INTEGER) } - preparedStatement.setDouble(35, linkConsumption.satisfaction) - preparedStatement.setInt(36, linkConsumption.cycle) + preparedStatement.setDouble(36, linkConsumption.satisfaction) + preparedStatement.setInt(37, linkConsumption.cycle) preparedStatement.executeUpdate() } preparedStatement.close() @@ -925,9 +925,24 @@ object LinkSource { val flightNumber = resultSet.getInt("flight_number") val modelId = resultSet.getInt("airplane_model") val rawQuality = resultSet.getInt("raw_quality") - val link = Link(fromAirport, toAirport, airline, price, distance, capacity, 0, duration, frequency, FlightType(flightType), flightNumber, linkId) + val transportType = resultSet.getInt("transport_type") + val link = + if (transportType == TransportType.FLIGHT.id) { + Link(fromAirport, toAirport, airline, price, distance, capacity, 0, duration, frequency, FlightType(flightType), flightNumber, linkId) + } else if (transportType == TransportType.GENERIC_TRANSIT.id) { + GenericTransit(fromAirport, toAirport, distance, capacity, linkId) + } else { + println("Unknown transport type for link consumption : " + resultSet.getInt("transport_type")) + Link(fromAirport, toAirport, airline, price, distance, capacity, 0, duration, frequency, FlightType(flightType), flightNumber, linkId) + } + + link match { + case flight : Link => + flight.setQuality(quality) + flight.setAssignedModel(AirplaneModelCache.getModel(modelId).getOrElse(Model.fromId(modelId))) + case _ => //ok + } - link.setQuality(quality) link.addSoldSeats(LinkClassValues.getInstance(resultSet.getInt("sold_seats_economy"), resultSet.getInt("sold_seats_business"), resultSet.getInt("sold_seats_first"))) link.minorDelayCount = resultSet.getInt("minor_delay_count") link.majorDelayCount = resultSet.getInt("major_delay_count") @@ -937,7 +952,7 @@ object LinkSource { link.addCancelledSeats(capacity * link.cancellationCount / frequency) } - link.setAssignedModel(AirplaneModelCache.getModel(modelId).getOrElse(Model.fromId(modelId))) + linkConsumptions.append(LinkConsumptionDetails( link = link, diff --git a/airline-data/src/main/scala/com/patson/data/LogSource.scala b/airline-data/src/main/scala/com/patson/data/LogSource.scala index fccd12567..7cd35b030 100644 --- a/airline-data/src/main/scala/com/patson/data/LogSource.scala +++ b/airline-data/src/main/scala/com/patson/data/LogSource.scala @@ -1,46 +1,58 @@ package com.patson.data import com.patson.data.Constants._ - -import scala.collection.mutable.ListBuffer -import java.sql.DriverManager - -import com.patson.model.airplane.Airplane -import java.sql.PreparedStatement - import com.patson.model._ -import java.sql.Statement -import java.sql.ResultSet - import com.patson.util.AirlineCache -import scala.collection.mutable.HashMap -import scala.collection.mutable.Map +import java.sql.Statement +import scala.collection.immutable +import scala.collection.mutable.{HashMap, ListBuffer, Map} object LogSource { val insertLogs = (logs : List[Log]) => { val connection = Meta.getConnection() //case class Log(airline : Airline, message : String, cateogry : LogCategory.Value, severity : LogSeverity.Value, cycle : Int) - val statement = connection.prepareStatement("INSERT INTO " + LOG_TABLE + "(airline, message, category, severity, cycle) VALUES(?,?,?,?,?)") - + val statement = connection.prepareStatement("INSERT INTO " + LOG_TABLE + "(airline, message, category, severity, cycle) VALUES(?,?,?,?,?)", Statement.RETURN_GENERATED_KEYS) + val propertyStatement = connection.prepareStatement(s"INSERT INTO $LOG_PROPERTY_TABLE(log, property, value) VALUES(?,?,?)") connection.setAutoCommit(false) - + + val pendingProperties = ListBuffer[(Int, immutable.Map[String, String])]() try { logs.foreach { - case Log(airline : Airline, message : String, category : LogCategory.Value, severity : LogSeverity.Value, cycle : Int) => { + case Log(airline : Airline, message : String, category : LogCategory.Value, severity : LogSeverity.Value, cycle : Int, properties : immutable.Map[String, String]) => { statement.setInt(1, airline.id) statement.setString(2, message) statement.setInt(3, category.id) statement.setInt(4, severity.id) statement.setInt(5, cycle) - statement.addBatch() + statement.execute() + + val generatedKeys = statement.getGeneratedKeys + if (generatedKeys.next()) { + val generatedId = generatedKeys.getInt(1) + pendingProperties.append((generatedId, properties)) + } } } - statement.executeBatch() - + + + + pendingProperties.foreach { + case(logId, properties) => + propertyStatement.setInt(1, logId) + properties.foreach { + case(property, value) => + propertyStatement.setString(2, property) + propertyStatement.setString(3, value) + propertyStatement.addBatch() + } + + } + propertyStatement.executeBatch() connection.commit() } finally { statement.close() + propertyStatement.close() connection.close() } } @@ -66,38 +78,73 @@ object LogSource { private def loadLogsByQueryString(queryString : String, parameters : List[Any], fullLoad : Boolean = false) : List[Log] = { val connection = Meta.getConnection() + val preparedStatement = connection.prepareStatement(queryString) + try { - val preparedStatement = connection.prepareStatement(queryString) - - for (i <- 0 until parameters.size) { - preparedStatement.setObject(i + 1, parameters(i)) - } - - - val resultSet = preparedStatement.executeQuery() - - val logs = ListBuffer[Log]() - - - val airlines = Map[Int, Airline]() - - while (resultSet.next()) { - val airlineId = resultSet.getInt("airline") - val airline = airlines.getOrElseUpdate(airlineId, AirlineCache.getAirline(airlineId, fullLoad).getOrElse(Airline.fromId(airlineId))) - val message = resultSet.getString("message") - val category = LogCategory(resultSet.getInt("category")) - val severity = LogSeverity(resultSet.getInt("severity")) - val cycle = resultSet.getInt("cycle") - logs += Log(airline, message, category, severity, cycle) + for (i <- 0 until parameters.size) { + preparedStatement.setObject(i + 1, parameters(i)) + } + + val resultSet = preparedStatement.executeQuery() + + val logs = ListBuffer[Log]() + + + val airlines = Map[Int, Airline]() + + val ids = ListBuffer[Int]() + while (resultSet.next()) { + ids.append(resultSet.getInt("id")) + } + resultSet.beforeFirst() + + val propertiesById = HashMap[Int, HashMap[String, String]]() + if (!ids.isEmpty) { + val propertyStatement = connection.prepareStatement(s"SELECT * FROM $LOG_PROPERTY_TABLE WHERE log IN (${ids.mkString(",")})") + try { + val propertyResultSet = propertyStatement.executeQuery() + while (propertyResultSet.next()) { + val logId = propertyResultSet.getInt("log") + val property = propertyResultSet.getString("property") + val value = propertyResultSet.getString("value") + val properties = propertiesById.getOrElseUpdate(logId, HashMap()) + properties.put(property, value) + } + + } finally { + propertyStatement.close() } - - resultSet.close() - preparedStatement.close() - - logs.toList - } finally { - connection.close() + } + + + while (resultSet.next()) { + val airlineId = resultSet.getInt("airline") + val airline = airlines.getOrElseUpdate(airlineId, AirlineCache.getAirline(airlineId, fullLoad).getOrElse(Airline.fromId(airlineId))) + val message = resultSet.getString("message") + val category = LogCategory(resultSet.getInt("category")) + val severity = LogSeverity(resultSet.getInt("severity")) + val cycle = resultSet.getInt("cycle") + val id = resultSet.getInt("id") + logs += Log( + airline, message, category, severity, cycle, + propertiesById.get(id) match { + case Some(properties) => properties.toMap + case None => scala.collection.immutable.Map.empty + } + ) + } + + + resultSet.close() + preparedStatement.close() + + logs.toList + } finally { + preparedStatement.close() + + connection.close() + } } def deleteLogsBeforeCycle(cutoffCycle : Int) = { diff --git a/airline-data/src/main/scala/com/patson/data/Meta.scala b/airline-data/src/main/scala/com/patson/data/Meta.scala index de2745d2d..7c06fb088 100644 --- a/airline-data/src/main/scala/com/patson/data/Meta.scala +++ b/airline-data/src/main/scala/com/patson/data/Meta.scala @@ -290,6 +290,7 @@ object Meta { createLoanInterestRate(connection) createResetUser(connection) createLog(connection) + createLogProperty(connection) createAlert(connection) createEvent(connection) createSantaClaus(connection) @@ -315,7 +316,9 @@ object Meta { createAdminLog(connection) createUserUuid(connection) createAirlineModifier(connection) + createAirlineModifierProperty(connection) createUserModifier(connection) + createAllianceLabelColor(connection) statement = connection.prepareStatement("CREATE TABLE " + AIRPORT_CITY_SHARE_TABLE + "(" + "airport INTEGER," + @@ -389,8 +392,7 @@ object Meta { "transport_type SMALLINT," + "last_update DATETIME DEFAULT CURRENT_TIMESTAMP," + "FOREIGN KEY(from_airport) REFERENCES " + AIRPORT_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE," + - "FOREIGN KEY(to_airport) REFERENCES " + AIRPORT_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE," + - "FOREIGN KEY(airline) REFERENCES " + AIRLINE_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE" + + "FOREIGN KEY(to_airport) REFERENCES " + AIRPORT_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE" + ")") statement.execute() @@ -446,6 +448,7 @@ object Meta { "distance INTEGER, " + "frequency SMALLINT, " + "duration SMALLINT, " + + "transport_type TINYINT, " + "flight_type TINYINT, " + "flight_number SMALLINT, " + "airplane_model SMALLINT, " + @@ -1116,6 +1119,7 @@ object Meta { statement.close() statement = connection.prepareStatement("CREATE TABLE " + LOG_TABLE + "(" + + "id INTEGER PRIMARY KEY AUTO_INCREMENT," + "airline INTEGER, " + "message VARCHAR(512) CHARACTER SET 'utf8'," + "category INTEGER," + @@ -1131,6 +1135,21 @@ object Meta { statement.close() } + def createLogProperty(connection : Connection) { + var statement = connection.prepareStatement("DROP TABLE IF EXISTS " + LOG_PROPERTY_TABLE) + statement.execute() + statement.close() + + statement = connection.prepareStatement("CREATE TABLE " + LOG_PROPERTY_TABLE + "(" + + "log INTEGER," + + "property VARCHAR(256)," + + "value VARCHAR(256)," + + "FOREIGN KEY(log) REFERENCES " + LOG_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE" + + ")") + statement.execute() + statement.close() + } + def createAlert(connection : Connection) { var statement = connection.prepareStatement("DROP TABLE IF EXISTS " + ALERT_TABLE) statement.execute() @@ -1958,11 +1977,12 @@ object Meta { statement.close() statement = connection.prepareStatement("CREATE TABLE " + AIRLINE_MODIFIER_TABLE + "(" + + "id INTEGER PRIMARY KEY AUTO_INCREMENT, " + "airline INTEGER, " + "modifier_name CHAR(20), " + "creation INTEGER," + "expiry INTEGER," + - "PRIMARY KEY (airline, modifier_name)," + + "INDEX " + AIRLINE_MODIFIER_INDEX_PREFIX + 1 + " (airline)," + "FOREIGN KEY(airline) REFERENCES " + AIRLINE_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE" + ")" ) @@ -1970,6 +1990,23 @@ object Meta { statement.close() } + def createAirlineModifierProperty(connection : Connection) { + var statement = connection.prepareStatement("DROP TABLE IF EXISTS " + AIRLINE_MODIFIER_PROPERTY_TABLE) + statement.execute() + statement.close() + + statement = connection.prepareStatement("CREATE TABLE " + AIRLINE_MODIFIER_PROPERTY_TABLE + "(" + + "id INTEGER," + + "name VARCHAR(256), " + + "value INTEGER," + + "PRIMARY KEY(id, name)," + + "FOREIGN KEY(id) REFERENCES " + AIRLINE_MODIFIER_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE" + + ")" + ) + statement.execute() + statement.close() + } + def createUserModifier(connection : Connection) { var statement = connection.prepareStatement("DROP TABLE IF EXISTS " + USER_MODIFIER_TABLE) statement.execute() @@ -1988,6 +2025,40 @@ object Meta { } + def createAllianceLabelColor(connection : Connection) { + var statement = connection.prepareStatement("DROP TABLE IF EXISTS " + ALLIANCE_LABEL_COLOR_BY_ALLIANCE_TABLE) + statement.execute() + statement.close() + + statement = connection.prepareStatement("DROP TABLE IF EXISTS " + ALLIANCE_LABEL_COLOR_BY_AIRLINE_TABLE) + statement.execute() + statement.close() + + statement = connection.prepareStatement("CREATE TABLE " + ALLIANCE_LABEL_COLOR_BY_AIRLINE_TABLE + "(" + + "airline INTEGER, " + + "target_alliance INTEGER," + + "color CHAR(20)," + + "PRIMARY KEY (airline, target_alliance)," + + "FOREIGN KEY(airline) REFERENCES " + AIRLINE_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE," + + "FOREIGN KEY(target_alliance) REFERENCES " + ALLIANCE_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE" + + ")" + ) + statement.execute() + statement.close() + + statement = connection.prepareStatement("CREATE TABLE " + ALLIANCE_LABEL_COLOR_BY_ALLIANCE_TABLE + "(" + + "alliance INTEGER, " + + "target_alliance INTEGER," + + "color CHAR(20)," + + "PRIMARY KEY (alliance, target_alliance)," + + "FOREIGN KEY(alliance) REFERENCES " + ALLIANCE_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE," + + "FOREIGN KEY(target_alliance) REFERENCES " + ALLIANCE_TABLE + "(id) ON DELETE CASCADE ON UPDATE CASCADE" + + ")" + ) + statement.execute() + statement.close() + } + diff --git a/airline-data/src/main/scala/com/patson/data/QuickCreateSchema.scala b/airline-data/src/main/scala/com/patson/data/QuickCreateSchema.scala index 3e21ad0eb..303b2b366 100644 --- a/airline-data/src/main/scala/com/patson/data/QuickCreateSchema.scala +++ b/airline-data/src/main/scala/com/patson/data/QuickCreateSchema.scala @@ -51,7 +51,7 @@ object QuickCreateSchema extends App { // Meta.createLog(connection) // Meta.createAlert(connection) //Meta.createDelegate(connection) - Meta.createUserModifier(connection) + Meta.createLogProperty(connection) } diff --git a/airline-data/src/main/scala/com/patson/data/UserSource.scala b/airline-data/src/main/scala/com/patson/data/UserSource.scala index 1cac6f163..37b620ff8 100644 --- a/airline-data/src/main/scala/com/patson/data/UserSource.scala +++ b/airline-data/src/main/scala/com/patson/data/UserSource.scala @@ -1,6 +1,6 @@ package com.patson.data -import com.patson.model._ +import com.patson.model.{UserModifier, _} import com.patson.data.Constants._ import scala.collection.mutable.ListBuffer @@ -10,6 +10,8 @@ import java.sql.Statement import java.util.Date import com.patson.util.{AirlineCache, UserCache} +import scala.collection.mutable + object UserSource { val dateFormat = new ThreadLocal[SimpleDateFormat]() { override def initialValue() = { @@ -81,7 +83,10 @@ object UserSource { val resultSet = preparedStatement.executeQuery() val userList = scala.collection.mutable.Map[Int, (User, ListBuffer[Int])]() //Map[UserId, (User, List[AirlineId])] - + + val modifiersByUserId : Map[Int, List[UserModifier.Value]] = UserSource.loadUserModifiers() + + while (resultSet.next()) { val userId = resultSet.getInt("u.id") val (user, userAirlines) = userList.getOrElseUpdate(userId, { @@ -94,7 +99,7 @@ object UserSource { val adminStatusObject = resultSet.getObject("u.admin_status") val adminStatus = if (adminStatusObject == null) None else Some(AdminStatus.withName(adminStatusObject.asInstanceOf[String])) - val modifiers = UserSource.loadUserModifierByUserId(userId) //this could be slow if we load all users + val modifiers = modifiersByUserId.getOrElse(userId, List.empty) (User(userName, resultSet.getString("u.email"), creationTime, lastActiveTime, status, level = resultSet.getInt("level"), adminStatus = adminStatus, modifiers = modifiers, id = userId), ListBuffer[Int]()) }) @@ -316,22 +321,23 @@ object UserSource { } - def loadUserModifiers() : List[(Int, UserModifier.Value)] = { //_1 is user Id + def loadUserModifiers() : Map[Int, List[UserModifier.Value]] = { //_1 is user Id val connection = Meta.getConnection() try { val preparedStatement = connection.prepareStatement("SELECT * FROM " + USER_MODIFIER_TABLE) val resultSet = preparedStatement.executeQuery() - val result : ListBuffer[(Int, UserModifier.Value)] = ListBuffer[(Int, UserModifier.Value)]() + val result = mutable.HashMap[Int, ListBuffer[UserModifier.Value]]() while (resultSet.next()) { val userModifier = UserModifier.withName(resultSet.getString("modifier_name")) - result.append((resultSet.getInt("user"), userModifier)) + val modifiers = result.getOrElseUpdate(resultSet.getInt("user"), ListBuffer()) + modifiers.append(userModifier) } resultSet.close() preparedStatement.close() - result.toList + result.view.mapValues(_.toList).toMap } finally { connection.close() } diff --git a/airline-data/src/main/scala/com/patson/init/GenericTransitGenerator.scala b/airline-data/src/main/scala/com/patson/init/GenericTransitGenerator.scala new file mode 100644 index 000000000..afedebc2d --- /dev/null +++ b/airline-data/src/main/scala/com/patson/init/GenericTransitGenerator.scala @@ -0,0 +1,68 @@ +package com.patson.init + +import com.patson.{Authentication, DemandGenerator, Util} +import com.patson.data._ +import com.patson.data.airplane._ +import com.patson.init.GeoDataGenerator.calculateLongitudeBoundary +import com.patson.model._ +import com.patson.model.airplane._ + +import java.util.Calendar +import scala.collection.mutable +import scala.collection.mutable.{ListBuffer, Set} +import scala.concurrent.Await +import scala.concurrent.duration.Duration +import scala.util.Random + + +object GenericTransitGenerator { + + def generateGenericTransit(airportCount : Int = 4000, range : Int = 50) : Unit = { + //purge existing generic transit + LinkSource.deleteLinksByCriteria(List(("transport_type", TransportType.GENERIC_TRANSIT.id))) + + val airports = AirportSource.loadAllAirports(true).sortBy { _.power }.takeRight(airportCount) + + var counter = 0; + var progressCount = 0; + + val processed = mutable.HashSet[(Int, Int)]() + val countryRelationships = CountrySource.getCountryMutualRelationships() + for (airport <- airports) { + //calculate max and min longitude that we should kick off the calculation + val boundaryLongitude = calculateLongitudeBoundary(airport.latitude, airport.longitude, range) + val airportsInRange = scala.collection.mutable.ListBuffer[(Airport, Double)]() + for (targetAirport <- airports) { + if (airport.id != targetAirport.id && + !processed.contains((targetAirport.id, airport.id)) && //check the swap pairs are not processed already to avoid duplicates + airport.longitude >= boundaryLongitude._1 && airport.longitude <= boundaryLongitude._2 && + countryRelationships.getOrElse((airport.countryCode, targetAirport.countryCode), 0) >= 2) { + val distance = Util.calculateDistance(airport.latitude, airport.longitude, targetAirport.latitude, targetAirport.longitude).toInt + if (range >= distance) { + airportsInRange += Tuple2(targetAirport, distance) + } + } + + processed.add((airport.id, targetAirport.id)) + } + + airportsInRange.foreach { case (targetAirport, distance) => + val minSize = Math.min(airport.size, targetAirport.size) + val capacity = minSize * 10000 //kinda random + val genericTransit = GenericTransit(from = airport, to = targetAirport, distance = distance.toInt, capacity = LinkClassValues.getInstance(economy = capacity)) + LinkSource.saveLink(genericTransit) + println(genericTransit) + } + + val progressChunk = airports.size / 100 + counter += 1 + if (counter % progressChunk == 0) { + progressCount += 1; + print(".") + if (progressCount % 10 == 0) { + print(progressCount + "% ") + } + } + } + } +} \ No newline at end of file diff --git a/airline-data/src/main/scala/com/patson/init/MainInit.scala b/airline-data/src/main/scala/com/patson/init/MainInit.scala index 7d866659a..12b5dcb55 100644 --- a/airline-data/src/main/scala/com/patson/init/MainInit.scala +++ b/airline-data/src/main/scala/com/patson/init/MainInit.scala @@ -9,6 +9,7 @@ object MainInit extends App { Meta.createSchema() GeoDataGenerator.mainFlow() AirplaneModelInitializer.mainFlow() + GenericTransitGenerator.generateGenericTransit() //AirlineGenerator.mainFlow() // AirportProfilePicturePatcher.patchProfilePictures() } \ No newline at end of file diff --git a/airline-data/src/main/scala/com/patson/model/Airline.scala b/airline-data/src/main/scala/com/patson/model/Airline.scala index 7d8c0e2b3..8eacd7cc4 100644 --- a/airline-data/src/main/scala/com/patson/model/Airline.scala +++ b/airline-data/src/main/scala/com/patson/model/Airline.scala @@ -135,7 +135,12 @@ case class Airline(name: String, isGenerated : Boolean = false, var id : Int = 0 val BASE_DELEGATE_COUNT = 5 val DELEGATE_PER_LEVEL = 3 lazy val delegateCount = BASE_DELEGATE_COUNT + airlineGrade.value * DELEGATE_PER_LEVEL + - AirlineSource.loadAirlineModifierByAirlineId(id).find(_.modifierType == AirlineModifierType.DELEGATE_BOOST).map(_ => DelegateBoostAirlineModifier.AMOUNT).getOrElse(0) + + AirlineSource.loadAirlineModifierByAirlineId(id).map { modifier => + modifier match { + case DelegateBoostAirlineModifier(amount, duration, creationCycle) => amount + case _ => 0 + } + }.sum + AirlineSource.loadAirlineBasesByAirline(id).flatMap(_.specializations).filter(_.isInstanceOf[DelegateSpecialization]).map(_.asInstanceOf[DelegateSpecialization].delegateBoost).sum } @@ -286,11 +291,19 @@ object Airline { } //remove all facilities AirlineSource.deleteLoungeByCriteria(List(("airline", airlineId))) - AirlineSource.deleteShuttleServiceByCriteria(List(("airline", airlineId))) + //AirlineSource.deleteShuttleServiceByCriteria(List(("airline", airlineId))) //remove all oil contract OilSource.deleteOilContractByCriteria(List(("airline", airlineId))) + airline.getAllianceId().foreach { allianceId => + AllianceSource.loadAllianceById(allianceId).foreach { alliance => + alliance.members.find(_.airline.id == airline.id).foreach { member => + alliance.removeMember(member, true) + } + } + } + AllianceSource.loadAllianceMemberByAirline(airline).foreach { allianceMember => AllianceSource.deleteAllianceMember(airlineId) if (allianceMember.role == AllianceRole.LEADER) { //remove the alliance @@ -298,6 +311,7 @@ object Airline { } } + AirlineSource.deleteReputationBreakdowns(airline.id) NegotiationSource.deleteLinkDiscountsByAirline(airline.id) @@ -381,16 +395,25 @@ object AirlineGrade { } object AirlineModifier { - def fromValues(modifierType : AirlineModifierType.Value, creationCycle : Int, expiryCycle : Option[Int]) : AirlineModifier = { + def fromValues(modifierType : AirlineModifierType.Value, creationCycle : Int, expiryCycle : Option[Int], properties : Map[AirlineModifierPropertyType.Value, Long]) : AirlineModifier = { import AirlineModifierType._ - modifierType match { + val modifier = modifierType match { case NERFED => NerfedAirlineModifier(creationCycle) - case DELEGATE_BOOST => DelegateBoostAirlineModifier(creationCycle) + case DELEGATE_BOOST => DelegateBoostAirlineModifier( + properties(AirlineModifierPropertyType.STRENGTH).toInt, + properties(AirlineModifierPropertyType.DURATION).toInt, + creationCycle) } + + modifier } } -abstract class AirlineModifier(val modifierType : AirlineModifierType.Value, val creationCycle : Int, val expiryCycle : Option[Int]) + + +abstract class AirlineModifier(val modifierType : AirlineModifierType.Value, val creationCycle : Int, val expiryCycle : Option[Int], var id : Int = 0) extends IdObject { + def properties : Map[AirlineModifierPropertyType.Value, Long] +} case class NerfedAirlineModifier(override val creationCycle : Int) extends AirlineModifier(AirlineModifierType.NERFED, creationCycle, None) { val FULL_EFFECT_DURATION = 300 //completely kicks in after 100 cycles @@ -405,14 +428,13 @@ case class NerfedAirlineModifier(override val creationCycle : Int) extends Airli 1 } } -} -case class DelegateBoostAirlineModifier(override val creationCycle : Int) extends AirlineModifier(AirlineModifierType.DELEGATE_BOOST, creationCycle, Some(creationCycle + DelegateBoostAirlineModifier.DURATION)) { + override def properties : Map[AirlineModifierPropertyType.Value, Long] = Map.empty } -object DelegateBoostAirlineModifier { - val DURATION = 52 - val AMOUNT = 3 +case class DelegateBoostAirlineModifier(amount : Int, duration : Int, override val creationCycle : Int) extends AirlineModifier(AirlineModifierType.DELEGATE_BOOST, creationCycle, Some(creationCycle + duration)) { + lazy val internalProperties = Map[AirlineModifierPropertyType.Value, Long](AirlineModifierPropertyType.STRENGTH -> amount , AirlineModifierPropertyType.DURATION -> duration) + override def properties : Map[AirlineModifierPropertyType.Value, Long] = internalProperties } @@ -420,4 +442,9 @@ object DelegateBoostAirlineModifier { object AirlineModifierType extends Enumeration { type AirlineModifierType = Value val NERFED, DELEGATE_BOOST = Value +} + +object AirlineModifierPropertyType extends Enumeration { + type AirlineModifierPropertyType = Value + val STRENGTH, DURATION = Value } \ No newline at end of file diff --git a/airline-data/src/main/scala/com/patson/model/AirlineBase.scala b/airline-data/src/main/scala/com/patson/model/AirlineBase.scala index b7ba576c7..ff0a722fc 100644 --- a/airline-data/src/main/scala/com/patson/model/AirlineBase.scala +++ b/airline-data/src/main/scala/com/patson/model/AirlineBase.scala @@ -84,7 +84,7 @@ case class AirlineBase(airline : Airline, airport : Airport, countryCode : Strin Title.PRIVILEGED_AIRLINE } val title = CountryAirlineTitle.getTitle(airport.countryCode, airline) - if (title.title.id <= Title.ESTABLISHED_AIRLINE.id) { //lower id means higher title + if (title.title.id <= requiredTitle.id) { //lower id means higher title Right(requiredTitle) } else { Left(requiredTitle) diff --git a/airline-data/src/main/scala/com/patson/model/Alliance.scala b/airline-data/src/main/scala/com/patson/model/Alliance.scala index 2032191d7..6b9aeae34 100644 --- a/airline-data/src/main/scala/com/patson/model/Alliance.scala +++ b/airline-data/src/main/scala/com/patson/model/Alliance.scala @@ -1,8 +1,10 @@ package com.patson.model import scala.collection.mutable.ListBuffer -import com.patson.data.CountrySource -import com.patson.util.ChampionUtil +import com.patson.data.{AllianceSource, CountrySource, CycleSource} +import com.patson.model.AllianceEvent.{BOOT_ALLIANCE, LEAVE_ALLIANCE, PROMOTE_LEADER, REJECT_ALLIANCE} +import com.patson.util.{AirlineCache, ChampionUtil} +import play.api.libs.json.Json import scala.math.BigDecimal.RoundingMode @@ -14,6 +16,33 @@ case class Alliance(name: String, creationCycle: Int, members: List[AllianceMemb AllianceStatus.ESTABLISHED } } + + def removeMember(member : AllianceMember, removeSelf : Boolean) : Unit = { + import AllianceRole._ + val targetAirlineId = member.airline.id + val currentCycle = CycleSource.loadCycle() + if (members.find(_.airline.id == targetAirlineId).isDefined) { //just to play safe + AllianceSource.deleteAllianceMember(targetAirlineId) + if (removeSelf) { + AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = name, airline = member.airline, event = LEAVE_ALLIANCE, cycle = currentCycle)) + if (member.role == LEADER) { + members.filter(_.role == CO_LEADER).sortBy(_.joinedCycle).headOption match { + case Some(coLeader) => + AllianceSource.saveAllianceMember(coLeader.copy(role = AllianceRole.LEADER)) + AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = name, airline = coLeader.airline, event = PROMOTE_LEADER, cycle = currentCycle)) + case None => AllianceSource.deleteAlliance(id) //no co-leader, remove the alliance + } + } + } else { + if (member.role == APPLICANT) { + AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = name, airline = member.airline, event = REJECT_ALLIANCE, cycle = currentCycle)) + } else { + AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = name, airline = member.airline, event = BOOT_ALLIANCE, cycle = currentCycle)) + } + } + } + + } } object AllianceStatus extends Enumeration { @@ -25,14 +54,19 @@ case class AllianceMember(allianceId: Int, airline: Airline, role: AllianceRole. object AllianceRole extends Enumeration { type AllianceRole = Value - val LEADER, FOUNDING_MEMBER, MEMBER, APPLICANT = Value + val LEADER, CO_LEADER, MEMBER, APPLICANT = Value + + val isAdmin : AllianceRole => Boolean = (role : AllianceRole) => role match { + case LEADER | CO_LEADER => true + case _ => false + } } case class AllianceHistory(allianceName: String, airline: Airline, event: AllianceEvent.Value, cycle: Int, var id: Int = 0) object AllianceEvent extends Enumeration { type AllianceEvent = Value - val FOUND_ALLIANCE, APPLY_ALLIANCE, JOIN_ALLIANCE, REJECT_ALLIANCE, LEAVE_ALLIANCE, BOOT_ALLIANCE, PROMOTE_LEADER = Value + val FOUND_ALLIANCE, APPLY_ALLIANCE, JOIN_ALLIANCE, REJECT_ALLIANCE, LEAVE_ALLIANCE, BOOT_ALLIANCE, PROMOTE_LEADER, PROMOTE_CO_LEADER, DEMOTE = Value } object Alliance { diff --git a/airline-data/src/main/scala/com/patson/model/FlightPreference.scala b/airline-data/src/main/scala/com/patson/model/FlightPreference.scala index 32ba5e245..bcb55a364 100644 --- a/airline-data/src/main/scala/com/patson/model/FlightPreference.scala +++ b/airline-data/src/main/scala/com/patson/model/FlightPreference.scala @@ -170,7 +170,7 @@ abstract class FlightPreference(homeAirport : Airport) { val frequencyRatioDelta = Math.max(-1, (frequencyThreshold - link.frequencyByClass(linkClass)).toDouble / frequencyThreshold) * frequencySensitivity val flightDurationRatioDelta = - if (flightDurationSensitivity == 0 || link.transportType == TransportType.SHUTTLE) { + if (flightDurationSensitivity == 0 || link.transportType != TransportType.FLIGHT) { 0 } else { val flightDurationThreshold = Computation.computeStandardFlightDuration(link.distance) diff --git a/airline-data/src/main/scala/com/patson/model/Shuttle.scala b/airline-data/src/main/scala/com/patson/model/GenericTransit.scala similarity index 55% rename from airline-data/src/main/scala/com/patson/model/Shuttle.scala rename to airline-data/src/main/scala/com/patson/model/GenericTransit.scala index 9b707bfb7..8618e36dc 100644 --- a/airline-data/src/main/scala/com/patson/model/Shuttle.scala +++ b/airline-data/src/main/scala/com/patson/model/GenericTransit.scala @@ -1,31 +1,33 @@ package com.patson.model -case class Shuttle(from : Airport, to : Airport, airline: Airline, distance : Int, var capacity: LinkClassValues, var id : Int = 0) extends Transport { - override val transportType : TransportType.Value = TransportType.SHUTTLE +case class GenericTransit(from : Airport, to : Airport, distance : Int, var capacity: LinkClassValues, var id : Int = 0) extends Transport { + override val transportType : TransportType.Value = TransportType.GENERIC_TRANSIT override val duration = (distance.toDouble / 30 * 60).toInt - override var frequency : Int = if (capacity.total == 0) 0 else 24 * 7 - override def computedQuality() : Int = Shuttle.QUALITY //constant quality + override var frequency : Int = 24 * 7 + override def computedQuality() : Int = GenericTransit.QUALITY //constant quality override val price : LinkClassValues = LinkClassValues.getInstance() //override val price : LinkClassValues = LinkClassValues.getInstance() //has to have some hidden price? otherwise it will be too strong? override val flightType : FlightType.Value = FlightType.SHORT_HAUL_DOMESTIC - override val cost = LinkClassValues.getInstance(economy = Pricing.computeStandardPrice(distance, FlightType.SHORT_HAUL_DOMESTIC, ECONOMY)) * 0.8 //hidden cost of taking shuttle + override val cost = LinkClassValues.getInstance(economy = Pricing.computeStandardPrice(distance, FlightType.SHORT_HAUL_DOMESTIC, ECONOMY)) * 0.25 //hidden cost of general transit - val upkeep = capacity.total * Shuttle.UPKEEP_PER_CAPACITY + val upkeep = 0 override var minorDelayCount : Int = 0 override var majorDelayCount : Int = 0 override var cancellationCount : Int = 0 override def toString() = { - s"Shuttle $id; ${airline.name}; ${from.city}(${from.iata}) => ${to.city}(${to.iata}); distance $distance" + s"Generic transit $id; ${from.city}(${from.iata}) => ${to.city}(${to.iata}); distance $distance" } override val frequencyByClass = (_ : LinkClass) => frequency + override val airline : Airline = GenericTransit.TRANSIT_PROVIDER } -object Shuttle { - val QUALITY = 40 - val UPKEEP_PER_CAPACITY = 10 - val SPEED = 30 //30km/hr +object GenericTransit { + val QUALITY = 35 + val TRANSIT_PROVIDER = Airline.fromId(0).copy(name = "Local Transit") } + + diff --git a/airline-data/src/main/scala/com/patson/model/Link.scala b/airline-data/src/main/scala/com/patson/model/Link.scala index 7f999245a..8e2539ac9 100644 --- a/airline-data/src/main/scala/com/patson/model/Link.scala +++ b/airline-data/src/main/scala/com/patson/model/Link.scala @@ -272,7 +272,7 @@ case class LinkConsideration(link : Transport, def to : Airport = if (inverted) link.from else link.to override def toString() : String = { - s"Consideration [${linkClass} - $link cost: $cost]" + s"Consideration [${linkClass} - Flight $id; ${link.airline.name}; ${from.city}(${from.iata}) => ${to.city}(${to.iata}); capacity ${link.capacity}; price ${link.price}; cost: $cost]" } diff --git a/airline-data/src/main/scala/com/patson/model/Log.scala b/airline-data/src/main/scala/com/patson/model/Log.scala index 432c7388e..e4a2f604d 100644 --- a/airline-data/src/main/scala/com/patson/model/Log.scala +++ b/airline-data/src/main/scala/com/patson/model/Log.scala @@ -1,14 +1,15 @@ package com.patson.model -case class Log(airline : Airline, message : String, category : LogCategory.Value, severity : LogSeverity.Value, cycle : Int) +case class Log(airline : Airline, message : String, category : LogCategory.Value, severity : LogSeverity.Value, cycle : Int, properties : Map[String, String] = Map.empty) object LogCategory extends Enumeration { type LogCategory = Value - val LINK, NEGOTIATION = Value + val LINK, NEGOTIATION, AIRPORT_RANK_CHANGE = Value val getDescription : LogCategory.Value => String = { case LINK => "Flight Route" case NEGOTIATION => "Negotiation" + case AIRPORT_RANK_CHANGE => "Airport Rank Change" } } diff --git a/airline-data/src/main/scala/com/patson/model/ShuttleService.scala b/airline-data/src/main/scala/com/patson/model/ShuttleService.scala deleted file mode 100644 index 31182d243..000000000 --- a/airline-data/src/main/scala/com/patson/model/ShuttleService.scala +++ /dev/null @@ -1,27 +0,0 @@ -package com.patson.model - -case class ShuttleService(airline : Airline, allianceId : Option[Int], airport : Airport, name : String = "", level : Int, foundedCycle : Int) { - val getValue = level * 25000000 - val getCapacity = level * 2000 - - val basicUpkeep : Long = { - (10000 + airport.income) * 0.3 * level - }.toLong -} - -object ShuttleService { - def getBaseScaleRequirement(level : Int) = { - if (level == 3) { - 11 - } else if (level == 2) { - 8 - } else { - 5 - } - } - - val COVERAGE_RANGE = 50 -} - - - diff --git a/airline-data/src/main/scala/com/patson/model/Transport.scala b/airline-data/src/main/scala/com/patson/model/Transport.scala index 904ccfc6a..9084e3858 100644 --- a/airline-data/src/main/scala/com/patson/model/Transport.scala +++ b/airline-data/src/main/scala/com/patson/model/Transport.scala @@ -104,7 +104,7 @@ abstract class Transport extends IdObject{ object TransportType extends Enumeration { type TransportType = Value - val FLIGHT, SHUTTLE = Value + val FLIGHT, GENERIC_TRANSIT = Value } diff --git a/airline-data/src/main/scala/com/patson/model/christmas/SantaClausAward.scala b/airline-data/src/main/scala/com/patson/model/christmas/SantaClausAward.scala index 79467a7e5..ffcfb0d22 100644 --- a/airline-data/src/main/scala/com/patson/model/christmas/SantaClausAward.scala +++ b/airline-data/src/main/scala/com/patson/model/christmas/SantaClausAward.scala @@ -91,10 +91,12 @@ class ReputationAward(santaClausInfo: SantaClausInfo) extends SantaClausAward(sa class DelegateAward(santaClausInfo: SantaClausInfo) extends SantaClausAward(santaClausInfo) { override val getType: SantaClausAwardType.Value = SantaClausAwardType.DELEGATE override def applyAward(): Unit = { - AirlineSource.saveAirlineModifier(santaClausInfo.airline.id, DelegateBoostAirlineModifier(CycleSource.loadCycle())) + AirlineSource.saveAirlineModifier(santaClausInfo.airline.id, DelegateBoostAirlineModifier(amount, duration, CycleSource.loadCycle())) } + val amount = 3 + val duration = 52 - override val description: String = s"Santa Claus dispatches his elves to help you! ${DelegateBoostAirlineModifier.AMOUNT} extra delegates for ${DelegateBoostAirlineModifier.DURATION} weeks" + override val description: String = s"Santa Claus dispatches his elves to help you! $amount extra delegates for $duration weeks" } diff --git a/airline-data/src/main/scala/com/patson/model/event/Event.scala b/airline-data/src/main/scala/com/patson/model/event/Event.scala index fe06e2e37..3f9e401aa 100644 --- a/airline-data/src/main/scala/com/patson/model/event/Event.scala +++ b/airline-data/src/main/scala/com/patson/model/event/Event.scala @@ -1,6 +1,6 @@ package com.patson.model.event -import com.patson.data.{AirlineSource, AirportSource, CountrySource, EventSource} +import com.patson.data.{AirlineSource, AirportSource, CountrySource, CycleSource, EventSource} import com.patson.model._ import scala.collection.mutable @@ -123,7 +123,7 @@ object Olympics { } val voteRewardOptions : List[EventReward] = List(OlympicsVoteCashReward(), OlympicsVoteLoyaltyReward()) - val passengerRewardOptions : List[EventReward] = List(OlympicsPassengerCashReward(), OlympicsPassengerLoyaltyReward(), OlympicsPassengerReputationReward()) + val passengerRewardOptions : List[EventReward] = List(OlympicsPassengerCashReward(), OlympicsPassengerLoyaltyReward(), OlympicsPassengerReputationReward(), OlympicsPassengerDelegateReward()) val getDemandMultiplier = (weekOfYear: Int) => { if (weekOfYear < Olympics.WEEKS_PER_YEAR - Olympics.GAMES_DURATION * 12) { @@ -173,7 +173,7 @@ object RewardCategory extends Enumeration { object RewardOption extends Enumeration { type RewardOption = Value - val CASH, LOYALTY, REPUTATION = Value + val CASH, LOYALTY, REPUTATION, DELEGATE = Value } abstract class EventReward(val eventType : EventType.Value, val rewardCategory : RewardCategory.Value, val rewardOption : RewardOption.Value) { @@ -186,6 +186,7 @@ abstract class EventReward(val eventType : EventType.Value, val rewardCategory : protected def applyReward(event: Event, airline : Airline) val description : String + def redeemDescription(eventId: Int, airlineId: Int) = description } object EventReward { @@ -221,15 +222,20 @@ case class OlympicsPassengerCashReward() extends EventReward(EventType.OLYMPICS, val SCORE_MULTIPLIER = 500 + def computeReward(eventId: Int, airlineId : Int) = { + val stats: Map[Int, BigDecimal] = EventSource.loadOlympicsAirlineStats (eventId, airlineId).toMap + val totalScore = stats.view.values.sum + Math.max((totalScore * 500).toLong, MIN_CASH_REWARD) + } override def applyReward(event: Event, airline : Airline) = { - val stats: Map[Int, BigDecimal] = EventSource.loadOlympicsAirlineStats (event.id, airline.id).toMap - val totalScore = stats.view.values.sum - val reward = Math.max((totalScore * 500).toLong, MIN_CASH_REWARD) + val reward = computeReward(event.id, airline.id) AirlineSource.adjustAirlineBalance(airline.id, reward) } override val description: String = "$20,000,000 or $500 * score (whichever is higher) cash reward" + override def redeemDescription(eventId: Int, airlineId : Int) = s"$$${java.text.NumberFormat.getIntegerInstance.format(computeReward(eventId, airlineId))} cash reward" + } case class OlympicsPassengerLoyaltyReward() extends EventReward(EventType.OLYMPICS, RewardCategory.OLYMPICS_PASSENGER, RewardOption.LOYALTY) { @@ -253,3 +259,30 @@ case class OlympicsPassengerReputationReward() extends EventReward(EventType.OLY override val description: String = s"+$REPUTATION_BONUS reputation boost (one time only, reputation will eventually drop back to normal level)" } +case class OlympicsPassengerDelegateReward() extends EventReward(EventType.OLYMPICS, RewardCategory.OLYMPICS_PASSENGER, RewardOption.DELEGATE) { + val BASE_BONUS = 2 + val MAX_BONUS = 6 + val DURATION = 52 * 4 + override def applyReward(event: Event, airline : Airline) = { + val cycle = CycleSource.loadCycle() + val bonus = computeReward(event.id, airline.id) + AirlineSource.saveAirlineModifier(airline.id, DelegateBoostAirlineModifier(bonus, DURATION, cycle)) + } + + def computeReward(eventId: Int, airlineId : Int) = { + val stats: Map[Int, BigDecimal] = EventSource.loadOlympicsAirlineStats (eventId, airlineId).toMap + val totalScore = stats.view.values.sum + EventSource.loadOlympicsAirlineGoal(eventId, airlineId) match { + case Some(goal) => + val overachieverRatio = Math.min(1.0, totalScore.toDouble / goal / 4) //at 400% then it claim 1.0 overachiever ratio + val extraBonus = ((MAX_BONUS - BASE_BONUS) * overachieverRatio).toInt //could be 0 + BASE_BONUS + extraBonus + case None => 0 + } + + } + + override val description: String = s"+$BASE_BONUS to $MAX_BONUS delegates for $DURATION weeks" + override def redeemDescription(eventId: Int, airlineId : Int) = s"${computeReward(eventId, airlineId)} extra delegates for $DURATION weeks" +} + diff --git a/airline-data/src/main/scala/com/patson/patch/Issue485ReplaceGenericTransitPatcher.scala b/airline-data/src/main/scala/com/patson/patch/Issue485ReplaceGenericTransitPatcher.scala new file mode 100644 index 000000000..310bab689 --- /dev/null +++ b/airline-data/src/main/scala/com/patson/patch/Issue485ReplaceGenericTransitPatcher.scala @@ -0,0 +1,57 @@ +package com.patson.patch + +import com.mchange.v2.c3p0.ComboPooledDataSource +import com.patson.data.Constants.{AIRLINE_INFO_TABLE, DATABASE_CONNECTION, DATABASE_PASSWORD, DATABASE_USER, DB_DRIVER} +import com.patson.data.Meta +import com.patson.init.GenericTransitGenerator + +import scala.collection.mutable.ListBuffer + + +object Issue485ReplaceGenericTransitPatcher extends App { + mainFlow + + + def mainFlow() { + compensateShuttleService() + GenericTransitGenerator.generateGenericTransit(4000, 50) + } + + def compensateShuttleService() = { + Class.forName(DB_DRIVER) + val dataSource = new ComboPooledDataSource() + dataSource.setUser(DATABASE_USER) + dataSource.setPassword(DATABASE_PASSWORD) + dataSource.setJdbcUrl(DATABASE_CONNECTION) + dataSource.setMaxPoolSize(100) + + val connection = dataSource.getConnection() + val shuttles = ListBuffer[(Int, Int, Int)]() //(airport, airline, levels) + try { + val statement = connection.prepareStatement("SELECT * FROM shuttle_service") + val result = statement.executeQuery() + while (result.next()) { + shuttles.append((result.getInt("airport"), result.getInt("airline"), result.getInt("level"))) + } + statement.close() + + connection.setAutoCommit(false) + + shuttles.foreach { case(airportId, airlineId, level) => + val patchStatement = connection.prepareStatement("UPDATE " + AIRLINE_INFO_TABLE + " SET balance = balance + ? WHERE airline = ?") + patchStatement.setInt(1, level * 25000000) + patchStatement.setInt(2, airlineId) + patchStatement.executeUpdate() + patchStatement.close + } + connection.commit() + + val purgeStatement = connection.prepareStatement("DELETE FROM shuttle_service") + purgeStatement.executeUpdate() + purgeStatement.close() + } finally { + connection.close + } + } + +} \ No newline at end of file diff --git a/airline-data/src/main/scala/com/patson/patch/Issue498_spec_patcher.scala b/airline-data/src/main/scala/com/patson/patch/Issue498_spec_patcher.scala new file mode 100644 index 000000000..e06e788ef --- /dev/null +++ b/airline-data/src/main/scala/com/patson/patch/Issue498_spec_patcher.scala @@ -0,0 +1,34 @@ +package com.patson.patch + +import com.patson.data.{AirlineSource, AirportSource} +import com.patson.init.actorSystem + +import scala.concurrent.Await +import scala.concurrent.duration.Duration + + +object Issue498_spec_patcher extends App { + mainFlow + + def mainFlow() { + patchInvalidSpecs() + + Await.result(actorSystem.terminate(), Duration.Inf) + } + + + def patchInvalidSpecs() = { + AirlineSource.loadAirlineBasesByCriteria(List.empty).sortBy(_.airline.id).foreach { base => + val specs = AirportSource.loadAirportBaseSpecializations(base.airport.id, base.airline.id) + val (updatingSpecs, purgingSpecs) = specs.partition(_.scaleRequirement <= base.scale) //remove spec that no longer able to support + + if (!purgingSpecs.isEmpty) { + AirportSource.updateAirportBaseSpecializations(base.airport.id, base.airline.id, updatingSpecs) + + purgingSpecs.foreach(_.unapply(base.airline, base.airport)) + + println(s"${base.airline} (${base.airport.displayText}) - purged specs $purgingSpecs") + } + } + } +} \ No newline at end of file diff --git a/airline-data/src/main/scala/com/patson/util/ChampionUtil.scala b/airline-data/src/main/scala/com/patson/util/ChampionUtil.scala index 002386348..5effbcdfb 100644 --- a/airline-data/src/main/scala/com/patson/util/ChampionUtil.scala +++ b/airline-data/src/main/scala/com/patson/util/ChampionUtil.scala @@ -102,14 +102,14 @@ object ChampionUtil { boost * reputationBoostTop10(ranking) } - def updateAirportChampionInfo(loyalists: List[Loyalist]) = { - val result = computeAirportChampionInfo(loyalists) - AirportSource.updateChampionInfo(result) - result - } +// def updateAirportChampionInfo(loyalists: List[Loyalist]) = { +// val result = computeAirportChampionInfo(loyalists) +// AirportSource.updateChampionInfo(result) +// result +// } - private[this] def computeAirportChampionInfo(loyalists: List[Loyalist]) = { + def computeAirportChampionInfo(loyalists: List[Loyalist]) = { val result = ListBuffer[AirportChampionInfo]() // val loyalists = airportIdFilter match { diff --git a/airline-data/src/test/scala/com/patson/GenericTransitSimulationSpec.scala b/airline-data/src/test/scala/com/patson/GenericTransitSimulationSpec.scala new file mode 100644 index 000000000..c266fcd7a --- /dev/null +++ b/airline-data/src/test/scala/com/patson/GenericTransitSimulationSpec.scala @@ -0,0 +1,430 @@ +package com.patson + +import akka.actor.ActorSystem +import akka.testkit.{ImplicitSender, TestKit} +import com.patson.model.FlightType._ +import com.patson.model._ +import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} + +import java.util.Collections +import scala.collection.mutable +import scala.collection.mutable.Set +import scala.jdk.CollectionConverters._ + +class GenericTransitSimulationSpec(_system: ActorSystem) extends TestKit(_system) with ImplicitSender + with WordSpecLike with Matchers with BeforeAndAfterAll { + + def this() = this(ActorSystem("MySpec")) + + override def afterAll { + TestKit.shutdownActorSystem(system) + } + + val testAirline1 = Airline("airline 1", id = 1) + val testAirline2 = Airline("airline 2", id = 2) + val fromAirport = Airport.fromId(1).copy(power = 40000, population = 1, name = "F0") //income 40k . mid income country + val airlineAppeal = AirlineAppeal(0, 100) + fromAirport.initAirlineAppeals(Map(testAirline1.id -> airlineAppeal, testAirline2.id -> airlineAppeal)) + fromAirport.initLounges(List.empty) + val toAirportsList = List( + Airport("", "", "T0", 0, 30, "", "", "", 1, 0, 0, 0, id = 2), + Airport("", "", "T1", 0, 60, "", "", "", 1, 0, 0, 0, id = 3), + Airport("", "", "T2", 0, 90, "", "", "", 1, 0, 0, 0, id = 4)) + + + toAirportsList.foreach { airport => + airport.initAirlineAppeals(Map(testAirline1.id -> airlineAppeal, testAirline2.id -> airlineAppeal)) + airport.initLounges(List.empty) + } + val toAirports = Set(toAirportsList : _*) + + val allAirportIds = Set[Int]() + allAirportIds ++= toAirports.map { _.id } + allAirportIds += fromAirport.id + val LOOP_COUNT = 10000 + + +// val airline1Link = Link(fromAirport, toAirport, testAirline1, 100, 10000, 10000, 0, 600, 1) +// val airline2Link = Link(fromAirport, toAirport, testAirline2, 100, 10000, 10000, 0, 600, 1) + val passengerGroup = PassengerGroup(fromAirport, SimplePreference(homeAirport = fromAirport, priceSensitivity = 1, preferredLinkClass = ECONOMY), PassengerType.BUSINESS) + //def findShortestRoute(from : Airport, toAirports : Set[Airport], allVertices: Set[Airport], linksWithCost : List[LinkWithCost], maxHop : Int) : Map[Airport, Route] = { + "Find shortest route".must { + "find a route if there's generic transit offered".in { + var links = List(LinkConsideration.getExplicit(GenericTransit(fromAirport, toAirportsList(0), 50, LinkClassValues.getInstance(10000), id = 1), 100, ECONOMY, false), + LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), + LinkConsideration.getExplicit(Link(toAirportsList(1), toAirportsList(2), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 3), 100, ECONOMY, false)) + + var routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Collections.emptyMap[Int, Int](), 3) + routes.isDefinedAt(toAirportsList(2)).shouldBe(true) + var route = routes.get(toAirportsList(2)).get + route.links.size.shouldBe(3) + assert(route.links.map(_.id).equals(links.map(_.id))) + + links = List( + LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 1), 100, ECONOMY, false), + LinkConsideration.getExplicit(GenericTransit(toAirportsList(0), toAirportsList(1), 50, LinkClassValues.getInstance(10000), id = 2), 100, ECONOMY, false), + LinkConsideration.getExplicit(Link(toAirportsList(1), toAirportsList(2), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 3), 100, ECONOMY, false)) + + routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Collections.emptyMap[Int, Int](), 3) + routes.isDefinedAt(toAirportsList(2)).shouldBe(true) + route = routes.get(toAirportsList(2)).get + route.links.size.shouldBe(3) + assert(route.links.map(_.id).equals(links.map(_.id))) + + links = List( + LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 1), 100, ECONOMY, false), + LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), + LinkConsideration.getExplicit(GenericTransit(toAirportsList(1), toAirportsList(2), 50, LinkClassValues.getInstance(10000), id = 3), 100, ECONOMY, false)) + + routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Collections.emptyMap[Int, Int](), 3) + routes.isDefinedAt(toAirportsList(2)).shouldBe(true) + route = routes.get(toAirportsList(2)).get + route.links.size.shouldBe(3) + assert(route.links.map(_.id).equals(links.map(_.id))) + + } + + + "direct flight more preferable than generic transit".in { + val links = List(LinkConsideration.getExplicit(GenericTransit(fromAirport, toAirportsList(0), 50, LinkClassValues.getInstance(10000), id = 1), 100, ECONOMY, false), + LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), + LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 3), 100, ECONOMY, false)) + + val routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Collections.emptyMap[Int, Int](), 3) + routes.isDefinedAt(toAirportsList(1)).shouldBe(true) + val route = routes.get(toAirportsList(1)).get + route.links.size.shouldBe(1) //should take the direct flight only + } + "use direct route even though it's more expensive as connection flight is not frequent enough".in { + val cheapLinks = List(LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, duration = 200, frequency = 1, SHORT_HAUL_DOMESTIC, id = 1), 400, ECONOMY, false), + LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, duration = 200, frequency = 1, SHORT_HAUL_DOMESTIC, id = 2), 400, ECONOMY, false), + LinkConsideration.getExplicit(Link(toAirportsList(1), toAirportsList(2), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, duration = 200, frequency = 1, SHORT_HAUL_DOMESTIC, id = 3), 400, ECONOMY, false)) + val expensiveLink = LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(2), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, duration = 600, frequency = 1, SHORT_HAUL_DOMESTIC, id = 4), 1400, ECONOMY, false) + val allLinks = expensiveLink :: cheapLinks + val routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, allLinks.asJava, Collections.emptyMap[Int, Int](), 3) + routes.isDefinedAt(toAirportsList(2)).shouldBe(true) + val route = routes.get(toAirportsList(2)).get + route.links.size.shouldBe(1) + route.links.equals(expensiveLink) + } + } + "findAllRoutes".must { + "find routes if there are valid links".in { + val links = List(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100, 100, 100), 10000, LinkClassValues.getInstance(10000, 10000, 10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 1), + GenericTransit(toAirportsList(0), toAirportsList(1), 50, LinkClassValues.getInstance(10000, 0, 0), id = 2), + Link(toAirportsList(1), toAirportsList(2), testAirline1, LinkClassValues.getInstance(100, 100, 100), 10000, LinkClassValues.getInstance(10000, 10000, 10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 3)) + + val economyPassengerGroup = PassengerGroup(fromAirport, AppealPreference(fromAirport, ECONOMY, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val businessPassengerGroup = PassengerGroup(fromAirport, AppealPreference(fromAirport, BUSINESS, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val firstPassengerGroup = PassengerGroup(fromAirport, AppealPreference(fromAirport, FIRST, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + + val toAirports = Set[Airport]() + toAirports ++= toAirportsList + val result : Map[PassengerGroup, Map[Airport, Route]] = PassengerSimulation.findAllRoutes(Map(economyPassengerGroup -> toAirports, businessPassengerGroup -> toAirports, firstPassengerGroup -> toAirports), links, allAirportIds) + + result.isDefinedAt(economyPassengerGroup).shouldBe(true) + toAirports.foreach { toAirport => + result(economyPassengerGroup).isDefinedAt(toAirport).shouldBe(true) + } + result.isDefinedAt(businessPassengerGroup).shouldBe(true) + toAirports.foreach { toAirport => + result(businessPassengerGroup).isDefinedAt(toAirport).shouldBe(true) + } + result.isDefinedAt(firstPassengerGroup).shouldBe(true) + toAirports.foreach { toAirport => + result(firstPassengerGroup).isDefinedAt(toAirport).shouldBe(true) + } + } + + "prefer direct flights but a few might still go for generic transit (from SFO to LAX , Transit LAX -> LGB, Flight SFO -> LAX, SFO -> LGB)".in { + val sfo = fromAirport.copy(iata = "SFO", name = "SFO", latitude = 37.61899948120117, longitude = -122.375, id = 1) + val sjc = fromAirport.copy(iata = "SJC", name = "SJC", latitude = 37.362598, longitude = -121.929001, id = 2) + val lax = fromAirport.copy(iata = "LAX", name = "LAX", latitude = 33.942501, longitude = -118.407997, id = 3) + val lgb = fromAirport.copy(iata = "LGB", name = "LGB", latitude = 33.817699, longitude = -118.152, id = 4) //long beach + sfo.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + sjc.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + lax.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + lgb.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + + val sjcToSfo = GenericTransit(sjc, sfo, Computation.calculateDistance(sfo, sjc), LinkClassValues.getInstance(economy = 10000), id = 1) + val sfoToLax = { + val fromAirport = sfo + val toAirport = lax + val distance = Computation.calculateDistance(fromAirport, toAirport) + val duration = Computation.computeStandardFlightDuration(distance) + val linkType = Computation.getFlightType(fromAirport, toAirport, distance) + val suggestedPrice = Pricing.computeStandardPriceForAllClass(distance, fromAirport, toAirport) + val newLink = Link(fromAirport, toAirport, testAirline1, price = suggestedPrice, distance = distance, LinkClassValues.getInstance(10000, 10000, 10000), rawQuality = 0, duration, frequency = Link.HIGH_FREQUENCY_THRESHOLD, linkType, id = 2) + newLink.setQuality(50) + newLink + } + val sfoToLgb = { + val fromAirport = sfo + val toAirport = lgb + val distance = Computation.calculateDistance(fromAirport, toAirport) + val duration = Computation.computeStandardFlightDuration(distance) + val linkType = Computation.getFlightType(fromAirport, toAirport, distance) + val suggestedPrice = Pricing.computeStandardPriceForAllClass(distance, fromAirport, toAirport) + val newLink = Link(fromAirport, toAirport, testAirline1, price = suggestedPrice, distance = distance, LinkClassValues.getInstance(10000, 10000, 10000), rawQuality = 0, duration, frequency = Link.HIGH_FREQUENCY_THRESHOLD, linkType, id = 3) + newLink.setQuality(50) + newLink + } + + val laxToLgb = GenericTransit(lax, lgb, Computation.calculateDistance(lax, lgb), LinkClassValues.getInstance(economy = 10000), id = 4) + + val links = List(sjcToSfo, sfoToLax, sfoToLgb, laxToLgb) + val economyPassengerGroup = PassengerGroup(sfo, AppealPreference(sfo, ECONOMY, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val businessPassengerGroup = PassengerGroup(sfo, AppealPreference(sfo, BUSINESS, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val firstPassengerGroup = PassengerGroup(sfo, AppealPreference(sfo, FIRST, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val toAirports = Set(lax) + val paxDirectCount = mutable.HashMap(economyPassengerGroup -> 0, businessPassengerGroup -> 0, firstPassengerGroup -> 0) + val paxTransitCount = mutable.HashMap(economyPassengerGroup -> 0, businessPassengerGroup -> 0, firstPassengerGroup -> 0) + for (i <- 0 until 1000) { + DemandGenerator.getFlightPreferencePoolOnAirport(sfo).pool.foreach { + case(preferredLinkClass, flightPreferences) => { + flightPreferences.filter(!isLoungePreference(_)).foreach { flightPreference => + val result = PassengerSimulation.findAllRoutes(Map(economyPassengerGroup -> toAirports, businessPassengerGroup -> toAirports, firstPassengerGroup -> toAirports), links, allAirportIds) + result.foreach { + case (paxGroup, routesByAirport) => + //println(preferredLinkClass + " " + flightPreferences) + if (routesByAirport(lax).links.length == 1) { + paxDirectCount.put(paxGroup, paxDirectCount(paxGroup) + 1) + } else if (routesByAirport(lax).links.length == 2) { + paxTransitCount.put(paxGroup, paxTransitCount(paxGroup) + 1) + } else { + println(s"Unexpected route: ${routesByAirport(lax)}") + } + } + } + } + } + } + println(s"paxDirectCount $paxDirectCount") + println(s"paxTransitCount $paxTransitCount") + assert(paxTransitCount(economyPassengerGroup).toDouble / paxDirectCount(economyPassengerGroup) < 0.2) + assert(paxTransitCount(economyPassengerGroup).toDouble / paxDirectCount(economyPassengerGroup) > 0.01) + assert(paxTransitCount(businessPassengerGroup).toDouble / paxDirectCount(businessPassengerGroup) < 0.2) + assert(paxTransitCount(businessPassengerGroup).toDouble / paxDirectCount(businessPassengerGroup) > 0.01) + assert(paxTransitCount(firstPassengerGroup).toDouble / paxDirectCount(firstPassengerGroup) < 0.2) + assert(paxTransitCount(firstPassengerGroup).toDouble / paxDirectCount(firstPassengerGroup) > 0.005) + + } + + "prefer direct flights but a few might still go for generic transit (from SFO to LAX , Transit SFO -> SJC, Flight SFO -> LAX, SJC -> LAX".in { + val sfo = fromAirport.copy(iata = "SFO", name = "SFO", latitude = 37.61899948120117, longitude = -122.375, id = 1) + val sjc = fromAirport.copy(iata = "SJC", name = "SJC", latitude = 37.362598, longitude = -121.929001, id = 2) + val lax = fromAirport.copy(iata = "LAX", name = "LAX", latitude = 33.942501, longitude = -118.407997, id = 3) + val lgb = fromAirport.copy(iata = "LGB", name = "LGB", latitude = 33.817699, longitude = -118.152, id = 4) //long beach + sfo.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + sjc.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + lax.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + lgb.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + + val sjcToSfo = GenericTransit(sjc, sfo, Computation.calculateDistance(sfo, sjc), LinkClassValues.getInstance(economy = 10000), id = 1) + val sfoToLax = { + val fromAirport = sfo + val toAirport = lax + val distance = Computation.calculateDistance(fromAirport, toAirport) + val duration = Computation.computeStandardFlightDuration(distance) + val linkType = Computation.getFlightType(fromAirport, toAirport, distance) + val suggestedPrice = Pricing.computeStandardPriceForAllClass(distance, fromAirport, toAirport) + val newLink = Link(fromAirport, toAirport, testAirline1, price = suggestedPrice, distance = distance, LinkClassValues.getInstance(10000, 10000, 10000), rawQuality = 0, duration, frequency = Link.HIGH_FREQUENCY_THRESHOLD, linkType, id = 2) + newLink.setQuality(50) + newLink + } + val sjcToLax = { + val fromAirport = sjc + val toAirport = lax + val distance = Computation.calculateDistance(fromAirport, toAirport) + val duration = Computation.computeStandardFlightDuration(distance) + val linkType = Computation.getFlightType(fromAirport, toAirport, distance) + val suggestedPrice = Pricing.computeStandardPriceForAllClass(distance, fromAirport, toAirport) + val newLink = Link(fromAirport, toAirport, testAirline1, price = suggestedPrice, distance = distance, LinkClassValues.getInstance(10000, 10000, 10000), rawQuality = 0, duration, frequency = Link.HIGH_FREQUENCY_THRESHOLD, linkType, id = 3) + newLink.setQuality(50) + newLink + } + + val laxToLgb = GenericTransit(lax, lgb, Computation.calculateDistance(lax, lgb), LinkClassValues.getInstance(economy = 10000), id = 4) + + val links = List(sjcToSfo, sfoToLax, sjcToLax, laxToLgb) + val economyPassengerGroup = PassengerGroup(sfo, AppealPreference(sfo, ECONOMY, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val businessPassengerGroup = PassengerGroup(sfo, AppealPreference(sfo, BUSINESS, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val firstPassengerGroup = PassengerGroup(sfo, AppealPreference(sfo, FIRST, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val toAirport = lax + val toAirports = Set(toAirport) + val paxDirectCount = mutable.HashMap(economyPassengerGroup -> 0, businessPassengerGroup -> 0, firstPassengerGroup -> 0) + val paxTransitCount = mutable.HashMap(economyPassengerGroup -> 0, businessPassengerGroup -> 0, firstPassengerGroup -> 0) + for (i <- 0 until 1000) { + DemandGenerator.getFlightPreferencePoolOnAirport(sfo).pool.foreach { + case(preferredLinkClass, flightPreferences) => { + flightPreferences.filter(!isLoungePreference(_)).foreach { flightPreference => + val result = PassengerSimulation.findAllRoutes(Map(economyPassengerGroup -> toAirports, businessPassengerGroup -> toAirports, firstPassengerGroup -> toAirports), links, allAirportIds) + result.foreach { + case (paxGroup, routesByAirport) => + //println(preferredLinkClass + " " + flightPreferences) + if (routesByAirport(toAirport).links.length == 1) { + paxDirectCount.put(paxGroup, paxDirectCount(paxGroup) + 1) + } else if (routesByAirport(toAirport).links.length == 2) { + paxTransitCount.put(paxGroup, paxTransitCount(paxGroup) + 1) + } else { + println(s"Unexpected route: ${routesByAirport(toAirport)}") + } + } + } + } + } + } + println(s"paxDirectCount $paxDirectCount") + println(s"paxTransitCount $paxTransitCount") + assert(paxTransitCount(economyPassengerGroup).toDouble / paxDirectCount(economyPassengerGroup) < 0.2) + assert(paxTransitCount(economyPassengerGroup).toDouble / paxDirectCount(economyPassengerGroup) > 0.01) + assert(paxTransitCount(businessPassengerGroup).toDouble / paxDirectCount(businessPassengerGroup) < 0.2) + assert(paxTransitCount(businessPassengerGroup).toDouble / paxDirectCount(businessPassengerGroup) > 0.01) + assert(paxTransitCount(firstPassengerGroup).toDouble / paxDirectCount(firstPassengerGroup) < 0.2) + assert(paxTransitCount(firstPassengerGroup).toDouble / paxDirectCount(firstPassengerGroup) > 0.005) + + } + + "super cheap flight with generic transit can steal many pax but not all (from SFO to LAX , Transit SFO -> SJC, Flight SFO -> LAX, SJC -> LAX".in { + val sfo = fromAirport.copy(iata = "SFO", name = "SFO", latitude = 37.61899948120117, longitude = -122.375, id = 1) + val sjc = fromAirport.copy(iata = "SJC", name = "SJC", latitude = 37.362598, longitude = -121.929001, id = 2) + val lax = fromAirport.copy(iata = "LAX", name = "LAX", latitude = 33.942501, longitude = -118.407997, id = 3) + val lgb = fromAirport.copy(iata = "LGB", name = "LGB", latitude = 33.817699, longitude = -118.152, id = 4) //long beach + sfo.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + sjc.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + lax.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + lgb.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 100))) + + val sjcToSfo = GenericTransit(sjc, sfo, Computation.calculateDistance(sfo, sjc), LinkClassValues.getInstance(economy = 10000), id = 1) + val sfoToLax = { + val fromAirport = sfo + val toAirport = lax + val distance = Computation.calculateDistance(fromAirport, toAirport) + val duration = Computation.computeStandardFlightDuration(distance) + val linkType = Computation.getFlightType(fromAirport, toAirport, distance) + val suggestedPrice = Pricing.computeStandardPriceForAllClass(distance, fromAirport, toAirport) + val newLink = Link(fromAirport, toAirport, testAirline1, price = suggestedPrice, distance = distance, LinkClassValues.getInstance(10000, 10000, 10000), rawQuality = 0, duration, frequency = Link.HIGH_FREQUENCY_THRESHOLD, linkType, id = 2) + newLink.setQuality(50) + newLink + } + val sjcToLax = { + val fromAirport = sjc + val toAirport = lax + val distance = Computation.calculateDistance(fromAirport, toAirport) + val duration = Computation.computeStandardFlightDuration(distance) + val linkType = Computation.getFlightType(fromAirport, toAirport, distance) + val cheapPrice = Pricing.computeStandardPriceForAllClass(distance, fromAirport, toAirport) * 0.7 + val newLink = Link(fromAirport, toAirport, testAirline1, price = cheapPrice, distance = distance, LinkClassValues.getInstance(10000, 10000, 10000), rawQuality = 0, duration, frequency = Link.HIGH_FREQUENCY_THRESHOLD, linkType, id = 3) + newLink.setQuality(50) + newLink + } + + val laxToLgb = GenericTransit(lax, lgb, Computation.calculateDistance(lax, lgb), LinkClassValues.getInstance(economy = 10000), id = 4) + + val links = List(sjcToSfo, sfoToLax, sjcToLax, laxToLgb) + val economyPassengerGroup = PassengerGroup(sfo, AppealPreference(sfo, ECONOMY, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val businessPassengerGroup = PassengerGroup(sfo, AppealPreference(sfo, BUSINESS, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val firstPassengerGroup = PassengerGroup(sfo, AppealPreference(sfo, FIRST, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) + val toAirport = lax + val toAirports = Set(toAirport) + val paxDirectCount = mutable.HashMap(economyPassengerGroup -> 0, businessPassengerGroup -> 0, firstPassengerGroup -> 0) + val paxTransitCount = mutable.HashMap(economyPassengerGroup -> 0, businessPassengerGroup -> 0, firstPassengerGroup -> 0) + for (i <- 0 until 1000) { + DemandGenerator.getFlightPreferencePoolOnAirport(sfo).pool.foreach { + case(preferredLinkClass, flightPreferences) => { + flightPreferences.filter(!isLoungePreference(_)).foreach { flightPreference => + val result = PassengerSimulation.findAllRoutes(Map(economyPassengerGroup -> toAirports, businessPassengerGroup -> toAirports, firstPassengerGroup -> toAirports), links, allAirportIds) + result.foreach { + case (paxGroup, routesByAirport) => + //println(preferredLinkClass + " " + flightPreferences) + if (routesByAirport(toAirport).links.length == 1) { + paxDirectCount.put(paxGroup, paxDirectCount(paxGroup) + 1) + } else if (routesByAirport(toAirport).links.length == 2) { + paxTransitCount.put(paxGroup, paxTransitCount(paxGroup) + 1) + } else { + println(s"Unexpected route: ${routesByAirport(toAirport)}") + } + } + } + } + } + } + println(s"paxDirectCount $paxDirectCount") + println(s"paxTransitCount $paxTransitCount") + assert(paxTransitCount(economyPassengerGroup).toDouble / paxDirectCount(economyPassengerGroup) > 2) + assert(paxTransitCount(economyPassengerGroup).toDouble / paxDirectCount(economyPassengerGroup) < 5) + assert(paxTransitCount(businessPassengerGroup).toDouble / paxDirectCount(businessPassengerGroup) > 0.7) + assert(paxTransitCount(businessPassengerGroup).toDouble / paxDirectCount(businessPassengerGroup) < 1.5) + assert(paxTransitCount(firstPassengerGroup).toDouble / paxDirectCount(firstPassengerGroup) > 0.2) + assert(paxTransitCount(firstPassengerGroup).toDouble / paxDirectCount(firstPassengerGroup) < 0.5) + + } + + } + + +// val airport1 = Airport("", "", "", 0, 0, "", "", "", 0, 0, 0, 0, 0) +// val airport2 = Airport("", "", "", 0, 100, "", "", "", 0, 0, 0, 0, 0) +// val airport3 = Airport("", "", "", 0, 200, "", "", "", 0, 0, 0, 0, 0) + + def isLoungePreference(preference: FlightPreference) : Boolean = { + preference.isInstanceOf[AppealPreference] && preference.asInstanceOf[AppealPreference].loungeLevelRequired > 0 + } + + + + "IsLinkAffordable".must { + "accept some routes with neutral conditions and generic transit as first and last link".in { + val sfo = fromAirport.copy(iata = "SFO", name = "SFO", latitude = 37.61899948120117, longitude = -122.375, id = 1) + val sjc = fromAirport.copy(iata = "SJC", name = "SJC", latitude = 37.362598, longitude = -121.929001, id = 2) + val lax = fromAirport.copy(iata = "LAX", name = "LAX", latitude = 33.942501, longitude = -118.407997, id = 3) + val lgb = fromAirport.copy(iata = "LGB", name = "LGB", latitude = 33.817699, longitude = -118.152, id = 4) //long beach + sfo.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 0))) + sjc.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 0))) + lax.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 0))) + lgb.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 0))) + + val sjcToSfo = GenericTransit(sjc, sfo, Computation.calculateDistance(sfo, sjc), LinkClassValues.getInstance(economy = 1000)) + val sfoToLax = { + val fromAirport = sfo + val toAirport = lax + val distance = Computation.calculateDistance(fromAirport, toAirport) + val duration = Computation.computeStandardFlightDuration(distance) + val linkType = Computation.getFlightType(fromAirport, toAirport, distance) + val suggestedPrice = Pricing.computeStandardPriceForAllClass(distance, fromAirport, toAirport) + val newLink = Link(fromAirport, toAirport, testAirline1, price = suggestedPrice, distance = distance, LinkClassValues.getInstance(10000, 10000, 10000), rawQuality = 0, duration, frequency = Link.HIGH_FREQUENCY_THRESHOLD, linkType) + newLink.setQuality(50) + newLink + } + val laxToLgb = GenericTransit(lax, lgb, Computation.calculateDistance(lax, lgb), LinkClassValues.getInstance(economy = 1000)) + + val links = List(sjcToSfo, sfoToLax, laxToLgb) + + //hmm kinda mix in flight preference here...might not be a good thing... loop 10000 times so result is more consistent + var totalRoutes = 0 + var totalAcceptedRoutes = 0; + for (i <- 0 until LOOP_COUNT) { + DemandGenerator.getFlightPreferencePoolOnAirport(sfo).pool.foreach { + case(preferredLinkClass, flightPreference) => { + flightPreference.filter(!isLoungePreference(_)).foreach { flightPreference => + val linkConsiderations = links.map { link => + val costBreakdown = flightPreference.computeCostBreakdown(link, preferredLinkClass) + LinkConsideration.getExplicit(link, costBreakdown.cost, preferredLinkClass, false) + } + + val route = Route(linkConsiderations, linkConsiderations.foldLeft(0.0) { _ + _.cost }) + + PassengerSimulation.getRouteRejection(route, sfo, lgb, preferredLinkClass) match { + case None => totalAcceptedRoutes += 1 + case Some(rejection) => //println(s"$flightPreference $rejection on $route") + } + totalRoutes += 1 + } + } + } + } + assert(totalAcceptedRoutes.toDouble / totalRoutes < 0.7) + assert(totalAcceptedRoutes.toDouble / totalRoutes > 0.5) + } + } +} diff --git a/airline-data/src/test/scala/com/patson/ShuttleSimulationSpec.scala b/airline-data/src/test/scala/com/patson/ShuttleSimulationSpec.scala deleted file mode 100644 index 82e0bc37a..000000000 --- a/airline-data/src/test/scala/com/patson/ShuttleSimulationSpec.scala +++ /dev/null @@ -1,263 +0,0 @@ -package com.patson - -import akka.actor.ActorSystem -import akka.testkit.{ImplicitSender, TestKit} -import com.patson.model.FlightType._ -import com.patson.model._ -import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} - -import java.util.Collections -import scala.collection.mutable.Set -import scala.jdk.CollectionConverters._ - -class ShuttleSimulationSpec(_system: ActorSystem) extends TestKit(_system) with ImplicitSender - with WordSpecLike with Matchers with BeforeAndAfterAll { - - def this() = this(ActorSystem("MySpec")) - - override def afterAll { - TestKit.shutdownActorSystem(system) - } - - val testAirline1 = Airline("airline 1", id = 1) - val testAirline2 = Airline("airline 2", id = 2) - val fromAirport = Airport.fromId(1).copy(power = 40000, population = 1, name = "F0") //income 40k . mid income country - val airlineAppeal = AirlineAppeal(0, 100) - fromAirport.initAirlineAppeals(Map(testAirline1.id -> airlineAppeal, testAirline2.id -> airlineAppeal)) - fromAirport.initLounges(List.empty) - val toAirportsList = List( - Airport("", "", "T0", 0, 30, "", "", "", 1, 0, 0, 0, id = 2), - Airport("", "", "T1", 0, 60, "", "", "", 1, 0, 0, 0, id = 3), - Airport("", "", "T2", 0, 90, "", "", "", 1, 0, 0, 0, id = 4)) - - - toAirportsList.foreach { airport => - airport.initAirlineAppeals(Map(testAirline1.id -> airlineAppeal, testAirline2.id -> airlineAppeal)) - airport.initLounges(List.empty) - } - val toAirports = Set(toAirportsList : _*) - - val allAirportIds = Set[Int]() - allAirportIds ++= toAirports.map { _.id } - allAirportIds += fromAirport.id - val LOOP_COUNT = 10000 - - -// val airline1Link = Link(fromAirport, toAirport, testAirline1, 100, 10000, 10000, 0, 600, 1) -// val airline2Link = Link(fromAirport, toAirport, testAirline2, 100, 10000, 10000, 0, 600, 1) - val passengerGroup = PassengerGroup(fromAirport, SimplePreference(homeAirport = fromAirport, priceSensitivity = 1, preferredLinkClass = ECONOMY), PassengerType.BUSINESS) - //def findShortestRoute(from : Airport, toAirports : Set[Airport], allVertices: Set[Airport], linksWithCost : List[LinkWithCost], maxHop : Int) : Map[Airport, Route] = { - "Find shortest route".must { - "find a route if there's shuttle offered by same airline".in { - var links = List(LinkConsideration.getExplicit(Shuttle(fromAirport, toAirportsList(0), testAirline1, 50, LinkClassValues.getInstance(10000), id = 1), 100, ECONOMY, false), - LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), - LinkConsideration.getExplicit(Link(toAirportsList(1), toAirportsList(2), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 3), 100, ECONOMY, false)) - - var routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Collections.emptyMap[Int, Int](), 3) - routes.isDefinedAt(toAirportsList(2)).shouldBe(true) - var route = routes.get(toAirportsList(2)).get - route.links.size.shouldBe(3) - route.links.equals(links) - - links = List( - LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 1), 100, ECONOMY, false), - LinkConsideration.getExplicit(Shuttle(toAirportsList(0), toAirportsList(1), testAirline1, 50, LinkClassValues.getInstance(10000), id = 2), 100, ECONOMY, false), - LinkConsideration.getExplicit(Link(toAirportsList(1), toAirportsList(2), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 3), 100, ECONOMY, false)) - - routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Collections.emptyMap[Int, Int](), 3) - routes.isDefinedAt(toAirportsList(2)).shouldBe(true) - route = routes.get(toAirportsList(2)).get - route.links.size.shouldBe(3) - route.links.equals(links) - - links = List( - LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 1), 100, ECONOMY, false), - LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), - LinkConsideration.getExplicit(Shuttle(toAirportsList(1), toAirportsList(2), testAirline1, 50, LinkClassValues.getInstance(10000), id = 3), 100, ECONOMY, false)) - - routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Collections.emptyMap[Int, Int](), 3) - routes.isDefinedAt(toAirportsList(2)).shouldBe(true) - route = routes.get(toAirportsList(2)).get - route.links.size.shouldBe(3) - route.links.equals(links) - - } - "find a route if there's a shuttle offered by alliance member".in { - val links = List( - LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 1), 100, ECONOMY, false), - LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), - LinkConsideration.getExplicit(Shuttle(toAirportsList(1), toAirportsList(2), testAirline2, 50, LinkClassValues.getInstance(10000), id = 3), 100, ECONOMY, false)) //shuttle by alliance member - - val routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Map(testAirline1.id -> 1, testAirline2.id -> 1).asJava, 3) - routes.isDefinedAt(toAirportsList(2)).shouldBe(true) - val route = routes.get(toAirportsList(2)).get - route.links.size.shouldBe(3) - route.links.equals(links) - } - "find no route if there's a shuttle offered by different airline".in { - val links = List( - LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 1), 100, ECONOMY, false), - LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), - LinkConsideration.getExplicit(Shuttle(toAirportsList(1), toAirportsList(2), testAirline2, 50, LinkClassValues.getInstance(10000), id = 3), 100, ECONOMY, false)) //shuttle by different airline - - val routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Collections.emptyMap[Int, Int](), 3) - routes.isDefinedAt(toAirportsList(2)).shouldBe(false) - } - "find no route if there's a shuttle offered by different alliance (last link shuttle)".in { - val links = List( - LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 1), 100, ECONOMY, false), - LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), - LinkConsideration.getExplicit(Shuttle(toAirportsList(1), toAirportsList(2), testAirline2, 50, LinkClassValues.getInstance(10000), id = 3), 100, ECONOMY, false)) //shuttle by different airline - - val routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Map(testAirline1.id -> 1, testAirline2.id -> 2).asJava, 3) - routes.isDefinedAt(toAirportsList(2)).shouldBe(false) - } - - "find no route if there's a shuttle offered by different alliance (first link shuttle)".in { - val links = List( - LinkConsideration.getExplicit(Shuttle(fromAirport, toAirportsList(0), testAirline2, 50, LinkClassValues.getInstance(10000), id = 1), 100, ECONOMY, false), //shuttle by different airline - LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false)) - - val routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Map(testAirline1.id -> 1, testAirline2.id -> 2).asJava, 3) - routes.isDefinedAt(toAirportsList(1)).shouldBe(false) - } - "find no route if there's a shuttle offered by both airlines but other airline is cheaper (first link shuttle)".in { - val links = List( - LinkConsideration.getExplicit(Shuttle(fromAirport, toAirportsList(0), testAirline1, 50, LinkClassValues.getInstance(10000), id = 1), 100, ECONOMY, false), //shuttle by current airline 1 but more expensive - LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), - LinkConsideration.getExplicit(Shuttle(fromAirport, toAirportsList(0), testAirline2, 50, LinkClassValues.getInstance(10000), id = 3), 50, ECONOMY, false)) //shuttle by different airline 2 but cheaper - - - val routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Map(testAirline1.id -> 1, testAirline2.id -> 2).asJava, 3) - routes.isDefinedAt(toAirportsList(1)).shouldBe(false) - } - "find a route if there's a shuttle offered by both airlines but current airline is cheaper (first link shuttle)".in { - val links = List( - LinkConsideration.getExplicit(Shuttle(fromAirport, toAirportsList(0), testAirline2, 50, LinkClassValues.getInstance(10000), id = 1), 100, ECONOMY, false), //shuttle by other airline 2 but more expensive - LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), - LinkConsideration.getExplicit(Shuttle(fromAirport, toAirportsList(0), testAirline1, 50, LinkClassValues.getInstance(10000), id = 3), 50, ECONOMY, false)) //shuttle by current airline 1 but cheaper - - - val routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Map(testAirline1.id -> 1, testAirline2.id -> 2).asJava, 3) - routes.isDefinedAt(toAirportsList(1)).shouldBe(true) - } - - "direct flight more preferable than shuttle".in { - val links = List(LinkConsideration.getExplicit(Shuttle(fromAirport, toAirportsList(0), testAirline1, 50, LinkClassValues.getInstance(10000), id = 1), 100, ECONOMY, false), - LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 2), 100, ECONOMY, false), - LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 3), 100, ECONOMY, false)) - - val routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, links.asJava, Collections.emptyMap[Int, Int](), 3) - routes.isDefinedAt(toAirportsList(1)).shouldBe(true) - val route = routes.get(toAirportsList(1)).get - route.links.size.shouldBe(1) //should take the direct flight only - } - "use direct route even though it's more expensive as connection flight is not frequent enough".in { - val cheapLinks = List(LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, duration = 200, frequency = 1, SHORT_HAUL_DOMESTIC, id = 1), 400, ECONOMY, false), - LinkConsideration.getExplicit(Link(toAirportsList(0), toAirportsList(1), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, duration = 200, frequency = 1, SHORT_HAUL_DOMESTIC, id = 2), 400, ECONOMY, false), - LinkConsideration.getExplicit(Link(toAirportsList(1), toAirportsList(2), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, duration = 200, frequency = 1, SHORT_HAUL_DOMESTIC, id = 3), 400, ECONOMY, false)) - val expensiveLink = LinkConsideration.getExplicit(Link(fromAirport, toAirportsList(2), testAirline1, LinkClassValues.getInstance(100), 10000, LinkClassValues.getInstance(10000), 0, duration = 600, frequency = 1, SHORT_HAUL_DOMESTIC, id = 4), 1400, ECONOMY, false) - val allLinks = expensiveLink :: cheapLinks - val routes = PassengerSimulation.findShortestRoute(passengerGroup, toAirports, allAirportIds, allLinks.asJava, Collections.emptyMap[Int, Int](), 3) - routes.isDefinedAt(toAirportsList(2)).shouldBe(true) - val route = routes.get(toAirportsList(2)).get - route.links.size.shouldBe(1) - route.links.equals(expensiveLink) - } - } - "findAllRoutes".must { - "find routes if there are valid links".in { - val links = List(Link(fromAirport, toAirportsList(0), testAirline1, LinkClassValues.getInstance(100, 100, 100), 10000, LinkClassValues.getInstance(10000, 10000, 10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 1), - Shuttle(toAirportsList(0), toAirportsList(1), testAirline1, 50, LinkClassValues.getInstance(10000, 0, 0), id = 2), - Link(toAirportsList(1), toAirportsList(2), testAirline1, LinkClassValues.getInstance(100, 100, 100), 10000, LinkClassValues.getInstance(10000, 10000, 10000), 0, 600, 1, SHORT_HAUL_DOMESTIC, id = 3)) - - val economyPassengerGroup = PassengerGroup(fromAirport, AppealPreference(fromAirport, ECONOMY, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) - val businessPassengerGroup = PassengerGroup(fromAirport, AppealPreference(fromAirport, BUSINESS, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) - val firstPassengerGroup = PassengerGroup(fromAirport, AppealPreference(fromAirport, FIRST, loungeLevelRequired = 0, loyaltyRatio = 1, 0), PassengerType.BUSINESS) - - val toAirports = Set[Airport]() - toAirports ++= toAirportsList - val result : Map[PassengerGroup, Map[Airport, Route]] = PassengerSimulation.findAllRoutes(Map(economyPassengerGroup -> toAirports, businessPassengerGroup -> toAirports, firstPassengerGroup -> toAirports), links, allAirportIds) - - result.isDefinedAt(economyPassengerGroup).shouldBe(true) - toAirports.foreach { toAirport => - result(economyPassengerGroup).isDefinedAt(toAirport).shouldBe(true) - } - result.isDefinedAt(businessPassengerGroup).shouldBe(true) - toAirports.foreach { toAirport => - result(businessPassengerGroup).isDefinedAt(toAirport).shouldBe(true) - } - result.isDefinedAt(firstPassengerGroup).shouldBe(true) - toAirports.foreach { toAirport => - result(firstPassengerGroup).isDefinedAt(toAirport).shouldBe(true) - } - } - - } - - -// val airport1 = Airport("", "", "", 0, 0, "", "", "", 0, 0, 0, 0, 0) -// val airport2 = Airport("", "", "", 0, 100, "", "", "", 0, 0, 0, 0, 0) -// val airport3 = Airport("", "", "", 0, 200, "", "", "", 0, 0, 0, 0, 0) - - def isLoungePreference(preference: FlightPreference) : Boolean = { - preference.isInstanceOf[AppealPreference] && preference.asInstanceOf[AppealPreference].loungeLevelRequired > 0 - } - - - - "IsLinkAffordable".must { - "accept some routes with neutral conditions and shuttle as first and last link".in { - val sfo = fromAirport.copy(iata = "SFO", name = "SFO", latitude = 37.61899948120117, longitude = -122.375, id = 1) - val sjc = fromAirport.copy(iata = "SJC", name = "SJC", latitude = 37.362598, longitude = -121.929001, id = 2) - val lax = fromAirport.copy(iata = "LAX", name = "LAX", latitude = 33.942501, longitude = -118.407997, id = 3) - val lgb = fromAirport.copy(iata = "LGB", name = "LGB", latitude = 33.817699, longitude = -118.152, id = 4) //long beach - sfo.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 0))) - sjc.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 0))) - lax.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 0))) - lgb.initAirlineAppeals(Map(testAirline1.id -> AirlineAppeal(loyalty = 0, 0))) - - val sjcToSfo = Shuttle(sjc, sfo, testAirline1, Computation.calculateDistance(sfo, sjc), LinkClassValues.getInstance(economy = 1000)) - val sfoToLax = { - val fromAirport = sfo - val toAirport = lax - val distance = Computation.calculateDistance(fromAirport, toAirport) - val duration = Computation.computeStandardFlightDuration(distance) - val linkType = Computation.getFlightType(fromAirport, toAirport, distance) - val suggestedPrice = Pricing.computeStandardPriceForAllClass(distance, fromAirport, toAirport) - val newLink = Link(fromAirport, toAirport, testAirline1, price = suggestedPrice, distance = distance, LinkClassValues.getInstance(10000, 10000, 10000), rawQuality = 0, duration, frequency = Link.HIGH_FREQUENCY_THRESHOLD, linkType) - newLink.setQuality(50) - newLink - } - val laxToLgb = Shuttle(lax, lgb, testAirline1, Computation.calculateDistance(lax, lgb), LinkClassValues.getInstance(economy = 1000)) - - val links = List(sjcToSfo, sfoToLax, laxToLgb) - - //hmm kinda mix in flight preference here...might not be a good thing... loop 10000 times so result is more consistent - var totalRoutes = 0 - var totalAcceptedRoutes = 0; - for (i <- 0 until LOOP_COUNT) { - DemandGenerator.getFlightPreferencePoolOnAirport(sfo).pool.foreach { - case(preferredLinkClass, flightPreference) => { - flightPreference.filter(!isLoungePreference(_)).foreach { flightPreference => - val linkConsiderations = links.map { link => - val costBreakdown = flightPreference.computeCostBreakdown(link, preferredLinkClass) - LinkConsideration.getExplicit(link, costBreakdown.cost, preferredLinkClass, false) - } - - val route = Route(linkConsiderations, linkConsiderations.foldLeft(0.0) { _ + _.cost }) - - PassengerSimulation.getRouteRejection(route, sfo, lgb, preferredLinkClass) match { - case None => totalAcceptedRoutes += 1 - case Some(rejection) => //println(s"$flightPreference $rejection on $route") - } - totalRoutes += 1 - } - } - } - } - assert(totalAcceptedRoutes.toDouble / totalRoutes > 0.3) - assert(totalAcceptedRoutes.toDouble / totalRoutes < 0.5) - } - } -} diff --git a/airline-data/start.sh b/airline-data/start.sh index 26d56550c..632ad3850 100755 --- a/airline-data/start.sh +++ b/airline-data/start.sh @@ -1 +1 @@ -java -Dactivator.home=/home/ubuntu/git/airline/airline-data -Xms1024m -Xmx1024m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=256m -jar /home/ubuntu/git/airline/airline-data/activator-launch-1.3.6.jar "runMain com.patson.MainSimulation" > airline-data.log & +java -Dactivator.home=/home/ubuntu/git/airline/airline-data -Dlog4j2.formatMsgNoLookups=true -Xms1024m -Xmx1024m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=256m -jar /home/ubuntu/git/airline/airline-data/activator-launch-1.3.6.jar "runMain com.patson.MainSimulation" > airline-data.log & diff --git a/airline-web/.gitignore b/airline-web/.gitignore index 479dd0079..2dfcf643b 100644 --- a/airline-web/.gitignore +++ b/airline-web/.gitignore @@ -12,3 +12,5 @@ target /.cache-tests /.sbtserver /conf/gmail-credentials.json +/conf/google-oauth-credentials.json +/google-tokens/ diff --git a/airline-web/README b/airline-web/README index 65a261e10..78b58919e 100644 --- a/airline-web/README +++ b/airline-web/README @@ -2,6 +2,9 @@ To properly setup emails need to download the gmail credentials json and authori Then copy the credentials json and put it to `conf` folder with name `gmail-credentials.json` and copy the `tokens` to the app directory +Similarly the banner uses google oauth. Need to authorize locally by downloading the credentals json as `google-oauth-credentials.json` to conf folder. +Then invoke the main method of `GooglePhotoUtil`, watch console, it should give u a URL, click on it and it should bring you to authorize it. Afterwards copy the `google-tokens` to the app directory + For google map api (for image search from server) set `google.apiKey` in `application.conf` diff --git a/airline-web/app/controllers/AdminApplication.scala b/airline-web/app/controllers/AdminApplication.scala index 6423ec8d1..b30090d0a 100644 --- a/airline-web/app/controllers/AdminApplication.scala +++ b/airline-web/app/controllers/AdminApplication.scala @@ -44,7 +44,7 @@ class AdminApplication @Inject()(cc: ControllerComponents) extends AbstractContr def addAirlineModifier(modifier : AirlineModifierType.Value, airlines : List[Airline]) = { val currentCycle = CycleSource.loadCycle() airlines.foreach { airline => - AirlineSource.saveAirlineModifier(airline.id, AirlineModifier.fromValues(modifier, currentCycle, None)) + AirlineSource.saveAirlineModifier(airline.id, AirlineModifier.fromValues(modifier, currentCycle, None, Map.empty)) } } diff --git a/airline-web/app/controllers/AirlineApplication.scala b/airline-web/app/controllers/AirlineApplication.scala index 07695d724..a3568208f 100644 --- a/airline-web/app/controllers/AirlineApplication.scala +++ b/airline-web/app/controllers/AirlineApplication.scala @@ -72,7 +72,7 @@ class AirlineApplication @Inject()(cc: ControllerComponents) extends AbstractCon result = result + ("slogan" -> JsString(airline.slogan.getOrElse(""))) alliance.foreach { alliance => - result = result + ("allianceName" -> JsString(alliance.name)) + result = result + ("allianceName" -> JsString(alliance.name)) + ("allianceId" -> JsNumber(alliance.id)) } if (!airlineModifiers.isEmpty) { @@ -172,6 +172,12 @@ class AirlineApplication @Inject()(cc: ControllerComponents) extends AbstractCon val reputationBreakdowns = AirlineSource.loadReputationBreakdowns(airlineId) airlineJson = airlineJson + ("baseAirports"-> Json.toJson(bases)) + ("reputationBreakdowns" -> Json.toJson(reputationBreakdowns)) + ("delegatesInfo" -> Json.toJson(airline.getDelegateInfo())) + AllianceSource.loadAllianceMemberByAirline(airline).foreach { allianceMembership => + airlineJson = airlineJson + + ("allianceId" -> JsNumber(allianceMembership.allianceId)) + + ("allianceRole" -> JsString(allianceMembership.role.toString)) + + ("isAllianceAdmin" -> JsBoolean(AllianceRole.isAdmin(allianceMembership.role))) + } if (extendedInfo) { val links = LinkSource.loadFlightLinksByAirlineId(airlineId) @@ -386,66 +392,7 @@ class AirlineApplication @Inject()(cc: ControllerComponents) extends AbstractCon } } - def getShuttleConsideration(airline : Airline, inputFacility : AirportFacility) : Consideration[ShuttleService] = { - val airport = inputFacility.airport - - var (cost, newShuttleService, levelChange) : (Long, ShuttleService, Int) = AirlineSource.loadShuttleServiceByAirlineAndAirport(inputFacility.airline.id, inputFacility.airport.id) match { - case Some(shuttleService) => - val newShuttleService = shuttleService.copy(level = inputFacility.level) - val cost = newShuttleService.getValue - shuttleService.getValue - (cost, newShuttleService, inputFacility.level - shuttleService.level) - case None => - val newShuttleService = ShuttleService(airline, airline.getAllianceId, airport, name = inputFacility.name, level = 1, CycleSource.loadCycle()) - if (inputFacility.level != 1) { - return Consideration(0, newShuttleService, Some(s"Cannot build shuttle service of level ${inputFacility.level}")) - } - val cost = newShuttleService.getValue - (cost, newShuttleService, inputFacility.level) - } - - if (newShuttleService.level < 0) { - return Consideration(0, newShuttleService.copy(level = 0), Some("Cannot downgrade further")) - } else if (newShuttleService.level > Lounge.MAX_LEVEL) { - return Consideration(0, newShuttleService.copy(level = Lounge.MAX_LEVEL), Some("Cannot upgrade further")) - } - - //check base requirement - AirlineSource.loadAirlineBaseByAirlineAndAirport(airline.id, airport.id) match { - case Some(base) => - if (base.scale < ShuttleService.getBaseScaleRequirement(newShuttleService.level)) { - return Consideration(cost, newShuttleService, Some("Require base at scale " + ShuttleService.getBaseScaleRequirement(newShuttleService.level) + " to build level " + newShuttleService.level + " Shuttle Service")) - } - case None => return Consideration(0, newShuttleService, Some("Cannot build Shuttle Service without a base in this airport")) - } - - if (cost < 0) { //no refund - cost = 0 - } - - if (cost > 0 && cost > airline.getBalance) { - return Consideration(cost, newShuttleService, Some("Not enough cash to build/upgrade the shuttle service")) - } - - //check whether there is a base - if (airline.getBases().find( _.airport.id == airport.id).isEmpty) { - return Consideration(cost, newShuttleService, Some("Cannot build shuttle service without a base")) - } - if (levelChange < 0) { - val existingShuttleCapacity = LinkSource.loadLinksByCriteria(List(("airline", airline.id), ("from_airport", inputFacility.airport.id), ("transport_type", TransportType.SHUTTLE.id))).map(_.capacity.total).sum - if (newShuttleService.getCapacity < existingShuttleCapacity) { - return Consideration(0, newShuttleService, Some(s"Cannot downgrade as current shuttle service utilize $existingShuttleCapacity capacity while the downgraded service will only have ${newShuttleService.getCapacity}")) - } - } - - if (Computation.getDomesticAirportWithinRange(airport, ShuttleService.COVERAGE_RANGE).filter(_.id != airport.id).isEmpty) { - return Consideration(0, newShuttleService, Some(s"No airports within ${ShuttleService.COVERAGE_RANGE} km")) - } - - return Consideration(cost, newShuttleService) - - } - def getDowngradeRejection(base : AirlineBase) : Option[String] = { if (base.scale == 1) { //cannot downgrade any further return Some("Cannot downgrade this base any further") @@ -625,6 +572,11 @@ class AirlineApplication @Inject()(cc: ControllerComponents) extends AbstractCon case None => val updateBase = base.copy(scale = base.scale - 1) AirlineSource.saveAirlineBase(updateBase) + + val (updatingSpecs, purgingSpecs) = base.specializations.partition(_.scaleRequirement <= updateBase.scale) //remove spec that no longer able to support + AirportSource.updateAirportBaseSpecializations(airportId, airlineId, updatingSpecs) + purgingSpecs.foreach(_.unapply(request.user, base.airport)) + Ok("Base downgraded") } case None => @@ -658,14 +610,7 @@ class AirlineApplication @Inject()(cc: ControllerComponents) extends AbstractCon } - var shuttleServiceJson : JsObject = AirlineSource.loadShuttleServiceByAirlineAndAirport(airlineId, airportId) match { - case Some(shuttleService) => - Json.toJson(shuttleService).asInstanceOf[JsObject] - case None => - Json.toJson(ShuttleService(airline = airline, allianceId = airline.getAllianceId, airport = airport, name = "", level = 0, foundedCycle = 0)).asInstanceOf[JsObject] - } - - Ok(Json.obj("lounge" -> loungeJson, "shuttleService" -> shuttleServiceJson)) + Ok(Json.obj("lounge" -> loungeJson)) case None => NotFound } @@ -696,24 +641,7 @@ class AirlineApplication @Inject()(cc: ControllerComponents) extends AbstractCon } Ok(result) - case FacilityType.SHUTTLE => - var result = Json.obj() - val upgradeConsideration = getShuttleConsideration(airline, inputFacility.copy(level = inputFacility.level + 1)) - - var upgradeJson = Json.obj("cost" -> JsNumber(upgradeConsideration.cost), "upkeep" -> JsNumber(Shuttle.UPKEEP_PER_CAPACITY)) - if (upgradeConsideration.isRejected) { - upgradeJson += ("rejection" -> JsString(upgradeConsideration.rejectionReason)) - } - result = result + ("upgrade" -> upgradeJson) - - val downgradeConsideration = getShuttleConsideration(airline, inputFacility.copy(level = inputFacility.level - 1)) - if (downgradeConsideration.isRejected) { - result = result + ("downgrade" -> Json.obj("rejection" -> JsString(downgradeConsideration.rejectionReason))) - } else { - result = result + ("downgrade" -> Json.obj()) - } - Ok(result) case _ => BadRequest("unknown facility type : " + inputFacility.facilityType) //Ok(Json.obj("lounge" -> Json.toJson(Lounge(airline = airline, allianceId = airline.getAllianceId, airport = airport, level = 0, status = LoungeStatus.INACTIVE, foundedCycle = 0)))) @@ -758,35 +686,6 @@ class AirlineApplication @Inject()(cc: ControllerComponents) extends AbstractCon AirlineSource.deleteLounge(lounge) } - if (consideration.cost > 0) { - AirlineSource.adjustAirlineBalance(request.user.id, -1 * consideration.cost) - AirlineSource.saveCashFlowItem(AirlineCashFlowItem(airlineId, CashFlowType.FACILITY_CONSTRUCTION, -1 * consideration.cost)) - } - Ok(Json.toJson(consideration.newFacility)) - } - case FacilityType.SHUTTLE => - val consideration = getShuttleConsideration(airline, inputFacility) - if (consideration.isRejected) { - BadRequest(consideration.rejectionReason) - } else { - val shuttleService = consideration.newFacility - if (shuttleService.level > 0) { - AirlineSource.saveShuttleService(shuttleService) - //establish shuttles if none is found - if (LinkSource.loadLinksByCriteria(List(("airline", airline.id), ("from_airport", airport.id), ("transport_type", TransportType.SHUTTLE.id))).map(_.asInstanceOf[Shuttle]).isEmpty) { - val shuttles = Computation.getDomesticAirportWithinRange(airport, ShuttleService.COVERAGE_RANGE).filter(_.id != airport.id).map { toAirport => - val distance = Computation.calculateDistance(airport, toAirport) - Shuttle(from = airport, to = toAirport, airline = airline, distance = distance, capacity = LinkClassValues.getInstance()) - } - LinkSource.saveLinks(shuttles) - } - } else{ - AirlineSource.deleteShuttleService(shuttleService) - LinkSource.loadLinksByCriteria(List(("airline", airline.id), ("from_airport", airport.id), ("transport_type", TransportType.SHUTTLE.id))).foreach { link => - LinkSource.deleteLink(link.id) - } - } - if (consideration.cost > 0) { AirlineSource.adjustAirlineBalance(request.user.id, -1 * consideration.cost) AirlineSource.saveCashFlowItem(AirlineCashFlowItem(airlineId, CashFlowType.FACILITY_CONSTRUCTION, -1 * consideration.cost)) diff --git a/airline-web/app/controllers/AllianceApplication.scala b/airline-web/app/controllers/AllianceApplication.scala index 2232103b7..f3107f1cb 100644 --- a/airline-web/app/controllers/AllianceApplication.scala +++ b/airline-web/app/controllers/AllianceApplication.scala @@ -36,10 +36,11 @@ class AllianceApplication @Inject()(cc: ControllerComponents) extends AbstractCo "airlineName" -> JsString(allianceMember.airline.name), "allianceRole" -> JsString(allianceMember.role match { case LEADER => "Leader" - case FOUNDING_MEMBER => "Founding member" + case CO_LEADER => "Co-leader" case MEMBER => "Member" case APPLICANT => "Applicant" }), + "isAdmin" -> JsBoolean(AllianceRole.isAdmin(allianceMember.role)), "allianceId" -> JsNumber(allianceMember.allianceId), "allianceName" -> JsString(AllianceCache.getAlliance(allianceMember.allianceId).get.name))) } @@ -55,13 +56,15 @@ class AllianceApplication @Inject()(cc: ControllerComponents) extends AbstractCo def getHistoryDescription(history : AllianceHistory) : String = { val eventAction = history.event match { - case FOUND_ALLIANCE => "founded alliance" - case APPLY_ALLIANCE => "applied for alliance" - case JOIN_ALLIANCE => "joined alliance" - case REJECT_ALLIANCE => "was rejected by alliance" - case LEAVE_ALLIANCE => "left alliance" - case BOOT_ALLIANCE => "was removed from alliance" - case PROMOTE_LEADER => "was promoted to alliance leader of" + case FOUND_ALLIANCE => "founded" + case APPLY_ALLIANCE => "applied for" + case JOIN_ALLIANCE => "joined" + case REJECT_ALLIANCE => "was rejected by" + case LEAVE_ALLIANCE => "left" + case BOOT_ALLIANCE => "was removed from" + case PROMOTE_LEADER => "was promoted to leader of" + case PROMOTE_CO_LEADER => "was promoted to co-leader of" + case DEMOTE => "was demoted in" } history.airline.name + " " + eventAction + " " + history.allianceName } @@ -150,11 +153,6 @@ class AllianceApplication @Inject()(cc: ControllerComponents) extends AbstractCo var allianceMemberJson = Json.arr() alliance.members.foreach { allianceMember => var thisMemberJson = Json.toJson(allianceMember).asInstanceOf[JsObject] - if (isCurrentMember && allianceMember.role == APPLICANT) { //current airline is within this alliance, get more info about applicant - getApplyRejection(allianceMember.airline, alliance).foreach { - rejection => thisMemberJson = thisMemberJson + ("rejection" -> JsString(rejection)) - } - } allianceMemberJson = allianceMemberJson.append(thisMemberJson) if (allianceMember.role == LEADER) { allianceJson = allianceJson.asInstanceOf[JsObject] + ("leader" -> Json.toJson(allianceMember.airline)) @@ -273,26 +271,102 @@ class AllianceApplication @Inject()(cc: ControllerComponents) extends AbstractCo } } } - + + def getRemoveConsideration(targetMember : AllianceMember, currentMember : AllianceMember) : Either[String, String] = { + if (targetMember.allianceId != currentMember.allianceId) { + Left(s"Airline ${targetMember.airline} does not belong to alliance ${currentMember.allianceId}") + } else if (targetMember.airline.id == currentMember.airline.id) { //remove self + Right(s"Leave this alliance?") + } else if (!AllianceRole.isAdmin(currentMember.role)) { + Left(s"Current airline ${currentMember.airline} cannot remove airline ${targetMember.airline} from alliance as current airline is not admin") + } else if (currentMember.role.id >= targetMember.role.id) { //higher the id, lower the rank. Can only remove people at the lower rank + Left(s"Current airline ${currentMember.airline} of role ${currentMember.role} cannot remove airline ${targetMember.airline} of role ${targetMember.role} from alliance") + } else { + Right(s"Remove ${targetMember.airline.name} from the alliance?") + } + } + + def getPromoteConsideration(targetMember : AllianceMember, currentMember : AllianceMember) : Either[String, String] = { + if (targetMember.allianceId != currentMember.allianceId) { + Left(s"Airline ${targetMember.airline} does not belong to alliance ${currentMember.allianceId}") + } else if (!AllianceRole.isAdmin(currentMember.role)) { + Left(s"Current airline ${currentMember.airline} cannot promote airline ${targetMember.airline} from alliance as current airline is not admin") + } else if (targetMember.role.id > AllianceRole.MEMBER.id) { + Left(s"Cannot promote non-member airline ${targetMember.airline} of role ${targetMember.role}") + } else if (currentMember.role.id >= targetMember.role.id) { //higher the id, lower the rank. Can only promote people at the lower rank + Left(s"Current airline ${currentMember.airline} of role ${currentMember.role} cannot promote airline ${targetMember.airline} of role ${targetMember.role} from alliance") + } else { + if (AllianceRole(targetMember.role.id - 1) == AllianceRole.LEADER) { + Right(s"Promote ${targetMember.airline.name} as the new Alliance Leader? Your airline will be demoted to Co-leader!") + } else { + Right(s"Promote ${targetMember.airline.name} as Alliance Co-Leader?") + } + } + } + + def getDemoteConsideration(targetMember : AllianceMember, currentMember : AllianceMember) : Either[String, String] = { + if (targetMember.allianceId != currentMember.allianceId) { + Left(s"Airline ${targetMember.airline} does not belong to alliance ${currentMember.allianceId}") + } else if (currentMember.airline.id == targetMember.airline.id) { + Left(s"Airline ${targetMember.airline} cannot demote self") + } else if (targetMember.role.id >= AllianceRole.MEMBER.id) { + Left(s"Current airline ${currentMember.airline} of role ${currentMember.role} cannot demote airline ${targetMember.airline} of role ${targetMember.role} any further") + } else if (!AllianceRole.isAdmin(currentMember.role)) { + Left(s"Current airline ${currentMember.airline} cannot demote airline ${targetMember.airline} from alliance as current airline is not admin") + } else if (currentMember.role.id >= targetMember.role.id) { //higher the id, lower the rank. Can only remove people at the lower rank + Left(s"Current airline ${currentMember.airline} of role ${currentMember.role} cannot demote airline ${targetMember.airline} of role ${targetMember.role} in alliance") + } else { + Right(s"Demote ${targetMember.airline.name}?") + } + } + def evaluateAlliance(airlineId : Int, allianceId : Int) = AuthenticatedAirline(airlineId) { implicit request => AllianceCache.getAlliance(allianceId, true) match { case None => NotFound("Alliance with id " + allianceId + " is not found") case Some(alliance) => - var result = Json.obj() + var result = Json.obj() alliance.members.find( _.airline.id == airlineId) match { - case Some(_) => result = result + ("isMember" -> JsBoolean(true)) //already a member + case Some(currentMember) => //already a member + //get member actions + var memberActionsJson = Json.arr() + alliance.members.foreach { targetMember => + var memberJson = Json.obj("airlineId" -> targetMember.airline.id) + getRemoveConsideration(targetMember, currentMember) match { + case Left(rejection) => memberJson = memberJson + ("removeRejection" -> JsString(rejection)) + case Right(prompt) => memberJson = memberJson + ("removePrompt" -> JsString(prompt)) + } + getPromoteConsideration(targetMember, currentMember) match { + case Left(rejection) => memberJson = memberJson + ("promoteRejection" -> JsString(rejection)) + case Right(prompt) => memberJson = memberJson + ("promotePrompt" -> JsString(prompt)) + } + getDemoteConsideration(targetMember, currentMember) match { + case Left(rejection) => memberJson = memberJson + ("demoteRejection" -> JsString(rejection)) + case Right(prompt) => memberJson = memberJson + ("demotePrompt" -> JsString(prompt)) + } + + if (AllianceRole.isAdmin(currentMember.role) && targetMember.role == AllianceRole.APPLICANT) { + getApplyRejection(targetMember.airline, alliance) match { + case Some(rejection) => memberJson = memberJson + ("acceptRejection" -> JsString(rejection)) + case None => memberJson = memberJson + ("acceptPrompt" -> JsString(s"Accept ${targetMember.airline.name} into the alliance?")) + } + + } + + memberActionsJson = memberActionsJson.append(memberJson) + } + result = result + ("isMember" -> JsBoolean(true)) + ("memberActions" -> memberActionsJson) case None => getApplyRejection(request.user, alliance) match { case Some(rejection) => result = result + ("rejection" -> JsString(rejection)) case None => //nothing } - + } - + Ok(result) } } - + def applyForAlliance(airlineId : Int, allianceId : Int) = AuthenticatedAirline(airlineId) { implicit request => AllianceCache.getAlliance(allianceId, true) match { case None => NotFound("Alliance with id " + allianceId + " is not found") @@ -309,52 +383,36 @@ class AllianceApplication @Inject()(cc: ControllerComponents) extends AbstractCo } } } - + def removeFromAlliance(airlineId : Int, targetAirlineId : Int) = AuthenticatedAirline(airlineId) { implicit request => - val currentCycle = CycleSource.loadCycle - AllianceSource.loadAllianceMemberByAirline(request.user) match { - case None => BadRequest("Current airline " + request.user + " cannot remove airline id "+ targetAirlineId + " from alliance as current airline does not belong to any alliance") - case Some(currentAirlineAllianceMember) => - val alliance = AllianceCache.getAlliance(currentAirlineAllianceMember.allianceId, false).get - if (airlineId == targetAirlineId) { //removing itself, ok! - AllianceSource.deleteAllianceMember(targetAirlineId) - AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = alliance.name, airline = request.user, event = LEAVE_ALLIANCE, cycle = currentCycle)) - if (currentAirlineAllianceMember.role == LEADER) { //remove the alliance - AllianceSource.deleteAlliance(currentAirlineAllianceMember.allianceId) - - SearchUtil.removeAlliance(alliance.id) - } - - Ok(Json.obj("removed" -> "alliance")) - } else { //check if current airline is leader and the target airline is within this alliance - if (currentAirlineAllianceMember.role != LEADER) { - BadRequest("Current airline " + request.user + " cannot remove airline id "+ targetAirlineId + " from alliance as current airline is not leader") - } else { - AirlineCache.getAirline(targetAirlineId) match { - case None => NotFound("Airline with id " + targetAirlineId + " not found") - case Some(targetAirline) => - AllianceSource.loadAllianceMemberByAirline(targetAirline) match { - case None => NotFound("Airline " + targetAirline + " does not belong to any alliance!") - case Some(allianceMember) => - if (allianceMember.allianceId != currentAirlineAllianceMember.allianceId) { - BadRequest("Airline " + targetAirline + " does not belong to alliance " + alliance) - } else { //OK ..removing - AllianceSource.deleteAllianceMember(targetAirlineId) - if (allianceMember.role == APPLICANT) { - AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = alliance.name, airline = allianceMember.airline, event = REJECT_ALLIANCE, cycle = currentCycle)) - } else { - AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = alliance.name, airline = allianceMember.airline, event = BOOT_ALLIANCE, cycle = currentCycle)) - } - - Ok(Json.obj("removed" -> "member")) - } - } + AllianceSource.loadAllianceMemberByAirline(request.user) match { + case None => BadRequest("Current airline " + request.user + " cannot remove airline id " + targetAirlineId + " from alliance as current airline does not belong to any alliance") + case Some(currentAirlineAllianceMember) => + val alliance = AllianceCache.getAlliance(currentAirlineAllianceMember.allianceId, false).get + if (airlineId == targetAirlineId) { //removing itself, ok! + alliance.removeMember(currentAirlineAllianceMember, true) + + Ok(Json.obj("removed" -> "alliance")) + } else { //check if current airline has the right permission and the target airline is within this alliance + AirlineCache.getAirline(targetAirlineId) match { + case None => NotFound("Airline with id " + targetAirlineId + " not found") + case Some(targetAirline) => + AllianceSource.loadAllianceMemberByAirline(targetAirline) match { + case None => NotFound("Airline " + targetAirline + " does not belong to any alliance!") + case Some(allianceMember) => + getRemoveConsideration(allianceMember, currentAirlineAllianceMember) match { + case Left(rejection) => BadRequest(rejection) + case Right(_) => //OK ..removing + alliance.removeMember(allianceMember, false) + + Ok(Json.obj("removed" -> "member")) + } } - } - } - } + } + } + } } - + def addToAlliance(airlineId : Int, targetAirlineId : Int) = AuthenticatedAirline(airlineId) { implicit request => val currentCycle = CycleSource.loadCycle @@ -362,8 +420,8 @@ class AllianceApplication @Inject()(cc: ControllerComponents) extends AbstractCo case None => BadRequest("Current airline " + request.user + " cannot add airline id "+ targetAirlineId + " to alliance as current airline does not belong to any alliance") case Some(currentAirlineAllianceMember) => //check if current airline is leader and the target airline has applied to this alliance - if (currentAirlineAllianceMember.role != LEADER) { - BadRequest("Current airline " + request.user + " cannot remove airline id "+ targetAirlineId + " from alliance as current airline is not leader") + if (!AllianceRole.isAdmin(currentAirlineAllianceMember.role)) { + BadRequest("Current airline " + request.user + " cannot accept airline id "+ targetAirlineId + " from alliance as current airline is not leader") } else { val alliance = AllianceCache.getAlliance(currentAirlineAllianceMember.allianceId, false).get AirlineCache.getAirline(targetAirlineId, true) match { @@ -392,35 +450,66 @@ class AllianceApplication @Inject()(cc: ControllerComponents) extends AbstractCo } } } - - def promoteToAllianceLeader(airlineId : Int, targetAirlineId : Int) = AuthenticatedAirline(airlineId) { implicit request => - val currentCycle = CycleSource.loadCycle - AllianceSource.loadAllianceMemberByAirline(request.user) match { - case None => BadRequest("Current airline " + request.user + " cannot add airline id "+ targetAirlineId + " to alliance as current airline does not belong to any alliance") - case Some(currentAirlineAllianceMember) => - //check if current airline is leader and the target airline has applied to this alliance - if (currentAirlineAllianceMember.role != LEADER) { - BadRequest("Current airline " + request.user + " cannot promote airline id "+ targetAirlineId + " from alliance as current airline is not leader") - } else { - val alliance = AllianceCache.getAlliance(currentAirlineAllianceMember.allianceId, false).get - AirlineCache.getAirline(targetAirlineId) match { - case None => NotFound("Airline with id " + targetAirlineId + " not found") - case Some(targetAirline) => - AllianceSource.loadAllianceMemberByAirline(targetAirline) match { - case None => NotFound("Airline " + targetAirline + " does not belong to any alliance!") - case Some(allianceMember) => - if (allianceMember.allianceId != currentAirlineAllianceMember.allianceId) { - BadRequest("Airline " + targetAirline + " does not belong to alliance " + alliance) - } else { //OK ..promoting - AllianceSource.saveAllianceMember(allianceMember.copy(role = LEADER)) - AllianceSource.saveAllianceMember(currentAirlineAllianceMember.copy(role = MEMBER)) - AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = alliance.name, airline = allianceMember.airline, event = PROMOTE_LEADER, cycle = currentCycle)) - - Ok(Json.toJson(allianceMember)) - } - } - } - } + + def promoteMember(airlineId : Int, targetAirlineId : Int) = AuthenticatedAirline(airlineId) { implicit request => + val currentCycle = CycleSource.loadCycle + AllianceSource.loadAllianceMemberByAirline(request.user) match { + case None => BadRequest("Current airline " + request.user + " cannot promote airline id " + targetAirlineId + " to alliance as current airline does not belong to any alliance") + case Some(currentMember) => + val alliance = AllianceCache.getAlliance(currentMember.allianceId, false).get + AirlineCache.getAirline(targetAirlineId) match { + case None => NotFound("Airline with id " + targetAirlineId + " not found") + case Some(targetAirline) => + AllianceSource.loadAllianceMemberByAirline(targetAirline) match { + case None => NotFound("Airline " + targetAirline + " does not belong to any alliance!") + case Some(targetMember) => + getPromoteConsideration(targetMember, currentMember) match { + case Left(rejection) => BadRequest(rejection) + case Right(_) => + //OK ..promoting + val newRole = AllianceRole(targetMember.role.id - 1) + AllianceSource.saveAllianceMember(targetMember.copy(role = newRole)) + + if (newRole == AllianceRole.LEADER && currentMember.role == AllianceRole.LEADER) { //promotion to leader, have to demote current leader then + AllianceSource.saveAllianceMember(currentMember.copy(role = AllianceRole(AllianceRole.LEADER.id + 1))) + AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = alliance.name, airline = targetMember.airline, event = PROMOTE_LEADER, cycle = currentCycle)) + } else { //otherwise assume it is always to co-leader + AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = alliance.name, airline = targetMember.airline, event = PROMOTE_CO_LEADER, cycle = currentCycle)) + } + + Ok(Json.toJson(targetMember)) + } + } + + } + } + } + + def demoteMember(airlineId : Int, targetAirlineId : Int) = AuthenticatedAirline(airlineId) { implicit request => + val currentCycle = CycleSource.loadCycle + AllianceSource.loadAllianceMemberByAirline(request.user) match { + case None => BadRequest("Current airline " + request.user + " cannot demote airline id " + targetAirlineId + " to alliance as current airline does not belong to any alliance") + case Some(currentMember) => + val alliance = AllianceCache.getAlliance(currentMember.allianceId, false).get + AirlineCache.getAirline(targetAirlineId) match { + case None => NotFound("Airline with id " + targetAirlineId + " not found") + case Some(targetAirline) => + AllianceSource.loadAllianceMemberByAirline(targetAirline) match { + case None => NotFound("Airline " + targetAirline + " does not belong to any alliance!") + case Some(targetMember) => + getDemoteConsideration(targetMember, currentMember) match { + case Left(rejection) => BadRequest(rejection) + case Right(_) => + //OK ..demoting + val newRole = AllianceRole(targetMember.role.id + 1) + AllianceSource.saveAllianceMember(targetMember.copy(role = newRole)) + AllianceSource.saveAllianceHistory(AllianceHistory(allianceName = alliance.name, airline = targetMember.airline, event = DEMOTE, cycle = currentCycle)) + + Ok(Json.toJson(targetMember)) + } + } + + } } } diff --git a/airline-web/app/controllers/Application.scala b/airline-web/app/controllers/Application.scala index 6a3295871..caf2518ab 100644 --- a/airline-web/app/controllers/Application.scala +++ b/airline-web/app/controllers/Application.scala @@ -568,11 +568,27 @@ class Application @Inject()(cc: ControllerComponents) extends AbstractController Ok(Json.obj("airlineGradeLookup" -> airlineGradeLookup)) } - def getAirportChampions(airportId : Int) = Action { - var result = Json.arr() + def getAirportChampions(airportId : Int, airlineId : Option[Int]) = Action { + var result = Json.obj() + var champsJson = Json.arr() + val airport = AirportCache.getAirport(airportId, true).get - ChampionUtil.loadAirportChampionInfoByAirport(airportId).sortBy(_.ranking).foreach { info => - result = result.append(Json.toJson(info).asInstanceOf[JsObject] + ("loyalty" -> JsNumber(BigDecimal(airport.getAirlineLoyalty(info.loyalist.airline.id)).setScale(2, RoundingMode.HALF_EVEN)))) + val champsSortedByRank = ChampionUtil.loadAirportChampionInfoByAirport(airportId).sortBy(_.ranking) + champsSortedByRank.foreach { info => + champsJson = champsJson.append(Json.toJson(info).asInstanceOf[JsObject] + ("loyalty" -> JsNumber(BigDecimal(airport.getAirlineLoyalty(info.loyalist.airline.id)).setScale(2, RoundingMode.HALF_EVEN)))) + } + result = result + ("champions" -> champsJson) + airlineId.foreach { airlineId => + if (champsSortedByRank.find(_.loyalist.airline.id == airlineId).isEmpty) { //query airline not a champ, now see check ranking + val loyalistsSorted = LoyalistSource.loadLoyalistsByAirportId(airportId).sortBy(_.amount).reverse + loyalistsSorted.find(_.airline.id == airlineId) match { + case Some(entry) => + val rank = loyalistsSorted.indexOf(entry) + 1 + val loyalty = BigDecimal(airport.getAirlineLoyalty(airlineId)).setScale(2, RoundingMode.HALF_EVEN) + result = result + ("currentAirline" -> (Json.toJson(entry).asInstanceOf[JsObject] + ("ranking" -> JsNumber(rank)) + ("loyalty" -> JsNumber(loyalty)))) + case None => //nothing + } + } } Ok(result) } diff --git a/airline-web/app/controllers/BannerApplication.scala b/airline-web/app/controllers/BannerApplication.scala new file mode 100644 index 000000000..4d5e7ba0d --- /dev/null +++ b/airline-web/app/controllers/BannerApplication.scala @@ -0,0 +1,23 @@ +package controllers + +import com.patson.data.NoticeSource +import com.patson.model.notice.{Notice, NoticeCategory} +import com.typesafe.config.ConfigFactory +import controllers.AuthenticationObject.AuthenticatedAirline +import play.api.libs.json.Json +import play.api.mvc._ + +import javax.inject.Inject + +class BannerApplication @Inject()(cc: ControllerComponents) extends AbstractController(cc) { + val configFactory = ConfigFactory.load() + val bannerEnabled = if (configFactory.hasPath("bannerEnabled")) configFactory.getBoolean("bannerEnabled") else false + + def getBanner() = Action { + if (bannerEnabled) { + Ok(Json.obj("bannerUrl" -> GooglePhotoUtil.drawBannerUrl())) + } else { + Ok(Json.obj()) + } + } +} diff --git a/airline-web/app/controllers/ColorApplication.scala b/airline-web/app/controllers/ColorApplication.scala new file mode 100644 index 000000000..f960dc2be --- /dev/null +++ b/airline-web/app/controllers/ColorApplication.scala @@ -0,0 +1,120 @@ +package controllers + +import com.patson.data.{AllianceSource, ColorSource} +import com.patson.model._ +import controllers.AuthenticationObject.AuthenticatedAirline +import play.api.libs.json._ +import play.api.mvc.Security.AuthenticatedRequest +import play.api.mvc._ + +import javax.inject.Inject +import scala.collection.mutable + + +class ColorApplication @Inject()(cc: ControllerComponents) extends AbstractController(cc) { + val REVERT_COLOR = "REVERT" + + def getAirlineLabelColors(airlineId : Int) = AuthenticatedAirline(airlineId) { request => + val allianceLabelColors = internalGetAllianceLabelColors(request.user) + + val allAirlinesByAllianceId : Map[Int, List[Airline]] = AllianceSource.loadAllAlliances().map(entry => (entry.id, entry.members.map(_.airline))).toMap + val airlineLabelColors : mutable.Map[Int, String] = mutable.Map() + allianceLabelColors.foreach { + case (allianceId, color) => + allAirlinesByAllianceId(allianceId).foreach { + airline => airlineLabelColors.put(airline.id, color) + } + } + + Ok(Json.toJson(airlineLabelColors.filter { + case (airlineId, color) => color != REVERT_COLOR + })) + } + + def getAllianceLabelColor(airlineId : Int, allianceId : Int) = AuthenticatedAirline(airlineId) { request => + val allianceLabelColors = internalGetAllianceLabelColors(request.user) + allianceLabelColors.get(allianceId) match { + case Some(color) => Ok(Json.obj("color" -> color)) + case None => Ok(Json.obj()) + } + } + + def internalGetAllianceLabelColors(currentAirline : Airline) = { + val allianceTextColors = mutable.HashMap[Int, String]() + currentAirline.getAllianceId().foreach { currentAllianceId => + if (AllianceSource.loadAllianceMemberByAirline(currentAirline).get.role != AllianceRole.APPLICANT) { //an approved member + allianceTextColors.addAll(ColorSource.loadAllianceLabelColorsFromAlliance(currentAllianceId)) + } + } + + allianceTextColors.addAll(ColorSource.loadAllianceLabelColorsFromAirline(currentAirline.id)) + + allianceTextColors + } + + + def setAllianceLabelColorAsAirline(airlineId: Int, targetAllianceId : Int, color : String) = AuthenticatedAirline(airlineId) { request => + ColorSource.saveAllianceLabelColorFromAirline(airlineId, targetAllianceId, color) + Ok(Json.obj()) + } + + def setAllianceLabelColorAsAlliance(airlineId: Int, targetAllianceId : Int, color : String) = AuthenticatedAirline(airlineId) { request => + val currentAirline = request.user + + currentAirline.getAllianceId() match { + case Some(currentAllianceId) => + val allianceRole = AllianceSource.loadAllianceMemberByAirline(currentAirline).get.role + if (!AllianceRole.isAdmin(allianceRole)) { + Forbidden(s"Cannot set alliance color : $currentAirline is not an admin of alliance") + } else { + ColorSource.saveAllianceLabelColorFromAlliance(currentAllianceId, targetAllianceId, color) + Ok(Json.obj()) + } + case None => + Forbidden(s"Cannot set alliance color: $currentAirline is not a part of any alliance") + } + } + + def deleteAllianceLabelColorAsAirline(airlineId: Int, targetAllianceId : Int) = AuthenticatedAirline(airlineId) { request => deleteColorAsAirlineBlock(request, airlineId, targetAllianceId) } + + def deleteColorAsAirlineBlock(request : AuthenticatedRequest[AnyContent, Airline], airlineId : Int, targetAllianceId : Int) : Result = { + + val currentAirline = request.user + + currentAirline.getAllianceId() match { + case Some(currentAllianceId) => + if (AllianceSource.loadAllianceMemberByAirline(currentAirline).get.role != AllianceRole.APPLICANT) { //an approved member + //check if there's existing color defined by alliance, if so, create an overwrite with color "REVERT" + if (ColorSource.loadAllianceLabelColorsFromAlliance(currentAllianceId).get(targetAllianceId).isDefined) { + ColorSource.saveAllianceLabelColorFromAirline(airlineId, targetAllianceId, REVERT_COLOR) + return Ok(Json.obj()) + } + } + case None => //nothing + } + + ColorSource.deleteAllianceLabelColorFromAirline(airlineId, targetAllianceId) + return Ok(Json.obj()) + } + + def deleteAllianceLabelColorAsAlliance(airlineId: Int, targetAllianceId : Int) = AuthenticatedAirline(airlineId) { request => + val currentAirline = request.user + + currentAirline.getAllianceId() match { + case Some(currentAllianceId) => + val allianceRole = AllianceSource.loadAllianceMemberByAirline(currentAirline).get.role + if (!AllianceRole.isAdmin(allianceRole)) { + Forbidden(s"Cannot set alliance color : $currentAirline is not an admin of alliance") + } else { + ColorSource.deleteAllianceLabelColorFromAlliance(currentAllianceId, targetAllianceId) + ColorSource.deleteAllianceLabelColorFromAirline(currentAirline.id, targetAllianceId) //otherwise admin can have revert stuck at airline level and hard to get out + Ok(Json.obj()) + } + case None => + Forbidden(s"Cannot set alliance color: $currentAirline is not a part of any alliance") + } + } + + + +} diff --git a/airline-web/app/controllers/GenericTransitApplication.scala b/airline-web/app/controllers/GenericTransitApplication.scala new file mode 100644 index 000000000..a461ca880 --- /dev/null +++ b/airline-web/app/controllers/GenericTransitApplication.scala @@ -0,0 +1,92 @@ +package controllers + +import com.patson.data.LinkSource +import com.patson.model._ +import play.api.libs.json._ +import play.api.mvc._ + +import javax.inject.Inject +import scala.math.BigDecimal.int2bigDecimal + + +class GenericTransitApplication @Inject()(cc: ControllerComponents) extends AbstractController(cc) { + def getGenericTransits(airportId : Int) = Action { request => + val genericTransits = (LinkSource.loadLinksByCriteria(List(("from_airport", airportId), ("transport_type", TransportType.GENERIC_TRANSIT.id))) ++ + LinkSource.loadLinksByCriteria(List(("to_airport", airportId), ("transport_type", TransportType.GENERIC_TRANSIT.id)))).map(_.asInstanceOf[GenericTransit]) + var resultJson = Json.arr() + val consumptionByLinkId = LinkSource.loadLinkConsumptionsByLinksId(genericTransits.map(_.id)).map(entry => (entry.link.id, entry)).toMap + genericTransits.foreach { transit => + val toAirport = + if (transit.from.id == airportId) { + transit.to + } else { + transit.from + } + + var transitJson = Json.obj("toAirportId" -> toAirport.id, "toAirportText" -> toAirport.displayText, "toAirportPopulation" -> toAirport.population, "capacity" -> transit.capacity.total, "linkId" -> transit.id) + consumptionByLinkId.get(transit.id) match { + case Some(consumption) => + transitJson = transitJson + ("passenger" -> JsNumber(consumption.link.getTotalSoldSeats)) + case None => + transitJson = transitJson + ("passenger" -> JsNumber(0)) + } + resultJson = resultJson.append(transitJson) + } + Ok(resultJson) + + } + +// def updateShuttles(airlineId : Int, airportId : Int) = AuthenticatedAirline(airlineId) { request => +// val airline : Airline = request.user +// val inputArray = request.body.asInstanceOf[AnyContentAsJson].json.asInstanceOf[JsArray] +// val updates = inputArray.value.map { entry => +// ShuttleUpdate(entry("linkId").as[Int], entry("capacity").as[Int]) +// }.toList +// +// val newCapacityByLinkId = updates.map(entry => (entry.linkId, entry.capacity)).toMap +// +// getRejections(airline, AirportCache.getAirport(airportId).get, updates) match { +// case Some(rejection) => +// BadRequest(rejection) +// case None => +// val updatingLinks = LinkSource.loadLinksByIds(updates.map(_.linkId)).map { shuttle => +// shuttle.capacity = LinkClassValues.getInstance(economy = newCapacityByLinkId(shuttle.id)) +// shuttle +// } +// LinkSource.updateLinks(updatingLinks) +// Ok(Json.obj()) +// } +// } + +// case class ShuttleUpdate(linkId : Int, capacity : Int) +// +// def getRejections(airline : Airline, airport : Airport, newShuttles : List[ShuttleUpdate]) : Option[String] = { +// AirlineSource.loadShuttleServiceByAirlineAndAirport(airline.id, airport.id) match { +// case Some(shuttleService) => +// if (newShuttles.map(_.capacity).sum > shuttleService.getCapacity) { +// return Some(s"New shuttles require capacity ${newShuttles.map(_.capacity).sum} > max ${shuttleService.getCapacity}") +// } +// val existingLinks = LinkSource.loadLinksByIds(newShuttles.map(_.linkId)) +// if (existingLinks.size != newShuttles.length) { +// return Some(s"New shuttles has count ${newShuttles.length} != existing ${existingLinks.size}") +// } +// +// newShuttles.foreach { +// case ShuttleUpdate(linkId, capacity) => +// if (capacity < 0 || capacity % 100 != 0) { +// return Some(s"Invalid shuttle capacity $capacity") +// } +// existingLinks.find(_.id == linkId).foreach { existingLink => +// if (existingLink.transportType != TransportType.SHUTTLE || existingLink.from.id != airport.id) { +// return Some(s"Invalid update to shuttle $existingLink") +// } +// } +// } +// return None +// case None => +// return Some(s"Shuttle service does not exists for ${airport.displayText}") +// } +// } + + +} diff --git a/airline-web/app/controllers/GooglePhotoUtil.java b/airline-web/app/controllers/GooglePhotoUtil.java new file mode 100644 index 000000000..331b5069d --- /dev/null +++ b/airline-web/app/controllers/GooglePhotoUtil.java @@ -0,0 +1,153 @@ +package controllers; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; +import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.store.FileDataStoreFactory; +import com.google.api.client.util.store.MemoryDataStoreFactory; +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.rpc.ApiException; +import com.google.auth.oauth2.UserCredentials; +import com.google.common.collect.ImmutableList; +import com.google.photos.library.v1.PhotosLibraryClient; +import com.google.photos.library.v1.PhotosLibrarySettings; +import com.google.photos.library.v1.internal.InternalPhotosLibraryClient; +import com.google.photos.library.v1.proto.ListAlbumsRequest; +import com.google.photos.types.proto.Album; +import com.google.photos.types.proto.MediaItem; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +public class GooglePhotoUtil { + private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); + + /** + * Global instance of the scopes required by this quickstart. + * If modifying these scopes, delete your previously saved tokens/ folder. + */ + private static final String CREDENTIALS_FILE_PATH = "/google-oauth-credentials.json"; + private static final String TOKENS_DIRECTORY_PATH = "google-tokens"; + + private static final List REQUIRED_SCOPES = + ImmutableList.of( + "https://www.googleapis.com/auth/photoslibrary.readonly"); + private static final String ALBUM_TITLE = "banners"; + private static final List bannerUrls = new ArrayList<>(); + + static { + refreshBanners(); + } + + /** + * Creates an authorized Credential object. + * @param HTTP_TRANSPORT The network HTTP Transport. + * @return An authorized Credential object. + * @throws IOException If the credentials.json file cannot be found. + */ + private static UserCredentials getCredentials(final NetHttpTransport HTTP_TRANSPORT) throws IOException { + // Load client secrets. + InputStream in = GooglePhotoUtil.class.getResourceAsStream(CREDENTIALS_FILE_PATH); + GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in)); + + // Build flow and trigger user authorization request. + GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( + HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, REQUIRED_SCOPES) + .setDataStoreFactory(new FileDataStoreFactory(new java.io.File(TOKENS_DIRECTORY_PATH))) + //.setDataStoreFactory(new MemoryDataStoreFactory()) + .setAccessType("offline") + .build(); + Credential credential = new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user"); + return UserCredentials.newBuilder() + .setClientId(clientSecrets.getDetails().getClientId()) + .setClientSecret(clientSecrets.getDetails().getClientSecret()) + .setRefreshToken(credential.getRefreshToken()) + .build(); + } + + public static void refreshBanners() { + synchronized (bannerUrls) { + bannerUrls.clear(); + try { + bannerUrls.addAll(loadBannerUrls()); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private static Random random = new Random(); + public static String drawBannerUrl() { + synchronized (bannerUrls) { + if (bannerUrls.isEmpty()) { + return null; + } else { + return bannerUrls.get(random.nextInt(bannerUrls.size())); + } + } + } + + + private static List loadBannerUrls() throws GeneralSecurityException, IOException { + final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); + // Set up the Photos Library Client that interacts with the API + PhotosLibrarySettings settings = + PhotosLibrarySettings.newBuilder() + .setCredentialsProvider( + FixedCredentialsProvider.create(getCredentials(HTTP_TRANSPORT))) + .build(); + + + List bannerUrls = new ArrayList<>(); + try (PhotosLibraryClient photosLibraryClient = + PhotosLibraryClient.initialize(settings)) { + +// Make a request to list all albums in the user's library + // Iterate over all the albums in this list + // Pagination is handled automatically + + InternalPhotosLibraryClient.ListAlbumsPagedResponse listAlbumsPagedResponse = photosLibraryClient.listAlbums(); + String albumId = null; + for (Album album : listAlbumsPagedResponse.iterateAll()) { + if (ALBUM_TITLE.equals(album.getTitle())) { + albumId = album.getId(); + break; + } + } + if (albumId == null) { + System.err.println("no album found with title " + ALBUM_TITLE); + } + + + // Make a request to list all media items in an album + // Provide the ID of the album as a parameter in the searchMediaItems call + // Iterate over all the retrieved media items + InternalPhotosLibraryClient.SearchMediaItemsPagedResponse response = photosLibraryClient.searchMediaItems(albumId); + + for (MediaItem item : response.iterateAll()) { + bannerUrls.add(item.getBaseUrl()); + } + } catch (ApiException e) { + e.printStackTrace(); + } + return bannerUrls; + } + + public static void main(String[] args) throws GeneralSecurityException, IOException { + loadBannerUrls(); + } +} diff --git a/airline-web/app/controllers/LogApplication.scala b/airline-web/app/controllers/LogApplication.scala index 215db783d..ae454cef1 100644 --- a/airline-web/app/controllers/LogApplication.scala +++ b/airline-web/app/controllers/LogApplication.scala @@ -21,7 +21,8 @@ class LogApplication @Inject()(cc: ControllerComponents) extends AbstractControl "categoryText" -> JsString(LogCategory.getDescription(log.category)), "severity" -> JsNumber(log.severity.id), "severityText" -> JsString(LogSeverity.getDescription(log.severity)), - "cycleAgo" -> JsNumber(currentCycle - log.cycle) + "cycleAgo" -> JsNumber(currentCycle - log.cycle), + "properties" -> Json.toJson(log.properties) )) } diff --git a/airline-web/app/controllers/OlympicsApplication.scala b/airline-web/app/controllers/OlympicsApplication.scala index f545c5490..ef6743b25 100644 --- a/airline-web/app/controllers/OlympicsApplication.scala +++ b/airline-web/app/controllers/OlympicsApplication.scala @@ -193,7 +193,9 @@ class OlympicsApplication @Inject()(cc: ControllerComponents) extends AbstractCo EventSource.loadPickedRewardOption(eventId, airlineId, RewardCategory.OLYMPICS_PASSENGER) match { case Some(claimedReward) => - result = result + ("claimedPassengerReward" -> Json.toJson(claimedReward)) + val redeemDescription = claimedReward.redeemDescription(eventId, airlineId) + val rewardJson = Json.toJson(claimedReward).asInstanceOf[JsObject] + ("redeemDescription" -> JsString(redeemDescription)) + result = result + ("claimedPassengerReward" -> rewardJson) case None => //then see if a reward can be claimed hasUnclaimedPassengerAward(eventId, airlineId, currentCycle) match { case Right(_) => @@ -385,7 +387,14 @@ class OlympicsApplication @Inject()(cc: ControllerComponents) extends AbstractCo def getOlympicsPassengerRewardOptions(airlineId : Int, eventId : Int) = AuthenticatedAirline(airlineId) { request => hasUnclaimedPassengerAward(eventId, airlineId, CycleSource.loadCycle()) match { case Right(_) => - Ok(Json.obj("title" -> "Reward of fulfilling passenger goal", "options" -> Olympics.passengerRewardOptions)) + var optionsArray = Json.arr() + Olympics.passengerRewardOptions.foreach { reward => + val description = reward.redeemDescription(eventId, airlineId) + val optionJson : JsValue = Json.toJson(reward).asInstanceOf[JsObject] + ("redeemDescription" -> JsString(description)) + optionsArray = optionsArray.append(optionJson) + } + + Ok(Json.obj("title" -> "Reward of fulfilling passenger goal", "options" -> optionsArray)) case Left(rejection) => BadRequest(rejection) } } diff --git a/airline-web/app/controllers/RankingUtil.scala b/airline-web/app/controllers/RankingUtil.scala index 19ef056bd..e12506405 100644 --- a/airline-web/app/controllers/RankingUtil.scala +++ b/airline-web/app/controllers/RankingUtil.scala @@ -29,20 +29,20 @@ object RankingUtil { } private[this] def updateRankings() = { - val linkConsumptions = LinkSource.loadLinkConsumptions() - val linkConsumptionsByAirline = linkConsumptions.groupBy(_.link.airline.id) + val flightConsumptions = LinkSource.loadLinkConsumptions().filter(_.link.transportType == TransportType.FLIGHT) + val flightConsumptionsByAirline = flightConsumptions.groupBy(_.link.airline.id) val links = LinkSource.loadAllFlightLinks().groupBy(_.airline.id) val airlinesById = AirlineSource.loadAllAirlines(fullLoad = true).map( airline => (airline.id, airline)).toMap val updatedRankings = scala.collection.mutable.Map[RankingType.Value, List[Ranking]]() - updatedRankings.put(RankingType.PASSENGER, getPassengerRanking(linkConsumptionsByAirline, airlinesById)) - updatedRankings.put(RankingType.PASSENGER_MILE, getPassengerMileRanking(linkConsumptionsByAirline, airlinesById)) + updatedRankings.put(RankingType.PASSENGER, getPassengerRanking(flightConsumptionsByAirline, airlinesById)) + updatedRankings.put(RankingType.PASSENGER_MILE, getPassengerMileRanking(flightConsumptionsByAirline, airlinesById)) updatedRankings.put(RankingType.REPUTATION, getReputationRanking(airlinesById)) updatedRankings.put(RankingType.SERVICE_QUALITY, getServiceQualityRanking(airlinesById)) updatedRankings.put(RankingType.LINK_COUNT, getLinkCountRanking(links, airlinesById)) - updatedRankings.put(RankingType.LINK_PROFIT, getLinkProfitRanking(linkConsumptions, airlinesById)) + updatedRankings.put(RankingType.LINK_PROFIT, getLinkProfitRanking(flightConsumptions, airlinesById)) updatedRankings.put(RankingType.LOUNGE, getLoungeRanking(LoungeHistorySource.loadAll, airlinesById)) - updatedRankings.put(RankingType.AIRPORT, getAirportRanking(linkConsumptions)) + updatedRankings.put(RankingType.AIRPORT, getAirportRanking(flightConsumptions)) // val linkConsumptionsByAirlineAndZone = getPassengersByZone(linkConsumptionsByAirline) // updatedRankings.put(RankingType.PASSENGER_AS, getPassengerByZoneRanking(linkConsumptionsByAirlineAndZone, airlinesById, RankingType.PASSENGER_AS, "AS")) // updatedRankings.put(RankingType.PASSENGER_AF, getPassengerByZoneRanking(linkConsumptionsByAirlineAndZone, airlinesById, RankingType.PASSENGER_AF, "AF")) diff --git a/airline-web/app/controllers/SearchApplication.scala b/airline-web/app/controllers/SearchApplication.scala index 5264142b1..d9fad66fd 100644 --- a/airline-web/app/controllers/SearchApplication.scala +++ b/airline-web/app/controllers/SearchApplication.scala @@ -194,7 +194,7 @@ class SearchApplication @Inject()(cc: ControllerComponents) extends AbstractCont } linkJson = linkJson + ("features" -> Json.toJson(getLinkFeatures(detailedLink).map(_.toString))) - case TransportType.SHUTTLE => + case TransportType.GENERIC_TRANSIT => //linkJson = linkJson + ("features" -> Json.toJson(List(LinkFeature.SHUTTLE.toString))) } linkJson = linkJson + ("transportType" -> JsString(detailedTransport.transportType.toString)) @@ -439,6 +439,7 @@ class SearchApplication @Inject()(cc: ControllerComponents) extends AbstractCont "distance" -> distance, "flightType" -> FlightType.label(Computation.getFlightType(fromAirport, toAirport, distance)), "directDemand" -> directDemand, + "mutualRelationship" -> countryRelationship, "fromAirportBusinessDemand" -> directFromAirportBusinessDemand, "toAirportBusinessDemand" -> directToAirportBusinessDemand, "fromAirportTouristDemand" -> directFromAirportTouristDemand, diff --git a/airline-web/app/controllers/SearchUtil.java b/airline-web/app/controllers/SearchUtil.java index f9383c7ab..984ffaa7b 100644 --- a/airline-web/app/controllers/SearchUtil.java +++ b/airline-web/app/controllers/SearchUtil.java @@ -73,8 +73,16 @@ public static void main(String[] args) throws IOException { public static void init() throws IOException { try (RestHighLevelClient client = getClient()) { + System.out.println("Initializing ES airports"); initAirports(client); + System.out.println("Initializing ES countires"); initCountries(client); + System.out.println("Initializing ES zones"); + initZones(client); + System.out.println("Initializing ES airlines"); + initAirlines(client); + System.out.println("Initializing ES alliances"); + initAlliances(client); } System.out.println("ES DONE"); } @@ -189,7 +197,7 @@ public static void addAirline(Airline airline) { System.out.println("Added airline " + airline + " to ES"); } - private static void initAlliances(RestHighLevelClient client) throws IOException { + public static void initAlliances(RestHighLevelClient client) throws IOException { if (isIndexExist(client, "alliances")) { client.indices().delete(new DeleteIndexRequest("alliances"), RequestOptions.DEFAULT); } @@ -236,6 +244,17 @@ public static void removeAlliance(int allianceId) { System.out.println("Removed alliance with id " + allianceId + " from ES"); } + public static void refreshAlliances() { + try (RestHighLevelClient client = getClient()) { + System.out.println("Refreshing ES alliances"); + initAlliances(client); + } catch (IOException exception) { + exception.printStackTrace(); + } + System.out.println("Refreshed ES alliances"); + } + + private static final Pattern letterSpaceOnlyPattern = Pattern.compile("^[ A-Za-z]+$"); public static List searchAirport(String input) { diff --git a/airline-web/app/controllers/ShuttleApplication.scala b/airline-web/app/controllers/ShuttleApplication.scala deleted file mode 100644 index c1ca2c703..000000000 --- a/airline-web/app/controllers/ShuttleApplication.scala +++ /dev/null @@ -1,103 +0,0 @@ -package controllers - -import com.patson.data.{AirlineSource, AlertSource, CycleSource, LinkSource, LinkStatisticsSource} -import com.patson.model.{ShuttleService, _} -import com.patson.util.AirportCache -import controllers.AuthenticationObject.AuthenticatedAirline -import models.{AirportFacility, Consideration, FacilityType} -import play.api.libs.json._ -import play.api.mvc._ - -import javax.inject.Inject -import scala.math.BigDecimal.int2bigDecimal - - -class ShuttleApplication @Inject()(cc: ControllerComponents) extends AbstractController(cc) { - def getShuttles(airlineId : Int, airportId : Int) = AuthenticatedAirline(airlineId) { request => - val airport = AirportCache.getAirport(airportId).get - val shuttles = LinkSource.loadLinksByCriteria(List(("airline", airlineId), ("from_airport", airportId), ("transport_type", TransportType.SHUTTLE.id))).map(_.asInstanceOf[Shuttle]) - if (shuttles.isEmpty) { //generate empty ones - var shuttlesJson = Json.arr() - Computation.getDomesticAirportWithinRange(airport, ShuttleService.COVERAGE_RANGE).filter(_.id != airport.id).foreach { airportWithinRange => - val shuttleJson = Json.obj( - "toAirportId"-> airportWithinRange.id, - "toAirportText" -> airportWithinRange.displayText, - "toAirportPopulation" -> airportWithinRange.population, - "capacity" -> 0, - "passenger" -> 0, - "unitCost" -> Shuttle.UPKEEP_PER_CAPACITY - ) - shuttlesJson = shuttlesJson.append(shuttleJson) - } - Ok(shuttlesJson) - } else { - var shuttlesJson = Json.arr() - val consumptionByLinkId = LinkSource.loadLinkConsumptionsByLinksId(shuttles.map(_.id)).map(entry => (entry.link.id, entry)).toMap - shuttles.foreach { shuttle => - var shuttleJson = Json.obj("toAirportId" -> shuttle.to.id, "toAirportText" -> shuttle.to.displayText, "toAirportPopulation" -> shuttle.to.population, "capacity" -> shuttle.capacity.total, "linkId" -> shuttle.id, "unitCost" -> Shuttle.UPKEEP_PER_CAPACITY) - consumptionByLinkId.get(shuttle.id) match { - case Some(consumption) => - shuttleJson = shuttleJson + ("passenger" -> JsNumber(consumption.link.getTotalSoldSeats)) - case None => - shuttleJson = shuttleJson + ("passenger" -> JsNumber(0)) - } - shuttlesJson = shuttlesJson.append(shuttleJson) - } - Ok(shuttlesJson) - } - } - - def updateShuttles(airlineId : Int, airportId : Int) = AuthenticatedAirline(airlineId) { request => - val airline : Airline = request.user - val inputArray = request.body.asInstanceOf[AnyContentAsJson].json.asInstanceOf[JsArray] - val updates = inputArray.value.map { entry => - ShuttleUpdate(entry("linkId").as[Int], entry("capacity").as[Int]) - }.toList - - val newCapacityByLinkId = updates.map(entry => (entry.linkId, entry.capacity)).toMap - - getRejections(airline, AirportCache.getAirport(airportId).get, updates) match { - case Some(rejection) => - BadRequest(rejection) - case None => - val updatingLinks = LinkSource.loadLinksByIds(updates.map(_.linkId)).map { shuttle => - shuttle.capacity = LinkClassValues.getInstance(economy = newCapacityByLinkId(shuttle.id)) - shuttle - } - LinkSource.updateLinks(updatingLinks) - Ok(Json.obj()) - } - } - - case class ShuttleUpdate(linkId : Int, capacity : Int) - - def getRejections(airline : Airline, airport : Airport, newShuttles : List[ShuttleUpdate]) : Option[String] = { - AirlineSource.loadShuttleServiceByAirlineAndAirport(airline.id, airport.id) match { - case Some(shuttleService) => - if (newShuttles.map(_.capacity).sum > shuttleService.getCapacity) { - return Some(s"New shuttles require capacity ${newShuttles.map(_.capacity).sum} > max ${shuttleService.getCapacity}") - } - val existingLinks = LinkSource.loadLinksByIds(newShuttles.map(_.linkId)) - if (existingLinks.size != newShuttles.length) { - return Some(s"New shuttles has count ${newShuttles.length} != existing ${existingLinks.size}") - } - - newShuttles.foreach { - case ShuttleUpdate(linkId, capacity) => - if (capacity < 0 || capacity % 100 != 0) { - return Some(s"Invalid shuttle capacity $capacity") - } - existingLinks.find(_.id == linkId).foreach { existingLink => - if (existingLink.transportType != TransportType.SHUTTLE || existingLink.from.id != airport.id) { - return Some(s"Invalid update to shuttle $existingLink") - } - } - } - return None - case None => - return Some(s"Shuttle service does not exists for ${airport.displayText}") - } - } - - -} diff --git a/airline-web/app/controllers/package.scala b/airline-web/app/controllers/package.scala index ba3e25051..3deb139c6 100644 --- a/airline-web/app/controllers/package.scala +++ b/airline-web/app/controllers/package.scala @@ -284,7 +284,7 @@ package object controllers { implicit object AirlineBaseFormat extends Format[AirlineBase] { def reads(json: JsValue): JsResult[AirlineBase] = { - val airport = AirportCache.getAirport((json \ "airportId").as[Int]).get + val airport = AirportCache.getAirport((json \ "airportId").as[Int], true).get val airline = AirlineCache.getAirline((json \ "airlineId").as[Int]).get val scale = (json \ "scale").as[Int] val headquarter = (json \ "headquarter").as[Boolean] @@ -359,22 +359,6 @@ package object controllers { } } - implicit object ShuttleServiceWrites extends Writes[ShuttleService] { - def writes(shuttleService: ShuttleService): JsValue = { - var jsObject = JsObject(List( - "airportId" -> JsNumber(shuttleService.airport.id), - "airportName" -> JsString(shuttleService.airport.name), - "airlineId" -> JsNumber(shuttleService.airline.id), - "airlineName" -> JsString(shuttleService.airline.name), - "name" -> JsString(shuttleService.name), - "level" -> JsNumber(shuttleService.level), - "upkeep" -> JsNumber(shuttleService.basicUpkeep), - "type" -> JsString(FacilityType.SHUTTLE.toString()), - "capacity" -> JsNumber(shuttleService.getCapacity), - "foundedCycle" -> JsNumber(shuttleService.foundedCycle))) - jsObject - } - } implicit object LoungeConsumptionDetailsWrites extends Writes[LoungeConsumptionDetails] { def writes(details: LoungeConsumptionDetails): JsValue = { diff --git a/airline-web/app/models/AirportFacility.scala b/airline-web/app/models/AirportFacility.scala index 1d6b9d569..f6eacd59d 100644 --- a/airline-web/app/models/AirportFacility.scala +++ b/airline-web/app/models/AirportFacility.scala @@ -21,5 +21,5 @@ object AirportFacility { object FacilityType extends Enumeration { type FacilityType = Value - val LOUNGE, SHUTTLE = Value + val LOUNGE = Value } \ No newline at end of file diff --git a/airline-web/app/views/index.scala.html b/airline-web/app/views/index.scala.html index 48090e628..28e425fae 100644 --- a/airline-web/app/views/index.scala.html +++ b/airline-web/app/views/index.scala.html @@ -100,6 +100,7 @@ +