299 lines
8.8 KiB
Dart
299 lines
8.8 KiB
Dart
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<Language> currentLang = currentUser.languages;
|
|
List<Language> 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<SectorOption> currentSectors = currentUser.sectors;
|
|
List<SectorOption> 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<VisionOption> currentVisions = currentUser.visions;
|
|
List<VisionOption> 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<WorkValueOption> currentWorks = currentUser.workValues;
|
|
List<WorkValueOption> 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<double> distances = [0, 10, 100, 200, 500, 700, 1000];
|
|
final List<double> 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
|
|
}
|