import 'dart:math'; import 'package:flutter/foundation.dart' show kDebugMode, debugPrint; import '../enumerations.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 // distance between latitudes and longitudes 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) { // weights const double distanceWeight = 0.55; const double skillWeight = 0.25; const double availabilityWeight = 0.065; const double visionWeight = 0.04; const double riskWeight = 0.035; const double workWeight = 0.025; const double cultureWeight = 0.02; const double communicationWeight = 0.015; if (kDebugMode) { double weightSum = (distanceWeight + skillWeight + availabilityWeight + visionWeight + riskWeight + workWeight + cultureWeight + communicationWeight); if (weightSum != 1) { debugPrint( 'DEBUG Info --> calculateMatchScore : Weights Sum $weightSum != 1'); } } // Score on locations distance double distance = shortestDistanceBetweenUsers(currentUser, otherUser); double distanceScore = _distanceToPercentage(distance); // Score on skills int matchingSkillsSought = currentUser.skillsSought .toSet() .intersection(otherUser.skills.toSet()) .length; int matchingSkillsOffered = otherUser.skills .toSet() .intersection(currentUser.skillsSought.toSet()) .length; int skillsSought = currentUser.skillsSought.length; int skillsOffered = otherUser.skills.length; // Idea: Calculate sum of matching skills divided by amount of skills listed. // As each list can have up to 3 skills max, this gives the following equation // with 3 skills: 0 1/3 2/3 3/3; with 2 skills: 0 1/2 2/2; with 1 skill: 0 1. // In total this will result in a total of 5 different states: // [0], [1/3], [1/2], [2/3], and [1]. double valueSought = matchingSkillsSought / skillsSought; int scoreSought; if (valueSought == 1) { scoreSought = 4; } else if (valueSought == 2 / 3) { scoreSought = 3; } else if (valueSought == 1 / 2) { scoreSought = 2; } else if (valueSought == 1 / 3) { scoreSought = 1; } else { scoreSought = 0; } double valueOffered = matchingSkillsOffered / skillsOffered; int scoreOffered; if (valueOffered == 1) { scoreOffered = 4; } else if (valueOffered == 2 / 3) { scoreOffered = 3; } else if (valueOffered == 1 / 2) { scoreOffered = 2; } else if (valueOffered == 1 / 3) { scoreOffered = 1; } else { scoreOffered = 0; } int skillsScore = scoreSought + scoreOffered; // 8 points max // Score on availability int availabilityScore; AvailabilityOption currentAvail = currentUser.availability; AvailabilityOption otherAvail = otherUser.availability; if (currentAvail == AvailabilityOption.flexible || otherAvail == AvailabilityOption.flexible || currentAvail == otherAvail) { availabilityScore = 3; } else { int availabilityDifference = (currentAvail.index - otherAvail.index).abs(); availabilityScore = 3 - availabilityDifference; } // 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 int riskScore = 0; if (currentUser.risk == otherUser.risk) { riskScore = 1; } // Score on CorporateCulture int cultureScore = 0; if (currentUser.culture == otherUser.culture) { cultureScore = 1; } // 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 = (distanceWeight * distanceScore / 100) + (skillWeight * skillsScore / 8) + (availabilityWeight * availabilityScore / 3) + (visionWeight * visionScore / 4) + (workWeight * workScore / 4) + (riskWeight * riskScore) + (cultureWeight * cultureScore) + (communicationWeight * communicationScore); return totalScore * 100; } double _distanceToPercentage(double distance) { // Self predefined data points final List distances = [0, 5, 50, 100, 200]; final List percentages = [100, 99.5, 95, 90, 50]; if (distance.isNaN || distance >= distances.last) { return percentages.last; } else if (distance <= distances[0]) { return percentages[0]; } // Linear interpolation for (int i = 1; i < distances.length; i++) { if (distances[i] >= distance) { double x0 = distances[i - 1]; double y0 = percentages[i - 1]; double x1 = distances[i]; double y1 = percentages[i]; // Interpolate double percentage = y0 + (y1 - y0) * (distance - x0) / (x1 - x0); return percentage; } } // Fallback return value, though code should never reach here return percentages.last; // 50 }