Add GPS capabilities to charging stations + 2d indexes
MODIFY
src/server/rest/service/ChargingStationService.ts
src/server/rest/service/ChargingStationService.ts
@@ -1153,10 +1153,8 @@ export default class ChargingStationService {
throw new AppAuthError({
errorCode: HTTPAuthError.ERROR,
user: req.user,
- action: Action.LIST,
- entity: Entity.CHARGING_STATIONS,
- module: MODULE_NAME,
- method: 'getChargingStations',
+ action: Action.LIST, entity: Entity.CHARGING_STATIONS,
+ module: MODULE_NAME, method: 'getChargingStations',
});
}
// Filter
@@ -1164,6 +1162,8 @@ export default class ChargingStationService {
// Get Charging Stations
const chargingStations = await ChargingStationStorage.getChargingStations(req.user.tenantID,
{
+ userGPSCoordinates: filteredRequest.UserGPSCoordinates,
+ userGPSMaxDistanceMeters: filteredRequest.UserGPSMaxDistanceMeters,
search: filteredRequest.Search,
withNoSiteArea: filteredRequest.WithNoSiteArea,
withSite: filteredRequest.WithSite,

MODIFY
src/server/rest/service/security/ChargingStationSecurity.ts
src/server/rest/service/security/ChargingStationSecurity.ts
@@ -77,6 +77,9 @@ export default class ChargingStationSecurity {
if (chargingStation.ocpiData) {
filteredChargingStation.ocpiData = chargingStation.ocpiData;
}
+ if (Utils.objectHasProperty(chargingStation, 'distanceMeters')) {
+ filteredChargingStation.distanceMeters = chargingStation.distanceMeters;
+ }
filteredChargingStation.connectors = chargingStation.connectors.map((connector) => {
if (!connector) {
return connector;
@@ -320,6 +323,18 @@ export default class ChargingStationSecurity {
filteredRequest.ConnectorType = sanitize(request.ConnectorType);
filteredRequest.IncludeDeleted = UtilsSecurity.filterBoolean(request.IncludeDeleted);
filteredRequest.ErrorType = sanitize(request.ErrorType);
+ if (Utils.containsGPSCoordinates([request.UserGPSLongitude, request.UserGPSLatitude])) {
+ filteredRequest.UserGPSCoordinates = [
+ Utils.convertToFloat(sanitize(request.UserGPSLongitude)),
+ Utils.convertToFloat(sanitize(request.UserGPSLatitude))
+ ];
+ if (request.UserGPSMaxDistanceMeters) {
+ request.UserGPSMaxDistanceMeters = Utils.convertToInt(sanitize(request.UserGPSMaxDistanceMeters));
+ if (request.UserGPSMaxDistanceMeters > 0) {
+ filteredRequest.UserGPSMaxDistanceMeters = request.UserGPSMaxDistanceMeters;
+ }
+ }
+ }
UtilsSecurity.filterSkipAndLimit(request, filteredRequest);
UtilsSecurity.filterSort(request, filteredRequest);
return filteredRequest;

MODIFY
src/storage/mongodb/AssetStorage.ts
src/storage/mongodb/AssetStorage.ts
@@ -66,7 +66,8 @@ export default class AssetStorage {
_id: assetToSave.id ? Utils.convertToObjectID(assetToSave.id) : new ObjectID(),
name: assetToSave.name,
siteAreaID: Utils.convertToObjectID(assetToSave.siteAreaID),
- coordinates: assetToSave.coordinates,
+ coordinates: Utils.containsGPSCoordinates(assetToSave.coordinates) ? assetToSave.coordinates.map(
+ (coordinate) => Utils.convertToFloat(coordinate)) : [],
assetType: assetToSave.assetType,
dynamicAsset: assetToSave.dynamicAsset,
issuer: Utils.convertToBoolean(assetToSave.issuer),

MODIFY
src/storage/mongodb/ChargingStationStorage.ts
src/storage/mongodb/ChargingStationStorage.ts
@@ -114,6 +114,7 @@ export default class ChargingStationStorage {
search?: string; chargingStationIDs?: string[]; siteAreaIDs?: string[]; withNoSiteArea?: boolean;
connectorStatuses?: string[]; connectorTypes?: string[]; statusChangedBefore?: Date;
siteIDs?: string[]; withSite?: boolean; includeDeleted?: boolean; offlineSince?: Date; issuer?: boolean;
+ userGPSCoordinates?: number[]; userGPSMaxDistanceMeters?: number;
},
dbParams: DbParams, projectFields?: string[]): Promise<DataResult<ChargingStation>> {
// Debug
@@ -126,8 +127,24 @@ export default class ChargingStationStorage {
dbParams.skip = Utils.checkRecordSkip(dbParams.skip);
// Create Aggregation
const aggregation = [];
+ // User coordinates
+ if (Utils.containsGPSCoordinates(params.userGPSCoordinates)) {
+ aggregation.push({
+ $geoNear: {
+ near: {
+ type: 'Point',
+ coordinates: params.userGPSCoordinates
+ },
+ distanceField: 'distanceMeters',
+ maxDistance: params.userGPSMaxDistanceMeters > 0 ? params.userGPSMaxDistanceMeters : Constants.MAX_GPS_DISTANCE_METERS,
+ spherical: true
+ }
+ });
+ }
// Set the filters
- const filters: any = { $or: DatabaseUtils.getNotDeletedFilter() };
+ const filters: any = {
+ $or: DatabaseUtils.getNotDeletedFilter()
+ };
// Filter
if (params.search) {
filters.$or = [
@@ -152,16 +169,16 @@ export default class ChargingStationStorage {
}
// Add Charging Station inactive flag
DatabaseUtils.pushChargingStationInactiveFlag(aggregation);
- // Add in aggregation
- aggregation.push({
- $match: filters
- });
// Include deleted charging stations if requested
if (params.includeDeleted) {
filters.$or.push({
'deleted': true
});
}
+ // Add in aggregation
+ aggregation.push({
+ $match: filters
+ });
// Connector Status
if (params.connectorStatuses) {
filters['connectors.status'] = { $in: params.connectorStatuses };
@@ -455,7 +472,8 @@ export default class ChargingStationStorage {
(connector) => ChargingStationStorage.connector2connectorMDB(connector)) : [],
chargePoints: chargingStationToSave.chargePoints ? chargingStationToSave.chargePoints.map(
(chargePoint) => ChargingStationStorage.chargePoint2ChargePointMDB(chargePoint)) : [],
- coordinates: chargingStationToSave.coordinates,
+ coordinates: Utils.containsGPSCoordinates(chargingStationToSave.coordinates) ? chargingStationToSave.coordinates.map(
+ (coordinate) => Utils.convertToFloat(coordinate)) : [],
remoteAuthorizations: chargingStationToSave.remoteAuthorizations ? chargingStationToSave.remoteAuthorizations : [],
currentIPAddress: chargingStationToSave.currentIPAddress,
capabilities: chargingStationToSave.capabilities,

MODIFY
src/storage/mongodb/CompanyStorage.ts
src/storage/mongodb/CompanyStorage.ts
@@ -56,7 +56,17 @@ export default class CompanyStorage {
issuer: Utils.convertToBoolean(companyToSave.issuer),
};
if (companyToSave.address) {
- companyMDB.address = companyToSave.address;
+ companyMDB.address = {
+ address1: companyToSave.address.address1,
+ address2: companyToSave.address.address2,
+ postalCode: companyToSave.address.postalCode,
+ city: companyToSave.address.city,
+ department: companyToSave.address.department,
+ region: companyToSave.address.region,
+ country: companyToSave.address.country,
+ coordinates: Utils.containsGPSCoordinates(companyToSave.address.coordinates) ? companyToSave.address.coordinates.map(
+ (coordinate) => Utils.convertToFloat(coordinate)) : [],
+ };
}
// Add Last Changed/Created props
DatabaseUtils.addLastChangedCreatedProps(companyMDB, companyToSave);

MODIFY
src/storage/mongodb/MongoDBStorage.ts
src/storage/mongodb/MongoDBStorage.ts
@@ -67,7 +67,8 @@ export default class MongoDBStorage {
const collections = await this.db.listCollections({ name: name }).toArray();
// Users
await this.handleIndexesInCollection(collections, tenantID, 'users', [
- { fields: { email: 1 }, options: { unique: true } }
+ { fields: { email: 1 }, options: { unique: true } },
+ { fields: { 'address.coordinates': '2dsphere' } },
]);
await this.handleIndexesInCollection(collections, tenantID, 'eulas');
// Logs
@@ -137,6 +138,22 @@ export default class MongoDBStorage {
{ fields: { transactionId: 1 } },
{ fields: { siteAreaID: 1, startedAt: 1 } }
]);
+ // Companies
+ await this.handleIndexesInCollection(collections, tenantID, 'companies', [
+ { fields: { 'address.coordinates': '2dsphere' } },
+ ]);
+ // Sites
+ await this.handleIndexesInCollection(collections, tenantID, 'sites', [
+ { fields: { 'address.coordinates': '2dsphere' } },
+ ]);
+ // Site Area
+ await this.handleIndexesInCollection(collections, tenantID, 'siteareas', [
+ { fields: { 'address.coordinates': '2dsphere' } },
+ ]);
+ // Charging Stations
+ await this.handleIndexesInCollection(collections, tenantID, 'chargingstations', [
+ { fields: { coordinates: '2dsphere' } },
+ ]);
Logging.logDebug({
tenantID: tenantID,
action: ServerAction.MONGO_DB,

MODIFY
src/storage/mongodb/SiteAreaStorage.ts
src/storage/mongodb/SiteAreaStorage.ts
@@ -123,7 +123,17 @@ export default class SiteAreaStorage {
numberOfPhases: Utils.convertToInt(siteAreaToSave.numberOfPhases),
};
if (siteAreaToSave.address) {
- siteAreaMDB.address = siteAreaToSave.address;
+ siteAreaMDB.address = {
+ address1: siteAreaToSave.address.address1,
+ address2: siteAreaToSave.address.address2,
+ postalCode: siteAreaToSave.address.postalCode,
+ city: siteAreaToSave.address.city,
+ department: siteAreaToSave.address.department,
+ region: siteAreaToSave.address.region,
+ country: siteAreaToSave.address.country,
+ coordinates: Utils.containsGPSCoordinates(siteAreaToSave.address.coordinates) ? siteAreaToSave.address.coordinates.map(
+ (coordinate) => Utils.convertToFloat(coordinate)) : [],
+ };
}
// Add Last Changed/Created props
DatabaseUtils.addLastChangedCreatedProps(siteAreaMDB, siteAreaToSave);

MODIFY
src/storage/mongodb/SiteStorage.ts
src/storage/mongodb/SiteStorage.ts
@@ -256,12 +256,24 @@ export default class SiteStorage {
// Properties to save
const siteMDB: any = {
_id: siteFilter._id,
- address: siteToSave.address,
issuer: Utils.convertToBoolean(siteToSave.issuer),
companyID: Utils.convertToObjectID(siteToSave.companyID),
autoUserSiteAssignment: Utils.convertToBoolean(siteToSave.autoUserSiteAssignment),
name: siteToSave.name,
};
+ if (siteToSave.address) {
+ siteMDB.address = {
+ address1: siteToSave.address.address1,
+ address2: siteToSave.address.address2,
+ postalCode: siteToSave.address.postalCode,
+ city: siteToSave.address.city,
+ department: siteToSave.address.department,
+ region: siteToSave.address.region,
+ country: siteToSave.address.country,
+ coordinates: Utils.containsGPSCoordinates(siteToSave.address.coordinates) ? siteToSave.address.coordinates.map(
+ (coordinate) => Utils.convertToFloat(coordinate)) : [],
+ };
+ }
// Add Last Changed/Created props
DatabaseUtils.addLastChangedCreatedProps(siteMDB, siteToSave);
// Modify and return the modified document

MODIFY
src/storage/mongodb/UserStorage.ts
src/storage/mongodb/UserStorage.ts
@@ -227,7 +227,7 @@ export default class UserStorage {
}
// Properties to save
// eslint-disable-next-line prefer-const
- let userMDB = {
+ const userMDB: any = {
_id: userToSave.id ? Utils.convertToObjectID(userToSave.id) : new ObjectID(),
issuer: Utils.convertToBoolean(userToSave.issuer),
name: userToSave.name,
@@ -236,7 +236,6 @@ export default class UserStorage {
phone: userToSave.phone,
mobile: userToSave.mobile,
locale: userToSave.locale,
- address: userToSave.address,
iNumber: userToSave.iNumber,
costCenter: userToSave.costCenter,
notifications: {
@@ -260,6 +259,19 @@ export default class UserStorage {
},
deleted: Utils.objectHasProperty(userToSave, 'deleted') ? userToSave.deleted : false
};
+ if (userToSave.address) {
+ userMDB.address = {
+ address1: userToSave.address.address1,
+ address2: userToSave.address.address2,
+ postalCode: userToSave.address.postalCode,
+ city: userToSave.address.city,
+ department: userToSave.address.department,
+ region: userToSave.address.region,
+ country: userToSave.address.country,
+ coordinates: Utils.containsGPSCoordinates(userToSave.address.coordinates) ? userToSave.address.coordinates.map(
+ (coordinate) => Utils.convertToFloat(coordinate)) : [],
+ };
+ }
// Check Created/Last Changed By
DatabaseUtils.addLastChangedCreatedProps(userMDB, userToSave);
// Modify and return the modified document

MODIFY
src/types/ChargingStation.ts
src/types/ChargingStation.ts
@@ -52,6 +52,7 @@ export default interface ChargingStation extends CreatedUpdatedProps {
capabilities?: ChargingStationCapabilities;
ocppStandardParameters?: KeyValue[];
ocppVendorParameters?: KeyValue[];
+ distanceMeters?: number;
ocpiData?: {
evse?: OCPIEvse;
};

MODIFY
src/types/requests/HttpChargingStationRequest.ts
src/types/requests/HttpChargingStationRequest.ts
@@ -32,6 +32,8 @@ export interface HttpChargingStationsRequest extends HttpDatabaseRequest {
SiteAreaID?: string;
IncludeDeleted?: boolean;
ErrorType?: string;
+ UserGPSCoordinates?: number[];
+ UserGPSMaxDistanceMeters?: number;
}
export interface HttpChargingStationParamsUpdateRequest {

MODIFY
src/utils/Constants.ts
src/utils/Constants.ts
@@ -73,6 +73,10 @@ export default class Constants {
public static readonly MAX_DATE = new Date('9999-12-31Z23:59:59:999');
public static readonly MIN_DATE = new Date('1970-01-01Z00:00:00:000');
+ public static readonly REGEX_VALIDATION_LATITUDE = /^-?([1-8]?[1-9]|[1-9]0)\.{0,1}[0-9]*$/;
+ public static readonly REGEX_VALIDATION_LONGITUDE = /^-?([1]?[0-7][0-9]|[1]?[0-8][0]|[1-9]?[0-9])\.{0,1}[0-9]*$/;
+ public static readonly MAX_GPS_DISTANCE_METERS = 20000;
+
public static readonly MONGO_USER_MASK = {
'_id': 0,
'__v': 0,

MODIFY
src/utils/Utils.ts
src/utils/Utils.ts
-import { AnalyticsSettingsType, AssetSettingsType, BillingSettingsType, PricingSettingsType, RefundSettingsType, RoamingSettingsType, SettingDBContent, SmartChargingContentType } from '../types/Setting';
-import { Car, CarCatalog, CarType } from '../types/Car';
-import { ChargePointStatus, OCPPProtocol, OCPPVersion } from '../types/ocpp/OCPPServer';
-import ChargingStation, { ChargePoint, Connector, ConnectorCurrentLimitSource, CurrentType } from '../types/ChargingStation';
-import Transaction, { InactivityStatus } from '../types/Transaction';
-import User, { UserRole, UserStatus } from '../types/User';
-
-import { ActionsResponse } from '../types/GlobalType';
-import AppError from '../exception/AppError';
-import Asset from '../types/Asset';
+import bcrypt from 'bcryptjs';
+import crypto from 'crypto';
+import { Request } from 'express';
+import fs from 'fs';
+import http from 'http';
+import _ from 'lodash';
+import moment from 'moment';
+import { ObjectID } from 'mongodb';
+import passwordGenerator from 'password-generator';
+import path from 'path';
+import tzlookup from 'tz-lookup';
+import url from 'url';
+import { v4 as uuid } from 'uuid';
+import validator from 'validator';
import Authorizations from '../authorization/Authorizations';
+import AppError from '../exception/AppError';
import BackendError from '../exception/BackendError';
+import TenantStorage from '../storage/mongodb/TenantStorage';
+import UserStorage from '../storage/mongodb/UserStorage';
+import Address from '../types/Address';
+import Asset from '../types/Asset';
+import { Car, CarCatalog, CarType } from '../types/Car';
import { ChargingProfile } from '../types/ChargingProfile';
+import ChargingStation, { ChargePoint, Connector, ConnectorCurrentLimitSource, CurrentType } from '../types/ChargingStation';
import Company from '../types/Company';
-import Configuration from './Configuration';
import ConnectorStats from '../types/ConnectorStats';
-import Constants from './Constants';
-import Cypher from './Cypher';
+import { ActionsResponse } from '../types/GlobalType';
import { HTTPError } from '../types/HTTPError';
-import Logging from './Logging';
import OCPIEndpoint from '../types/ocpi/OCPIEndpoint';
-import { ObjectID } from 'mongodb';
-import { Request } from 'express';
+import { ChargePointStatus, OCPPProtocol, OCPPVersion } from '../types/ocpp/OCPPServer';
import { ServerAction } from '../types/Server';
+import { AnalyticsSettingsType, AssetSettingsType, BillingSettingsType, PricingSettingsType, RefundSettingsType, RoamingSettingsType, SettingDBContent, SmartChargingContentType } from '../types/Setting';
import Site from '../types/Site';
import SiteArea from '../types/SiteArea';
import Tag from '../types/Tag';
import Tenant from '../types/Tenant';
import TenantComponents from '../types/TenantComponents';
-import TenantStorage from '../storage/mongodb/TenantStorage';
-import UserStorage from '../storage/mongodb/UserStorage';
+import Transaction, { InactivityStatus } from '../types/Transaction';
+import User, { UserRole, UserStatus } from '../types/User';
import UserToken from '../types/UserToken';
-import _ from 'lodash';
-import bcrypt from 'bcryptjs';
-import crypto from 'crypto';
-import fs from 'fs';
-import http from 'http';
-import moment from 'moment';
-import passwordGenerator from 'password-generator';
-import path from 'path';
-import tzlookup from 'tz-lookup';
-import url from 'url';
-import { v4 as uuid } from 'uuid';
-import validator from 'validator';
+import Configuration from './Configuration';
+import Constants from './Constants';
+import Cypher from './Cypher';
+import Logging from './Logging';
+
const MODULE_NAME = 'Utils';
@@ -1116,6 +1117,25 @@ export default class Utils {
sc && sc.length >= Constants.PWD_SPECIAL_MIN_COUNT;
}
+ public static containsAddressGPSCoordinates(address: Address): boolean {
+ // Check if GPS are available
+ if (address && Utils.containsGPSCoordinates(address.coordinates)) {
+ return true;
+ }
+ return false;
+ }
+
+ public static containsGPSCoordinates(coordinates: number[]): boolean {
+ // Check if GPs are available
+ if (coordinates && coordinates.length === 2 && coordinates[0] && coordinates[1]) {
+ // Check Longitude & Latitude
+ if (new RegExp(Constants.REGEX_VALIDATION_LONGITUDE).test(coordinates[0].toString()) &&
+ new RegExp(Constants.REGEX_VALIDATION_LATITUDE).test(coordinates[1].toString())) {
+ return true;
+ }
+ }
+ return false;
+ }
public static generatePassword(): string {
let password = '';

Add GPS capabilities to charging stations + 2d indexes