|
|
@@ -1,5 +1,7 @@
|
|
1
|
1
|
const Schmervice = require('@hapipal/schmervice')
|
|
2
|
2
|
const cosineSimilarity = require('compute-cosine-similarity')
|
|
|
3
|
+const haversine = require('haversine')
|
|
|
4
|
+const profile = require('../plugins/profile')
|
|
3
|
5
|
|
|
4
|
6
|
const magic = 1000
|
|
5
|
7
|
const scoreResponses = (seeker, potentialMatch) => {
|
|
|
@@ -159,38 +161,102 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
159
|
161
|
|
|
160
|
162
|
return await Profile.query().delete().where('profile_id', profileId)
|
|
161
|
163
|
}
|
|
162
|
|
-
|
|
|
164
|
+ /**
|
|
|
165
|
+ * Grab the zip code string
|
|
|
166
|
+ */
|
|
|
167
|
+ _getZipCodeFromProfile(profile) {
|
|
|
168
|
+ // There should only be one zip code entry per profile
|
|
|
169
|
+ let zip = profile.responses.filter(response => response.response_key_id == 16)[0]
|
|
|
170
|
+ const responseIndexForZip = profile.responses.indexOf(zip)
|
|
|
171
|
+ if(responseIndexForZip >= 0) {
|
|
|
172
|
+ profile.responses.splice(responseIndexForZip, 1)
|
|
|
173
|
+ }
|
|
|
174
|
+ return zip.val
|
|
|
175
|
+ }
|
|
163
|
176
|
/**
|
|
164
|
177
|
* Score a profile
|
|
165
|
178
|
* @param {number} profileId
|
|
166
|
179
|
* @returns {Array} Ordered and scored Profiles
|
|
167
|
180
|
*/
|
|
168
|
|
- async scoreProfilesFor(profileId) {
|
|
|
181
|
+ async scoreProfilesFor(profileId, maxDistanceMiles, distanceUnit) {
|
|
169
|
182
|
const { Profile } = this.server.models()
|
|
170
|
|
-
|
|
|
183
|
+
|
|
171
|
184
|
// Our User Profile to score for
|
|
172
|
185
|
const userProfile = await Profile.query()
|
|
173
|
|
- .findOne('profile_id', profileId)
|
|
174
|
|
- .withGraphFetched('responses')
|
|
175
|
|
- .withGraphFetched('user')
|
|
176
|
|
-
|
|
177
|
|
- const isPosterOpposite = userProfile.user.is_poster == 1 ? 0 : 1
|
|
|
186
|
+ .findOne('profile_id', profileId)
|
|
|
187
|
+ .withGraphFetched('responses')
|
|
|
188
|
+ .withGraphFetched('user')
|
|
|
189
|
+
|
|
|
190
|
+ // Move unneeded responses
|
|
|
191
|
+ const userZip = this._getZipCodeFromProfile(userProfile)
|
|
178
|
192
|
|
|
179
|
193
|
// Find all Profiles that are NOT of our userProfile.type
|
|
180
|
194
|
// ie. If userProfile.type == seeker, then find: poster
|
|
181
|
195
|
let profileIdsOfOppositeType = await Profile.query()
|
|
182
|
|
- .withGraphFetched('responses')
|
|
183
|
|
- .withGraphFetched('user')
|
|
184
|
|
-
|
|
|
196
|
+ .withGraphFetched('responses')
|
|
|
197
|
+ .withGraphFetched('user')
|
|
|
198
|
+
|
|
185
|
199
|
// TODO: Let Objection optimize this
|
|
186
|
|
- profileIdsOfOppositeType = profileIdsOfOppositeType.filter(
|
|
187
|
|
- profile => profile.user.is_poster == isPosterOpposite,
|
|
188
|
|
- )
|
|
189
|
|
-
|
|
190
|
|
- const scored = profileIdsOfOppositeType.map(profile => ({
|
|
191
|
|
- profile_id: profile.profile_id,
|
|
192
|
|
- score: scoreResponses(userProfile, profile),
|
|
|
200
|
+ const isPosterOpposite = userProfile.user.is_poster == 1 ? 0 : 1
|
|
|
201
|
+ profileIdsOfOppositeType = profileIdsOfOppositeType.filter(profile => profile.user.is_poster == isPosterOpposite)
|
|
|
202
|
+
|
|
|
203
|
+ const profilePlusDistance = await Promise.all(profileIdsOfOppositeType.map(async profile => {
|
|
|
204
|
+ const targetZip = this._getZipCodeFromProfile(profile)
|
|
|
205
|
+ const distance = await this._compareDistance(userZip, targetZip, distanceUnit)
|
|
|
206
|
+ return {
|
|
|
207
|
+ ...profile,
|
|
|
208
|
+ distance: [distance.toFixed(2), distanceUnit]
|
|
|
209
|
+ }
|
|
193
|
210
|
}))
|
|
|
211
|
+
|
|
|
212
|
+ // Filter by distance
|
|
|
213
|
+ // TODO: probably do this with a query
|
|
|
214
|
+ const distanceFiltered = profilePlusDistance.filter(profile => {
|
|
|
215
|
+ const profileDistance = Math.floor(parseFloat(profile.distance) * 100)
|
|
|
216
|
+ const adjustedMaxDistance = Math.floor(parseFloat(maxDistanceMiles) * 100)
|
|
|
217
|
+ return profileDistance <= adjustedMaxDistance
|
|
|
218
|
+ })
|
|
|
219
|
+
|
|
|
220
|
+ const scored = distanceFiltered.map(profile => {
|
|
|
221
|
+ return {
|
|
|
222
|
+ // Uncomment to return the whole profile
|
|
|
223
|
+ // ...profile,
|
|
|
224
|
+ profile_id: profile.profile_id,
|
|
|
225
|
+ score: scoreResponses(userProfile, profile),
|
|
|
226
|
+ distance: profile.distance
|
|
|
227
|
+ }
|
|
|
228
|
+ })
|
|
|
229
|
+ // Order by score
|
|
194
|
230
|
return scored.sort((a, b) => a.score - b.score)
|
|
195
|
231
|
}
|
|
|
232
|
+
|
|
|
233
|
+ /**
|
|
|
234
|
+ * Use the db for zipcode info
|
|
|
235
|
+ * @param {string} zipCode
|
|
|
236
|
+ * @param {object}
|
|
|
237
|
+ */
|
|
|
238
|
+ async _latLonForZip(zipCode) {
|
|
|
239
|
+ const { ZipCode } = this.server.models()
|
|
|
240
|
+
|
|
|
241
|
+ const zipInfo = await ZipCode.query().findOne('zip_code_id', parseInt(zipCode))
|
|
|
242
|
+ const latitude = parseFloat(zipInfo.latitude)
|
|
|
243
|
+ const longitude = parseFloat(zipInfo.longitude)
|
|
|
244
|
+
|
|
|
245
|
+ return { latitude, longitude }
|
|
|
246
|
+ }
|
|
|
247
|
+ /**
|
|
|
248
|
+ * Get the distance between two zipcodes
|
|
|
249
|
+ * using the haversine formula
|
|
|
250
|
+ * @param {string} start_zip
|
|
|
251
|
+ * @param {string} end_zip
|
|
|
252
|
+ * @param {number} distance in miles
|
|
|
253
|
+ */
|
|
|
254
|
+ async _compareDistance(start_zip, end_zip, distanceUnit) {
|
|
|
255
|
+ if(!start_zip || !end_zip) return
|
|
|
256
|
+
|
|
|
257
|
+ const start = await this._latLonForZip(start_zip)
|
|
|
258
|
+ const end = await this._latLonForZip(end_zip)
|
|
|
259
|
+
|
|
|
260
|
+ return haversine(start, end, { unit: distanceUnit })
|
|
|
261
|
+ }
|
|
196
|
262
|
}
|