import 'dart:math'; import '../enumerations.dart'; import '../models/language.dart'; import '../models/user_profile.dart'; /// Approximate determination of age int calcAge(int? birthYear) { if (birthYear != null) { return (DateTime.now().year - birthYear); } return 0; } /// Returns the approximate age in parentheses, /// or an empty string if [birthYear] is the current year or null. String ageInfo(int? birthYear) { int age = calcAge(birthYear); String ageInfo = age > 0 ? '($age)' : ''; return ageInfo; } /// Convert decimal coordinate to degrees minutes seconds (DMS). String convertDecimalToDMS(double decimalValue, {required bool isLatitude}) { bool isNegative = decimalValue < 0; double absoluteValue = decimalValue.abs(); int degrees = absoluteValue.toInt(); double minutesDecimal = (absoluteValue - degrees) * 60; int minutes = minutesDecimal.toInt(); double secondsDecimal = (minutesDecimal - minutes) * 60; double seconds = double.parse(secondsDecimal.toStringAsFixed(2)); String direction; if (isLatitude) { direction = isNegative ? 'S' : 'N'; } else { direction = isNegative ? 'W' : 'E'; } // return formatted string return '${degrees.abs()}° ${minutes.abs()}\' ${seconds.abs().toStringAsFixed(2)}" $direction'; } /// Distance in kilometers between two location coordinates double calculateDistance(double lat1, double lon1, double lat2, double lon2) { const R = 6371; // earth radius in kilometers // Haversine formula to get distance between latitudes and longitudes // https://en.wikipedia.org/wiki/Haversine_formula final dLat = _degreesToRadians(lat2 - lat1); final dLon = _degreesToRadians(lon2 - lon1); final a = sin(dLat / 2) * sin(dLat / 2) + cos(_degreesToRadians(lat1)) * cos(_degreesToRadians(lat2)) * sin(dLon / 2) * sin(dLon / 2); final c = 2 * atan2(sqrt(a), sqrt(1 - a)); return R * c; } double _degreesToRadians(double degrees) { return degrees * pi / 180; } /// Shortest distance between two users locations double shortestDistanceBetweenUsers( UserProfile currentUser, UserProfile otherUser) { try { if (currentUser.locations.isEmpty || otherUser.locations.isEmpty) { return double.nan; } double shortestDistance = double.nan; // locations currentUser for (var loc1 in currentUser.locations.values) { if (loc1 != null && loc1.latitude != null && loc1.longitude != null) { for (var loc2 in otherUser.locations.values) { if (loc2 != null && loc2.latitude != null && loc2.longitude != null) { double distance = calculateDistance(loc1.latitude!, loc1.longitude!, loc2.latitude!, loc2.longitude!); if (shortestDistance.isNaN || distance < shortestDistance) { shortestDistance = distance; } } } } } return shortestDistance; } catch (e) { return double.nan; } } /// Calculates the matching score of [otherUser] for [currentUser]. double calculateMatchScore(UserProfile currentUser, UserProfile otherUser) { // Score on locations distance double distance = shortestDistanceBetweenUsers(currentUser, otherUser); double distanceScore = _distanceToPoints(distance); // Score on common languages List currentLang = currentUser.languages; List otherLang = otherUser.languages; int matchingLanguages = currentLang.toSet().intersection(otherLang.toSet()).length; int langScore = 0; if (matchingLanguages >= 3) { langScore = 10; } else if (matchingLanguages == 2) { langScore = 9; } else if (matchingLanguages == 1) { langScore = 8; } // Score on common Sectors List currentSectors = currentUser.sectors; List otherSectors = otherUser.sectors; int matchingSectors = currentSectors.toSet().intersection(otherSectors.toSet()).length; int sectorScore = 0; if (matchingSectors >= 4) { sectorScore = 10; } else if (matchingSectors == 3) { sectorScore = 9; } else if (matchingSectors == 2) { sectorScore = 7; } else if (matchingSectors == 1) { sectorScore = 5; } // Score on skills int matchingSkillsSought = currentUser.skillsSought .toSet() .intersection(otherUser.skills.toSet()) .length; int matchingSkillsOffered = currentUser.skills .toSet() .intersection(otherUser.skillsSought.toSet()) .length; int skillsOffered = otherUser.skills.length; int scoreSought = 0; if (matchingSkillsSought >= 3) { scoreSought = 15; } else if (matchingSkillsSought == 2) { scoreSought = 12; } else if (matchingSkillsSought == 1) { scoreSought = 9; } double valueOffered = matchingSkillsOffered / skillsOffered; int scoreOffered; if (valueOffered == 1) { scoreOffered = 15; } else if (valueOffered == 4 / 5) { scoreOffered = 14; } else if (valueOffered == 3 / 4) { scoreOffered = 13; } else if (valueOffered == 2 / 3) { scoreOffered = 12; } else if (valueOffered == 3 / 5) { scoreOffered = 11; } else if (valueOffered == 1 / 2) { scoreOffered = 10; } else if (valueOffered == 2 / 5) { scoreOffered = 9; } else if (valueOffered == 1 / 3) { scoreOffered = 8; } else if (valueOffered == 1 / 4) { scoreOffered = 7; } else if (valueOffered == 1 / 5) { scoreOffered = 6; } else { scoreOffered = 0; } double skillsScore = (scoreSought + scoreOffered) / 1.5; // Score on availability int availabilityScore; AvailabilityOption currentAvail = currentUser.availability; AvailabilityOption otherAvail = otherUser.availability; if (currentAvail != otherAvail && (currentAvail == AvailabilityOption.flexible || otherAvail == AvailabilityOption.flexible)) { availabilityScore = 4; } else { int availabilityDifference = (currentAvail.index - otherAvail.index).abs(); // map possible values [0 1 2 3] to points {0 1 3 5] availabilityScore = (3 - availabilityDifference) * 2 - 1; } // Score on Vision List currentVisions = currentUser.visions; List otherVisions = otherUser.visions; int matchingVisions = currentVisions.toSet().intersection(otherVisions.toSet()).length; int visionScore; if (matchingVisions == 4 || ((currentVisions.length == otherVisions.length) && (matchingVisions == currentVisions.length))) { visionScore = 4; // full match, max score } else if (matchingVisions > 1) { visionScore = 3; // some match } else if (matchingVisions == 1) { visionScore = 2; // one match } else { visionScore = 0; // no match } // Score on WorkValues List currentWorks = currentUser.workValues; List otherWorks = otherUser.workValues; int matchingWorks = currentWorks.toSet().intersection(otherWorks.toSet()).length; int workScore; if (matchingWorks == 2 || ((currentWorks.length == otherWorks.length) && (matchingWorks == 1))) { workScore = 4; // full match, max score } else if (matchingWorks == 1) { workScore = 2; // semi match } else { workScore = 0; // no match } // Score on Risk RiskTolerance currentRisk = currentUser.risk; RiskTolerance otherRisk = currentUser.risk; int riskScore = 0; if (currentUser.risk == otherUser.risk) { riskScore = 3; } else { int riskDifference = (currentRisk.index - otherRisk.index).abs(); riskScore = 3 - riskDifference; } // Score on CorporateCulture int cultureScore = 0; if (currentUser.culture == otherUser.culture) { cultureScore = 2; } // Score on Communication int communicationScore = 0; if (currentUser.communication == otherUser.communication) { communicationScore = 1; } // Calc total score. Sum of each score divided by its own max score value, // multiplied with its own weight. double totalScore = distanceScore + langScore + sectorScore + skillsScore + availabilityScore + riskScore + visionScore + workScore + cultureScore + communicationScore; return min(totalScore, 99.9); } double _distanceToPoints(double distance) { // Self predefined data points final List distances = [0, 10, 100, 200, 500, 700, 1000]; final List points = [60, 55, 40, 20, 10, 5, 0]; if (distance.isNaN || distance >= distances.last) { return points.last; } else if (distance <= distances[0]) { return points[0]; } // Linear interpolation for (int i = 1; i < distances.length; i++) { if (distances[i] >= distance) { double x0 = distances[i - 1]; double y0 = points[i - 1]; double x1 = distances[i]; double y1 = points[i]; // Interpolate double result = y0 + (y1 - y0) * (distance - x0) / (x1 - x0); return result; } } // Fallback return value, though code should never reach here return points.last; // 50 }