Skip to content
Advertisement

How to get a boolean value if a location coordinate is within the given $geoWithin or $geoNear radius in mongoose?

I want to get a boolean value eg. true or false if a location coordinate is within the given radius is the $geoWithin query or $geoNear pipeline in mongoose aggregate function. If I user $geoNear in the mongoose aggregate pipeline then it only returns the filtered result. So far I have done the following,

The Model:

import * as mongoose from 'mongoose';
import { User } from 'src/user/user.model';

export const BlipSchema = new mongoose.Schema(
  {
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User',
    },
    media: [
      new mongoose.Schema(
        {
          fileName: String,
          mediaType: {
            type: String,
            enum: ['image', 'video'],
          },
        },
        {
          toJSON: {
            transform: function (doc, ret) {
              delete ret._id;
            },
          },
        },
      ),
    ],
    likesCount: {
      type: Number,
      default: 0,
    },
    commentsCount: {
      type: Number,
      default: 0,
    },
    isLiked: Boolean,
    status: {
      type: String,
      enum: ['public', 'private'],
      default: 'public',
    },
    location: {
      type: {
        type: String,
        default: 'Point',
        enum: ['Point'],
      },
      coordinates: [Number],
      address: String,
      description: String,
    },
    lat: Number,
    lng: Number,
    address: String,
    city: {
      type: String,
      default: '',
    },
    createdAt: {
      type: Date,
      default: Date.now,
    },
  },
  {
    toJSON: {
      transform: function (doc, ret) {
        ret.id = ret._id;
        delete ret._id;
        delete ret.location;
        delete ret.__v;
      },
    },
    toObject: { virtuals: true },
  },
);

export interface Media {
  fileName: string;
  mediaType: string;
}

export interface Location {
  type: string;
  coordinates: [number];
  address: string;
  description: string;
}

export interface Blip {
  user: User;
  media: [Media];
  likesCount: number;
  commentsCount: number;
  isLiked: boolean;
  status: string;
  lat: number;
  lng: number;
  address: string;
  city: string;
  createdAt: string;
}

The function in my controller:

async getLocationBlips(user: User, query: any) {
    const aggrea = [];
    aggrea.push(
      {
        $lookup: {
          from: 'users',
          localField: 'user',
          foreignField: '_id',
          as: 'user',
        },
      },
      {
        $unwind: '$user',
      },
      { $set: { 'user.id': '$user._id' } },
      {
        $group: {
          _id: { lat: '$lat', lng: '$lng' },
          lat: { $first: '$lat' },
          lng: { $first: '$lng' },
          isViewable: { $first: false },
          totalBlips: { $sum: 1 },
          blips: {
            $push: {
              id: '$_id',
              media: '$media',
              user: '$user',
              likesCount: '$likesCount',
              commentsCount: '$commentsCount',
              isLiked: '$isLiked',
              status: '$status',
              address: '$address',
              city: '$city',
              createdAt: '$createdAt',
            },
          },
        },
      },
      { $unset: 'blips.media._id' },
      { $unset: 'blips.user._id' },
      { $unset: 'blips.user.__v' },
      {
        $project: {
          _id: 0,
          lat: 1,
          lng: 1,
          totalBlips: 1,
          isViewable: 1,
          blips: 1,
        },
      },
      { $sort: { totalBlips: -1 } },
    );
    if (query.page !== undefined && query.limit !== undefined) {
      const page = query.page * 1;
      const limit = query.limit * 1;
      const skip = (page - 1) * limit;
      aggrea.push({ $skip: skip }, { $limit: parseInt(query.limit, 10) });
    }
    const blipLocations = await this.blipModel.aggregate(aggrea);

    const total = blipLocations.length;

    let viewableBlips = [];

    if (query.lat && query.lng) {
      const radius = query.distance / 3963.2;
      viewableBlips = await this.blipModel.find({
        location: {
          $geoWithin: {
            $centerSphere: [[query.lng, query.lat], radius],
          },
        },
      });
    }

    await Promise.all(
      blipLocations.map(async (blpL) => {
        await Promise.all(
          blpL.blips.map(async (blp) => {
            const like = await this.likeModel.findOne({
              user: user.id,
              blip: blp.id,
            });
            if (like) {
              blp.isLiked = true;
            } else {
              blp.isLiked = false;
            }
            if (query.lat && query.lng) {
              viewableBlips.forEach((vBlp) => {
                if (vBlp._id.toString() === blp.id.toString()) {
                  console.log(vBlp.id);
                  blpL.isViewable = true;
                }
              });
            }
          }),
        );
      }),
    );
    return {
      status: true,
      message: 'Data fetched successfully',
      results: total,
      blipLocations,
    };
  }

In the above snippet, I have a field called isViewable. Right now I am updating this field in runtime. But I want to update this field in the aggregation pipeline. Is there any way to check if the location coordinate is within the provided $geoWithin or $geoNear from the aggregation pipeline? Thanks.

Advertisement

Answer

If you’re fine with using $geoNear (you need the 2dsphere index & it must be the first stage in the pipeline as noted in docs) you could add a distance field and then another field which will output a boolean based on it, like so:

this.blipModel.aggregate([
    {
        $geoNear: {
            key: 'location',
            near: {
                type: 'Point',
                coordinates: [query.lng, query.lat]
            },
            distanceField: 'distance',
            spherical: true
        }
    }, 
    {
        $addFields: {
            isViewable: {
                $cond: {
                    if: {$lte: ["$distance", query.distance]},
                    then: true,
                    else: false
                }
            }
        }
    }
]);

And optionally unset the distance field in another stage if you don’t need it.

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement