src/components/models/bike-model.js
import Model from './model';
import Database from '../../util/database';
import ImageUtil from '../../util/imageutility';
import TimeUtil from '../../util/timeutility';
import PersistStorage from '../../util/persistentstorage';
import AuthState from '../../util/authenticationstate';
const BIKE_TYPE = ImageUtil.getTypes().BIKE;
/**
* Class for the bike model to be used by the BikePresenter and AddBikePresenter
* @extends Model
*/
class BikeModel extends Model {
/**
* Creates an instance of BikeModel. Sets the default callback, creates an observerlist,
* and registers an on read from the database.
*
* @constructor
*/
constructor() {
super();
this._callback = this._defaultCallback;
this.listener = null;
this._data = {data: []};
this._createObserverList();
this._registerDBReadListener();
this._checkForLocalData('data'); // Offline equivalent of _registerDBReadListener, only useful if the user hasn't logged out
}
/**
* Delete a bike from the database
*
* @param {string} id - A bike id to delete
* @param {Function} callback - A function to call when remove succeeds or fails
*/
deleteBikeByID(id, callback) {
const { index } = this._bikeIDExists(id);
const bike = JSON.parse(JSON.stringify(this._data.data[index]));
this._data.data = this._data.data.filter((el) => el.id !== id);
Database.removeBikeItem(id, (resultItem) => {
Database.removeImages(bike.thumbnail, (resultImage) => {
callback(resultItem && resultImage);
this._notifyAll(null);
});
});
}
/**
* Returns the bike data by providing the bike ID.
*
* @param {string} id - A bike ID
* @return {Object} The data coresponding to the bike id
*/
_getBikeByID = (id) => {
return this._data.data.filter((el) => {
return el.id === id;
})[0];
}
/**
* Default callback
*/
_defaultCallback(message) {
console.log(message);
}
/**
* Set the model's callback to a new callback. This callback can be used anywhere and is usually passed in from a presenter.
*
* @param {Function} callback - A callback to run when certain code is executed
*/
setCallback(callback) {
this._callback = callback;
}
/**
* Register an 'on' read from the database to get updates anytime data changes in the database.
*/
_registerDBReadListener() {
this.listener = Database.readBikeDataOn((snapshot) => {
// console.log(snapshot.val());
this._insertDataOnRead(snapshot.val());
this._notifyAll(this._data); // Don't supply data to force a refresh by the presenter
});
}
/**
* Toggle the database listener off and then on again to get the data again.
* TODO : Better method to do this?
*/
toggleListeners() {
if (this.listener != null) {
Database.readBikeDataOff(this.listener);
this._registerDBReadListener();
}
}
/**
* Get method for presenters to get data.
*
* @return {Object} data stored in the model
*/
get() {
return {...this._data} // Immutable
}
/**
* Update method for presenters to update the model's data. Datetime and Owner are handled in database class.
* Callback needs to be set with BikeM.setCallback(callback); callback takes in 1 parameter.
*
* @param {Object} newData - New data to add
*/
update(newData) {
// Add ID here
if (newData.data.id === '' || newData.data.id === undefined) {
console.log('Fetching new ID...');
newData.data.id = Database.getNewBikeID();
}
newData.data.milliseconds = TimeUtil.getDateTime();
try {
const {exists, index} = this._bikeDataExists(newData);
if (exists && this._checkImages(index, newData.data.thumbnail)) {
newData.data.thumbnail = this._removeIllustrationKey(newData.data.thumbnail);
this._insertDataOnUpdate(newData, exists, index);
this._editExistingInDatabase(newData.data, (result) => {this._callback(true); this._notifyAll(this._data);});
} else {
const { BikeImages } = Database.getImageFolders();
newData.data.stolen = false // true: if bike is stolen; false: if the bike is not stolen or the owner has marked it as found
newData.data.found = false // true: if stolen=true && bike was found; false: if stolen=false || (stolen=true && bike is not found)
// Write to database
this._writeImageToDBStorage(newData.data.id, newData.data.thumbnail, BikeImages, (uploaded_images, num_defaults) => {
newData.data.thumbnail = uploaded_images;
// Check if there's actually images
if (!ImageUtil.checkImageListValid(uploaded_images)) {
this._callback(false);
return;
}
const {exists, index} = this._bikeDataExists(newData); // Need to recompute each time because would have changed on second image
this._insertDataOnUpdate(newData, exists, index);
// console.log(result);
// console.log(this._data.data);
// If the number of defaults in the original amount is the same
const finishCallback = ImageUtil.checkNumDefaults(BIKE_TYPE, num_defaults, uploaded_images) ? (result) => {this._callback(result); this._notifyAll(this._data);} : (_) => 'default';
// variable 'result' - true: ID was found in database so edit it; false: ID not found in database so add it
// const dbCall = result ? Database.editBikeData : Database.writeBikeData;
// this._addToDatabase(dbCall, newData.data, finishCallback);
const funcCall = exists ? this._editExistingInDatabase : this._writeNewInDatabase;
funcCall(newData.data, finishCallback);
}, this._callback);
}
} catch (error) {
console.log(error);
this._callback(false);
}
}
/**
* Checks if there are new images in the bike stored vs what was passed in.
*
* @param {Number} index - The index of the bike in the local data
* @param {List} thumbnails - A list of thumbnails
* @return {Boolean} true: If the thumbnails are the same; false: If the thumbnails are different or if the bike doesn't exist
*/
_checkImages(index, thumbnails) {
if (index >= 0) {
const bike = JSON.parse(JSON.stringify(this._data.data[index]));
const bikeThumbnails = ImageUtil.addRemainingDefaults(ImageUtil.getTypes().BIKE, bike.thumbnail);
const paramThumbnailsDefaults = ImageUtil.addRemainingDefaults(ImageUtil.getTypes().BIKE, thumbnails);
const bikeThumbnailsNoIllustration = this._removeIllustrationKey(bikeThumbnails);
const thumbnailsNoIllustration = this._removeIllustrationKey(paramThumbnailsDefaults);
// console.log(bikeThumbnailsNoIllustration, thumbnailsNoIllustration);
return JSON.stringify(bikeThumbnailsNoIllustration) === JSON.stringify(thumbnailsNoIllustration);
} else {
return false; // Bike does not exist
}
}
/**
* Removes the illustration key from the object and only adds the actual link.
*
* @param {List} thumbnails - A list of thumbnail objects with the property 'illustration'
* @return {List} A list of thumbnails
*/
_removeIllustrationKey(thumbnails) {
let new_thumbnails = [];
const DEFAULT_IMAGE = ImageUtil.getDefaultImage(ImageUtil.getTypes().BIKE);
for (let i=0; i < thumbnails.length; i++) {
if (thumbnails[i] === DEFAULT_IMAGE || (thumbnails[i].hasOwnProperty('illustration') && thumbnails[i].illustration === DEFAULT_IMAGE)) {
continue;
}
if (thumbnails[i].hasOwnProperty('illustration')) {
new_thumbnails.push(thumbnails[i].illustration);
} else {
new_thumbnails.push(thumbnails[i]);
}
}
return new_thumbnails;
}
/**
* Write the image to the firebase storage and call the callbacks with the urls that were defined.
*
* @param {Number} id - The id of the bike corresponding to the image
* @param {List} images - A list of objects with the property 'illustration'
* @param {string} imagesFolder - The folder to upload images to
* @param {Function} onSuccess - A callback to call when an image has been successfully uploaded
* @param {Function} onError - A callback to call when an image has failed to upload
*/
_writeImageToDBStorage(id, images, imagesFolder, onSuccess, onError) {
const FILE_EXTENSION = '.jpg';
let uploaded_pictures = [];
let count_default = 0;
// If there are no images, return
if (!ImageUtil.checkImageListValid(images)) {
onError(false);
return;
}
for (let i=0; i < images.length; i++) {
// Check if there's a default image, if so, skip it
if (ImageUtil.isDefaultImage(BIKE_TYPE, images[i].illustration)) {
count_default++;
continue;
} else if (ImageUtil.isAlreadyUploaded(images[i].illustration)) {
uploaded_pictures.push(images[i].illustration);
continue;
}
// Name of file is the name of the index and file extension
const filename = i + ImageUtil.getFileExtension();
// Write image to database
Database.writeImage(id, images[i].illustration, filename, imagesFolder, (url) => {
uploaded_pictures.push(url);
onSuccess(uploaded_pictures, count_default);
return url;
}, (error) => {
console.log(error);
onError(false);
});
}
}
// Could generalize _writeNewInDatabase and _editExistingInDatabase into one function
// But there was a problem with assigning the functions to variables so just went with this instead
/**
* Write new data in database and call the function callback depending on if it was successful or not.
*
* @param {Object} newData - Data to be written to the database
* @param {Function} callback - A function to call on the success or failure of the call
*/
_writeNewInDatabase(newData, callback) {
return Database.writeBikeData(newData, (data) => {
// console.log(data);
callback(typeof data !== 'undefined' && data !== undefined);
// return typeof data !== 'undefined' && data !== undefined
// this._callback(typeof data !== 'undefined' && data !== undefined);
},(error) => {
console.log(error);
callback(false);
// this._callback(false);
});
}
/**
* Overwrite existing data in database and call the function callback depending on if it was successful or not.
*
* @param {Object} newData - Data to be written to the database
* @param {Function} callback - A function to call on the success or failure of the call
*/
_editExistingInDatabase(newData, callback) {
return Database.editBikeData(newData, (data) => {
// console.log(data);
callback(typeof data !== 'undefined' && data !== undefined);
// return typeof data !== 'undefined' && data !== undefined;
// this._callback(typeof data !== 'undefined' && data !== undefined);
},(error) => {
console.log(error);
callback(false);
// this._callback(false);
});
}
/**
* Insert data into the data object on an update trigger (from Presenter).
*
* @param {Object} newData - New data passed in, of the form : {data: []}
* @param {Boolean} exists - If the bike already exists
* @param {Number} index - The index of the bike. Positive if it exists, negative if it doesn't
* @return {Boolean} true: Data was an edited value; false: Data was a new value
*/
_insertDataOnUpdate(newData, exists, index) {
let i = 0;
// console.log(newData, exists, index);
// If only one piece, just insert it
if (this._data.data.length === 0) {
this._data.data.push(newData.data);
return false;
}
if (exists && index >= 0) {
this._data.data[index] = newData.data; // Data found, overwrite
return true;
} else {
this._data.data.push(newData.data); // Appends to the list - Use this if only a single piece of data is passed in
return false; // Data not found
}
}
/**
* Checks if the bike exists based on the data of the bike.
*
* @param {Object} bikeData - The data to check
* @return {Boolean, Number} exists: true: If the bike exists; false: otherwise. index - The index of the bike if it exists, -1 if not
*/
_bikeDataExists(bikeData) {
return this._bikeIDExists(bikeData.data.id)
}
/**
* Checks if the bike exists based on the id.
*
* @param {string} id - The id of a bike
* @return {Boolean, Number} exists: true: If the bike exists; false: otherwise. index - The index of the bike if it exists, -1 if not
*/
_bikeIDExists(id) {
let i = 0;
// Loop through and see if there's a match, probably a better way to do this with indexOf or filter
while (i < this._data.data.length && this._data.data[i].id !== id) {
i++;
}
const exists = i !== this._data.data.length;
const index = exists ? i : -1;
return { exists, index };
}
/**
* Checks if an object has a certain property.
*
* @param {Object} obj - An object to check
* @param {string} property - The name of a property
* @return {Boolean} true: if the object has the property; false: otherwise
*/
_hasProperty(obj, property) {
return obj.hasOwnProperty(property);
}
/**
* Insert data into the data object on a read from the database.
*
* @param {Object} databaseData - An objects of objects containing data from the database.
*/
_insertDataOnRead(databaseData) {
let tempData = {data:[]};
let dataID = 0;
const currentUser = AuthState.getCurrentUserID();
if (databaseData != null) { // Check if there are objects in the database
for (let val in databaseData) {
if (!this._hasProperty(databaseData[val], 'id')) { // If it doesn't have an id, skip it because it isn't valid
continue;
}
// Bike page only displays current user
if (currentUser == null || currentUser != databaseData[val].owner) {
continue;
}
// Arrays don't show up in firebase so we manually have to insert to make sure we don't get errors in the view
if (!this._hasProperty(databaseData[val], 'colour')) {
databaseData[val].colour = [];
}
if (!this._hasProperty(databaseData[val], 'thumbnail')) {
databaseData[val].thumbnail = [];
}
databaseData[val].dataID = dataID; // Assign a dataID which is just an incremental temporary value
tempData.data.push(databaseData[val]);
dataID++;
}
this._data = tempData;
this._saveDataToLocalStorage('data', this._data);
}
}
/**
* Save data to local storage.
*
* @param {string} key - A key to store the data under
* @param {Object} data - The data to store
*/
async _saveDataToLocalStorage(key, data) {
await PersistStorage.retrieveData(key, (retrievedData) => {
if (retrievedData != [] && retrievedData != null && retrievedData != undefined) {
// We only want to store data if there isn't already data.
PersistStorage.storeData(key, JSON.stringify(data), (error) => {
console.log(error);
});
}
}, (error) => {
console.log(error);
});
}
/**
* Checks if local data is stored, and if so, update the data. This is because we don't wait for this data
* when authenticating and the user can see their bikes if offline.
*
* @param {string} key - Key to get data for
*/
async _checkForLocalData(key) {
await PersistStorage.retrieveData(key, (data) => {
if (data != null) {
this._data = JSON.parse(data);
this._notifyAll(null);
}
}, (error) => {
console.log(error);
});
}
}
export default BikeModel;