Browse Source

Merge branch 'release/v0.6.3'

Jérôme BUISINE 10 months ago
parent
commit
461a1e9c37

+ 1 - 0
.gitignore

@@ -28,3 +28,4 @@ yarn-error.log*
 /data/*
 /doc
 /experimentConfig.js
+/results

+ 3 - 0
config.js

@@ -38,5 +38,8 @@ export const sceneFileNameBlackList = ['config', 'seuilExpe', extractsDirName]
 // Cron time for extracts deletion (every day at 03:00 AM)
 export const deleteExtractsCronTime = '0 3 * * *'
 
+// Cron time for stats estimation (every day at 03:00 AM)
+export const expeStatsCronTime = '0 3 * * *'
+
 // Logger configurations (Default application, WebSocket, Database)
 export { logger, dbLogger }

+ 4 - 0
config.messagesId.js

@@ -5,6 +5,9 @@
 // Message ID for data collection
 export const COLLECT_DATA = 'COLLECT_DATA'
 
+// newsletter has been asked
+export const NEWS = 'NEWSLETTER'
+
 // Message IDs for experiments events
 export const EXPERIMENT = {
   // An experiment was started
@@ -15,6 +18,7 @@ export const EXPERIMENT = {
 
   // An experiment was validated
   VALIDATED: 'EXPERIMENT_VALIDATED'
+
 }
 
 export default { COLLECT_DATA, EXPERIMENT }

+ 104 - 0
expeStats.js

@@ -0,0 +1,104 @@
+'use strict'
+
+// import { experiments } from './experimentConfig'
+const config = require('./experimentConfig')
+
+const fs = require('fs-extra')
+
+const winston = require('winston')
+const execSync = require('child_process').execSync
+
+// get whitelist scene for MatchExtractsWithReference experiment
+const scenes = config.experiments.MatchExtractsWithReference.availableScenes.whitelist
+
+// File logger configuration
+const fileLogger = winston.createLogger({
+  level: 'info',
+  format: winston.format.json(),
+  transports: [
+    new winston.transports.File({ filename: 'logs/expeStats.log' }),
+    new winston.transports.File({ filename: 'logs/expeStats.error.log', level: 'error' }),
+    new winston.transports.Console({
+      level: 'debug',
+      handleExceptions: true,
+      format: winston.format.json()
+    })
+  ],
+  exitOnError: false
+})
+
+const setup = async (logToFile = false) => {
+  if (logToFile) fileLogger.info({ log: 'Start extraction of data from mongo for `MatchExtractsExperiments`.', date: new Date() })
+
+  execSync('python utils/extract_experiment.py', { encoding: 'utf-8' })
+  if (logToFile) fileLogger.info({ log: 'Mongo extraction done', date: new Date() })
+  execSync('python utils/extract_stats_freq_and_min_all.py --file results/experiments_results.json --output results/match_extracts_stats.csv', { encoding: 'utf-8' })
+  if (logToFile) fileLogger.info({ log: 'Stats computation done, need to create probability for each scene', date: new Date() })
+
+  // read extracted stats in order to compute probabilities
+  let statsPath = 'results/match_extracts_stats.csv'
+  let buffer = fs.readFileSync(statsPath)
+  let lines = buffer.toString().split('\n')
+
+  let stats = {}
+  let nUsers = 0
+
+  for (let l of lines) {
+    if (l.length > 0) {
+      // extract data from csv file
+      let data = l.split(';')
+
+      // data[0] contains scene name
+      // data[1] contains number of users who do this scene
+      let u = Number(data[1])
+      stats[String(data[0])] = u
+      nUsers += u
+    }
+  }
+
+  // start computing probabilities
+  let probabilities = {}
+  let probsArr = []
+  let nUnknownScenes = 0
+
+  // based on white list
+  for (let s of scenes) {
+    if (s in stats) {
+      probabilities[s] = stats[s] / nUsers
+
+      probsArr.push(probabilities[s])
+    }
+    else {
+      nUnknownScenes += 1
+    }
+  }
+
+  // normalize probabilities
+  let currentMax = Math.max(...probsArr)
+
+  for (let s of scenes) {
+    // if new scene
+    if (!(s in stats)) {
+      // multiply prob criteria based on number of unknown scene
+      // => increase chance for user to pass this scene
+      probabilities[s] = (1 + (1 - (nUnknownScenes / scenes.length))) * currentMax
+      probsArr.push(probabilities[s])
+    }
+  }
+
+  // get sum of current probs
+  let sum = probsArr.reduce((a, b) => a + b, 0)
+
+  for (let s of scenes) {
+    probabilities[s] /= sum
+  }
+
+  if (logToFile) fileLogger.info({ log: 'New probabilities extracted:' + JSON.stringify(probabilities, null, 3), date: new Date() })
+
+  fs.writeFile('results/match_extracts_probs.json', JSON.stringify(probabilities, null, 3))
+}
+
+// Execute setup command
+setup()
+
+module.exports = { setup, expeStatsServiceLogger: fileLogger }

+ 18 - 1
experimentConfig.default.js

@@ -49,6 +49,8 @@ export const experiments = {
         y: 4
       }
     },
+    calibrationScene: '50_shades_of_grey',
+    showCalibrationEvery: 5,
     scenesConfig: {},
     availableScenes: {
       whitelist: [
@@ -56,6 +58,7 @@ export const experiments = {
         'p3d_bunny-fur-view0_part6',
         'p3d_car2-view0_part6',
         'p3d_caustic-view0_part6',
+        'p3d_chopper-titan-view0_part6',
         'p3d_coffee-splash-view0_part6',
         'p3d_cornel-box-view0_part6',
         'p3d_crown-view0_part6',
@@ -81,9 +84,9 @@ export const experiments = {
         'p3d_pavilion-night-view0_part6',
         'p3d_pavilion-night-view1_part6',
         'p3d_pavilion-night-view2_part6',
+        'p3d_sportscar-view0_part6',
         'p3d_staircase-view1_part6',
         'p3d_staircase2-view0_part6',
-        'p3d_staircase2-view1_part6',
         'p3d_tt-view0_part6',
         'p3d_vw-van-view0_part6'
       ],
@@ -99,6 +102,8 @@ export const experiments = {
     mixins: [mixins.ExperimentBaseAreSameImages],
     defaultConfig: {},
     scenesConfig: {},
+    calibrationScene: '50_shades_of_grey',
+    showCalibrationEvery: 5,
     availableScenes: {
       whitelist: null,
       blacklist: null
@@ -108,6 +113,8 @@ export const experiments = {
     mixins: [mixins.ExperimentBaseAreSameImages],
     defaultConfig: {},
     scenesConfig: {},
+    calibrationScene: '50_shades_of_grey',
+    showCalibrationEvery: 5,
     availableScenes: {
       whitelist: null,
       blacklist: null
@@ -117,6 +124,8 @@ export const experiments = {
     mixins: [mixins.ExperimentBaseAreSameImages, mixins.ExperimentBaseExtracts],
     defaultConfig: {},
     scenesConfig: {},
+    calibrationScene: '50_shades_of_grey',
+    showCalibrationEvery: 5,
     availableScenes: {
       whitelist: null,
       blacklist: null
@@ -126,6 +135,8 @@ export const experiments = {
     mixins: [mixins.ExperimentBase],
     defaultConfig: {},
     scenesConfig: {},
+    calibrationScene: '50_shades_of_grey',
+    showCalibrationEvery: 5,
     availableScenes: {
       whitelist: ['Appart1opt02', 'EchecsBas'],
       blacklist: null
@@ -142,6 +153,8 @@ export const experiments = {
       }
     },
     scenesConfig: {},
+    calibrationScene: '50_shades_of_grey',
+    showCalibrationEvery: 5,
     availableScenes: {
       whitelist: ['Appart1opt02', 'EchecsBas'],
       blacklist: null
@@ -158,6 +171,8 @@ export const experiments = {
       }
     },
     scenesConfig: {},
+    calibrationScene: '50_shades_of_grey',
+    showCalibrationEvery: 5,
     availableScenes: {
       whitelist: null,
       blacklist: null
@@ -169,6 +184,8 @@ export const experiments = {
       lockConfig: true
     },
     scenesConfig: {},
+    calibrationScene: '50_shades_of_grey',
+    showCalibrationEvery: 5,
     availableScenes: {
       whitelist: ['50_shades_of_grey'],
       blacklist: null

+ 5 - 1
index.js

@@ -4,10 +4,14 @@ import { CronJob } from 'cron'
 
 import server from './server'
 import { setup as cleanExtracts, extractsRemoverServiceLogger } from './cleanExtracts'
-import { imagesPath, deleteExtractsCronTime } from './config'
+import { setup as expeStats, expeStatsServiceLogger } from './expeStats'
+import { imagesPath, deleteExtractsCronTime, expeStatsCronTime } from './config'
 
 const argv = process.argv.slice(2)
 
+new CronJob(expeStatsCronTime, () => expeStats(true), null, true, null, null, false)
+expeStatsServiceLogger.info('Started the expe stats service.')
+
 // Start the extracts remover service
 if (!argv.includes('--no-delete')) { /* eslint no-new: 0 */
   new CronJob(deleteExtractsCronTime, () => cleanExtracts(imagesPath, false, true), null, true, null, null, false)

+ 4 - 0
server/database/controllers/Data.js

@@ -33,4 +33,8 @@ export default class Data {
   static find(dataId) {
     return DataModel.findById(dataId)
   }
+
+  static findOne(data) {
+    return DataModel.findOne(data)
+  }
 }

+ 81 - 0
server/routes/experimentCheck.js

@@ -0,0 +1,81 @@
+'use strict'
+
+import express from 'express'
+import boom from '@hapi/boom'
+
+import { TEST_MODE } from '../../config'
+import DataController from '../database/controllers/Data'
+import { asyncMiddleware, checkRequiredParameters } from '../functions'
+
+const router = express.Router()
+
+/**
+ * @api {post} /experimentCheck /experimentCheck
+ * @apiVersion 0.1.11
+ * @apiName experimentCheck
+ * @apiGroup API
+ *
+ * @apiDescription Collect user's data
+ *
+ * @apiParam {String} msgId The type of message to store
+ * @apiParam {any} Any data that needs to be stored
+ *
+ * @apiExample Usage example
+ * curl -i -L -H "Content-Type: application/json" -X POST "https://diran.univ-littoral.fr/api/experimentCheck" -d {"msgId":"test","msg":{}}
+ *
+ * @apiSuccessExample {string} Success response example
+ * HTTP/1.1 204 OK /api/experimentCheck
+ *
+ * @apiError (Error 4xx) 400_[1] Missing parameter(s)
+ * @apiErrorExample {json} Missing parameter
+ * HTTP/1.1 400 Bad Request
+ * {
+ *   "message": "Missing parameter(s). Required parameters : msgId, msg."
+ * }
+ *
+ * @apiError (Error 4xx) 400_[2] Invalid query parameter
+ * @apiErrorExample {json} Invalid query parameter(s)
+ * HTTP/1.1 400 Bad Request
+ * {
+ *   "message": "Invalid body parameter(s).",
+ *   "data": [
+ *     "\"msgId\" must be a string."
+ *   ]
+ * }
+ *
+ */
+
+router.post('/', asyncMiddleware(async (req, res) => {
+  // Check the request contains all the required body parameters
+  const b = req.body
+  checkRequiredParameters(['msgId', 'msg'], b)
+
+  const {
+    msgId,
+    msg,
+    userId = null,
+    experimentId = null
+  } = req.body
+
+  console.log('Trying to find data')
+
+  // Check there is no errors with parameters
+  if (typeof b.msgId !== 'string')
+    throw boom.badRequest('Invalid body parameter(s).', ['"msgId" must be a string.'])
+
+  let status = 204
+
+  console.log('Trying to find data')
+  // Add data to the database
+  if (!TEST_MODE) {
+    console.log({ msgId, msg, userId, experimentId })
+    const findDoc = await DataController.findOne({ msgId, msg, userId, experimentId })
+    console.log(findDoc)
+    if (findDoc)
+      status = 208
+  }
+
+  res.status(status).send()
+}))
+
+export default router

+ 2 - 0
server/routes/index.js

@@ -8,6 +8,7 @@ import getImageExtracts from './getImageExtracts'
 import ping from './ping'
 import dataCollect from './dataCollect'
 import experimentCollect from './experimentCollect'
+import experimentCheck from './experimentCheck'
 
 const router = express.Router()
 
@@ -17,6 +18,7 @@ router.use('/getImage', getImage)
 router.use('/getImageExtracts', getImageExtracts)
 router.use('/dataCollect', dataCollect)
 router.use('/experimentCollect', experimentCollect)
+router.use('/experimentCheck', experimentCheck)
 router.use('/ping', ping)
 
 export default router

+ 7 - 1
src/components/ExperimentBlock.vue

@@ -15,7 +15,8 @@
           </v-layout>
           -->
 
-          <h2>"{{ $route.meta.fullName }}"</h2>
+          <h2 v-if="runExpe === true">"{{ $route.meta.fullName }}"</h2>
+
           <!-- <h3>{{ sceneName }}</h3> -->
 
           <slot name="header"></slot>
@@ -63,6 +64,11 @@ export default {
       type: String,
       required: false,
       default: null
+    },
+    runExpe: {
+      type: Boolean,
+      required: false,
+      default: true
     }
   }
 }

+ 130 - 0
src/components/ExperimentsComponents/Newsletter.vue

@@ -0,0 +1,130 @@
+<template>
+  <v-flex xs12>
+    <div style="margin-top:10%">
+      <p style="font-size: 1.4em;">Si vous souhaitez être informé des résultats non nomminatifs de ces recherches vous pouvez, sans obligation, laisser votre adresse mail, elle ne sera utilisée que pour vous permettre d'accéder aux résultats de l'étude une fois analysés.</p>
+
+      <v-text-field
+        v-model="email"
+        :rules="emailRules"
+        label="Votre adresse email"
+        required
+      />
+
+      <v-btn @click="addNewsletter" color="success" large>Valider mon adresse</v-btn>
+    </div>
+
+    <div class="text-center">
+      <v-dialog
+        v-model="dialogError"
+        width="500"
+      >
+        <v-card>
+          <v-card-title
+            class="headline lighten-2"
+            primary-title
+          >
+            <p style="font-size:0.8em;">L'adresse email saisie n'est pas correcte.</p>
+          </v-card-title>
+
+          <v-divider />
+
+          <v-card-actions>
+            <v-spacer />
+            <v-btn
+              color="#DF0101"
+              text
+              @click="dialogError = false"
+            >
+              Fermer
+            </v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-dialog>
+    </div>
+
+    <div class="text-center">
+      <v-dialog
+        v-model="dialogSuccess"
+        width="500"
+      >
+        <v-card>
+          <v-card-title
+            class="headline lighten-4"
+            primary-title
+          >
+            <p style="font-size:0.8em;">Votre adresse email a bien été ajoutée à la newsletter.</p>
+          </v-card-title>
+          <!-- <v-card-text>
+          </v-card-text> -->
+
+          <v-divider />
+
+          <v-card-actions>
+            <v-spacer />
+            <v-btn
+              color="success"
+              text
+              @click="dialogSuccess = false"
+            >
+              Ok
+            </v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-dialog>
+    </div>
+  </v-flex>
+</template>
+
+<script>
+import { NEWS as newsletter } from '@/../config.messagesId'
+import { mapActions } from 'vuex'
+
+export default {
+  name: 'Newsletter',
+  data() {
+    return {
+      emailRules: [
+        v => !!v || 'L\'adresse email est obligatoire',
+        v => /.+@.+/.test(v) || 'L\'email doit être valide'
+      ],
+      email: '',
+      dialogError: false,
+      dialogSuccess: false
+    }
+  },
+  methods: {
+    ...mapActions(['sendMessage', 'checkExistData']),
+
+    async addNewsletter() {
+      // need to check if mail is already there or not
+      const data = {
+        msgId: newsletter,
+        msg: {
+          experimentName: this.experimentName,
+          sceneName: this.sceneName,
+          userEmail: this.email
+        }
+      }
+
+      if (this.validEmail(this.email)) {
+        const findDoc = await this.checkExistData(data)
+
+        console.log(findDoc)
+
+        if (findDoc === 204) {
+          console.log('Create new email input')
+          this.sendMessage(data)
+          this.dialogSuccess = true
+        }
+      }
+      else {
+        this.dialogError = true
+      }
+    },
+    validEmail: function (email) {
+      let re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+      return re.test(email)
+    }
+  }
+}
+</script>

+ 24 - 0
src/config.utils.js

@@ -72,3 +72,27 @@ export const getExperimentSceneList = experimentName => {
 
   return configuredScenesList
 }
+
+/**
+ * Read config to get calibration scene for a given experiment.
+ *
+ * @param {Object} experimentName The selected experiment
+ * @returns {String} The calibration scene name
+ */
+export const getCalibrationScene = experimentName => {
+  const config = getConfigObject(experimentName)
+
+  return config[experimentName].calibrationScene
+}
+
+/**
+ * Read config to get calibration expected frequency for a given experiment.
+ *
+ * @param {Object} experimentName The selected experiment
+ * @returns {Number} The frequency expected
+ */
+export const getCalibrationFrequency = experimentName => {
+  const config = getConfigObject(experimentName)
+
+  return Number(config[experimentName].showCalibrationEvery)
+}

+ 1 - 0
src/functions.js

@@ -4,6 +4,7 @@ export const API_ROUTES = {
 
   dataCollect: `${API_PREFIX}/dataCollect`,
   experimentCollect: `${API_PREFIX}/experimentCollect`,
+  experimentCheck: `${API_PREFIX}/experimentCheck`,
 
   listScenes: `${API_PREFIX}/listScenes`,
 

+ 8 - 1
src/mixins/ExperimentBase.vue

@@ -31,6 +31,13 @@ export default {
     ...mapGetters(['getHostURI', 'getExperimentProgress', 'isExperimentDone'])
   },
   mounted() {
+    // when new session push (if not already there) current user advancement (number of scenes) and redirect to calibration
+    if (window.sessionStorage.getItem('sin3d-nb-scenes') === null) {
+      window.sessionStorage.setItem('sin3d-nb-scenes', 1)
+      this.sceneName = '50_shades_of_grey'
+      this.$router.push(`/experiments/${this.experimentName}/50_shades_of_grey`)
+    }
+
     if (!this.getExperimentProgress({ experimentName: this.experimentName, sceneName: this.sceneName }).experimentName)
       this.sendMessage({
         msgId: experimentMsgId.STARTED,
@@ -41,7 +48,7 @@ export default {
       })
 
     // Check if the experiment is already finished
-    if (this.experimentName && this.sceneName && this.isExperimentDone({ experimentName: this.experimentName, sceneName: this.sceneName })) {
+    if (this.experimentName && this.sceneName && this.isExperimentDone({ experimentName: this.experimentName, sceneName: this.sceneName }) && this.sceneName !== '50_shades_of_grey') {
       console.warn('Redirected from experiment. You can\'t go back in an experiment after finishing it.')
       this.$router.push(`/experiments/${this.experimentName}`)
     }

+ 18 - 6
src/mixins/ExperimentBaseExtracts.vue

@@ -26,7 +26,9 @@ export default {
 
       showHoverBorder: null,
       lockConfig: null,
-      comment: null
+      comment: null,
+      dialogMore: false,
+      dialogLess: false
     }
   },
   computed: {
@@ -116,11 +118,11 @@ export default {
       const qualityIndex = this.qualities.indexOf(quality)
       let action, newQuality
 
-      if (event.button === 0) action = 'needLess' // Left click
-      if (event.button === 2) action = 'needMore' // Right click
+      if (event.button === 0) action = 'needMore' // Left click
+      if (event.button === 2) action = 'needLess' // Right click
 
-      if (event.button === 0 && event.ctrlKey) action = 'need10Less' // ctrl + Right click
-      if (event.button === 2 && event.ctrlKey) action = 'need10More' // ctrl + Left click
+      if (event.button === 0 && event.ctrlKey) action = 'need10More' // ctrl + Right click
+      if (event.button === 2 && event.ctrlKey) action = 'need10Less' // ctrl + Left click
 
       if (action === 'needLess') newQuality = precQuality
       if (action === 'needMore') newQuality = nextQuality
@@ -140,7 +142,17 @@ export default {
       }
 
       // Do not load a new extract if same quality
-      if (newQuality === quality) return
+      if (newQuality === quality) {
+        // display alert once limit is reached
+        if (action === 'needLess' || action === 'need10Less') {
+          this.dialogLess = true
+        }
+        if (action === 'needMore' || action === 'need10More') {
+          this.dialogMore = true
+        }
+
+        return
+      }
 
       // Set loading state
       this.extracts[index].loading = true

+ 2 - 2
src/router/experiments.js

@@ -5,7 +5,7 @@ export default [
     component: () => import('@/views/Experiments/MatchExtractsWithReference'),
     props: true,
     meta: {
-      fullName: 'Cliquer sur les zones de l\'image de gauche (clic droit de la souris)  afin de la faire correspondre à celle de droite'
+      fullName: 'Cliquer sur les zones de l\'image de gauche (clic gauche de la souris)  afin de la faire correspondre à celle de droite'
       // fullName: 'Match extracts qualities to reference image'
     }
   },
@@ -69,7 +69,7 @@ export default [
     component: () => import('@/views/Experiments/CalibrationMeasurement'),
     props: true,
     meta: {
-      fullName: 'Cliquer sur les zones de l\'image de gauche (clic droit de la souris)  afin de faire correspondre la teinte à celle de droite'
+      fullName: 'Cliquer sur les zones de l\'image de gauche (clic gauche de la souris)  afin de faire correspondre la teinte à celle de droite'
     }
   }
 ]

+ 18 - 0
src/store/actions.js

@@ -82,6 +82,24 @@ export default {
     })
   },
 
+  async checkExistData({ state, getters: { getHostURI } }, { msgId, msg = undefined }) {
+    const res = await fetch(`${getHostURI}${API_ROUTES.experimentCheck}`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify({
+        uuid: state.uuid,
+        userId: state.userId,
+        experimentId: state.experimentId,
+        msgId,
+        msg
+      })
+    })
+
+    return res.status
+  },
+
   async loadScenesList({ getters: { isHostConfigured, getHostURI }, commit }) {
     if (!isHostConfigured) throw new Error('Host is not configured.')
 

+ 76 - 12
src/views/ExperimentValidated.vue

@@ -1,8 +1,8 @@
 <template>
-  <div>
-    <h2>"{{ experimentFullName }}"</h2>
+  <div style="text-align:center">
+    <!-- <h2>"{{ experimentFullName }}"</h2> -->
 
-    <v-card>
+    <!-- <v-card>
       <v-card-title primary-title>
         <v-spacer />
         <div class="headline">Experiment validated for the scene "{{ sceneName }}"</div>
@@ -10,7 +10,7 @@
       </v-card-title>
       <v-card-actions>
         <v-spacer />
-        <!-- <v-btn flat exact to="/experiments/">
+        <v-btn flat exact to="/experiments/">
           <v-icon left>home</v-icon>
           Select another experiment
         </v-btn>
@@ -18,7 +18,7 @@
         <v-btn flat exact :to="`/experiments/${experimentName}`">
           <v-icon left>arrow_back</v-icon>
           Go back to scene selection
-        </v-btn> -->
+        </v-btn>
         <v-btn v-if="hasScenesLeft" flat exact :to="`/experiments/${experimentName}/${getRandomScene}`">
           <v-icon left>shuffle</v-icon>
           Continue with a random scene
@@ -26,18 +26,37 @@
         <div v-if="!hasScenesLeft" class="headline">You finished all the scenes, thanks for your contribution!</div>
         <v-spacer />
       </v-card-actions>
-    </v-card>
+    </v-card> -->
+
+    <loader v-if="!loaded" :message="loadingMessage" />
+
+    <div v-if="loaded" style="margin-top:10%">
+      <p style="font-size: 1.4em;">
+        Nous vous remercions d'avoir participé à cette expérience.
+        Elle va nous permettre d'améliorer les calculs d'images.
+      </p>
+    </div>
+
+    <!-- Add of newsletter component -->
+    <Newsletter v-if="loaded" />
   </div>
 </template>
 
 <script>
+import Loader from '@/components/Loader.vue'
 import { mapActions, mapGetters } from 'vuex'
 import Experiments from '@/router/experiments'
-import { getExperimentSceneList } from '@/config.utils'
-import { rand } from '@/functions'
+import { getExperimentSceneList, getCalibrationScene, getCalibrationFrequency } from '@/config.utils'
+// import { rand } from '@/functions'
+import Newsletter from '@/components/ExperimentsComponents/Newsletter.vue'
+import stats from './../../results/match_extracts_probs.json'
 
 export default {
   name: 'ExperimentValidated',
+  components: {
+    Newsletter,
+    Loader
+  },
   props: {
     experimentName: {
       type: String,
@@ -51,7 +70,9 @@ export default {
   data() {
     return {
       experimentFullName: null,
-      availableScenes: []
+      availableScenes: [],
+      loaded: false,
+      loadingMessage: 'Chargement...'
     }
   },
   computed: {
@@ -62,10 +83,42 @@ export default {
       return this.availableScenes.length > 0
     },
     getRandomScene() {
-      return this.availableScenes[rand(0, this.availableScenes.length - 1)]
+      // need to get only data from available data
+      let availableStats = {}
+      let sumProbs = 0
+
+      for (let scene of this.availableScenes) {
+        availableStats[scene] = stats[scene]
+        sumProbs += stats[scene]
+      }
+
+      // renormalize probs for specific available scenes
+      for (let scene of this.availableScenes) {
+        availableStats[scene] /= sumProbs
+      }
+
+      let sceneChoice = this.availableScenes[0] // default choice
+      let sum = 0 // store sum prob during choice selection
+      let p = Math.random() // random number between 0 and 1
+
+      for (let scene of this.availableScenes) {
+        sum += availableStats[scene]
+
+        // get choice selection
+        if (sum >= p) {
+          sceneChoice = scene
+          break
+        }
+      }
+
+      return sceneChoice
     }
   },
   async mounted() {
+    // get information about calibration scene
+    let calibrationScene = getCalibrationScene(this.experimentName)
+    let calibrationSceneFreq = getCalibrationFrequency(this.experimentName)
+
     // reload scene list to update
     await this.loadScenesList
 
@@ -84,9 +137,20 @@ export default {
         this.progression[this.experimentName] &&
         !this.progression[this.experimentName][aScene].done)
 
-    if (this.hasScenesLeft) {
-      this.$router.push(`/experiments/${this.experimentName}/${this.getRandomScene}`)
+    // check if necessary to show calibration before new scenes
+    if (window.sessionStorage.getItem('sin3d-nb-scenes') !== null) {
+      let nScenes = Number(window.sessionStorage.getItem('sin3d-nb-scenes'))
+      window.sessionStorage.setItem('sin3d-nb-scenes', nScenes + 1)
+
+      if (nScenes % calibrationSceneFreq === 0) {
+        this.$router.push(`/experiments/${this.experimentName}/${calibrationScene}`)
+      }
+      else if (this.hasScenesLeft) {
+        this.$router.push(`/experiments/${this.experimentName}/${this.getRandomScene}`)
+      }
     }
+
+    this.loaded = true
   }
 }
 </script>

+ 2 - 2
src/views/Experiments/CalibrationMeasurement.vue

@@ -131,7 +131,7 @@ export default {
       const URI = `${this.getHostURI}${API_ROUTES.listSceneQualities(
         this.sceneName
       )}`
-      const { data } = await fetch(URI).then(res => res.json())
+      const { data } = await fetch(URI).then(res => res.json()).catch(e => console.log(e))
       this.qualities = data
 
       // remove reference
@@ -140,7 +140,7 @@ export default {
 
     // get reference size of image
     await Promise.all([
-      this.getImage('max').then(res => (reference = res))
+      this.getImage('max').then(res => (reference = res)).catch(e => console.log(e))
     ])
 
     this.maxWidth = reference.metadata.width

+ 290 - 9
src/views/Experiments/MatchExtractsWithReference.vue

@@ -1,5 +1,7 @@
 <template>
   <ExperimentBlock
+    :run-expe="runExpe"
+    :calibration-check="calibrationCheck"
     :experiment-name="experimentName"
     :scene-name="sceneName"
     :loading-message="loadingMessage"
@@ -17,7 +19,192 @@
     </template>
 
     <template v-slot:content>
-      <v-flex xs12 sm6 :style="{ 'max-width': maxWidth + 'px', 'min-width': maxWidth + 'px', 'margin-right': 20 + 'px' }">
+      <div class="text-center">
+        <v-dialog
+          v-model="dialogLess"
+          v-if="explanation === false"
+          width="500"
+        >
+          <v-card>
+            <v-card-title
+              class="headline lighten-2"
+              primary-title
+            >
+              <p style="font-size:0.8em;">La qualité de cette zone est déjà à son <strong>minimum</strong></p>
+            </v-card-title>
+
+            <v-divider />
+
+            <v-card-actions>
+              <v-spacer />
+              <v-btn
+                color="primary"
+                text
+                @click="dialogLess = false"
+              >
+                Fermer
+              </v-btn>
+            </v-card-actions>
+          </v-card>
+        </v-dialog>
+      </div>
+      <div class="text-center">
+        <v-dialog
+          v-model="dialogMore"
+          v-if="explanation === false"
+          width="500"
+        >
+          <v-card>
+            <v-card-title
+              class="headline lighten-2"
+              primary-title
+            >
+              <p style="font-size:0.8em;">La qualité de cette zone est déjà à son <strong>maximum</strong></p>
+            </v-card-title>
+
+            <v-divider />
+
+            <v-card-actions>
+              <v-spacer />
+              <v-btn
+                color="primary"
+                text
+                @click="dialogMore = false"
+              >
+                Fermer
+              </v-btn>
+            </v-card-actions>
+          </v-card>
+        </v-dialog>
+      </div>
+      <div class="text-center">
+        <v-dialog
+          v-model="dialogRefError"
+          v-if="explanation === false"
+          width="600"
+        >
+          <v-card>
+            <v-card-title
+              class="headline lighten-2"
+              primary-title
+            >
+              <p style="font-size:0.8em;">
+                Vous venez de cliquer sur l'image de référence. Celle-ci ne sera pas modifiée par cette action. <br /><br />
+                L'expérience a pour objectif de régler la qualité de l'image de gauche pour qu'elle soit la plus proche de l'image de droite.
+              </p>
+            </v-card-title>
+
+            <v-divider />
+
+            <v-card-actions>
+              <v-spacer />
+              <v-btn
+                color="primary"
+                text
+                @click="dialogRefError = false"
+              >
+                Fermer
+              </v-btn>
+            </v-card-actions>
+          </v-card>
+        </v-dialog>
+      </div>
+      <div class="text-center">
+        <v-dialog
+          v-model="calibrationCheck"
+          width="600"
+        >
+          <v-card>
+            <v-card-title
+              class="headline lighten-2"
+              primary-title
+            >
+              <p style="font-size:0.8em;">
+                La scène de calibration vous est proposée de nouveau dans le but de vérifier que l'image de gauche correspond toujours à celle de droite sur votre écran actuel.
+                <br /><br />
+                Vous pouvez la régler de nouveau si cela est nécessaire.
+              </p>
+            </v-card-title>
+
+            <v-divider />
+
+            <v-card-actions>
+              <v-spacer />
+              <v-btn
+                color="primary"
+                text
+                @click="calibrationCheck = false"
+              >
+                Fermer
+              </v-btn>
+            </v-card-actions>
+          </v-card>
+        </v-dialog>
+      </div>
+      <v-flex v-if="explanation === true || disableStart === true" xs12 sm12>
+        <div style="margin-top:10%">
+          <p v-if="disableStart === false && calibrationScene === true" style="font-size: 1.4em;">
+            Vous allez voir des images, l'image de droite constitue toujours l'image de référence. Vous devez régler la qualité de l'image de gauche pour qu'elle soit la plus proche de l'image de droite.
+            La première image est constituée de carré gris, c'est une partie de calibration. Si vous souhaitez régler votre écran (contraste, luminosité) vous pouvez le faire maintenant mais il vous est demandé de ne plus changer ce réglage au cours de l'expérience.
+            Vous allez ensuite voir des scènes d'image de synthèse. Vous pouvez arrêter l'expérience quand vous souhaitez (ou faire une pause).
+          </p>
+
+          <p v-if="disableStart === true" style="margin-top:2%; color:orange; font-size: 1.4em;">
+            <strong>Attention !</strong> L'expérience requiert un écran de résolution minimale de <strong>1920 x 1080</strong> pixels.
+            <br />
+            <br />
+            Vous pouvez redimensionner la fenêtre de votre navigateur pour poursuivre l'expérience.
+          </p>
+
+          <p v-if="disableStart === false" style="margin-top:2%; color:#007acc; font-size: 1.4em;">
+            <strong>La résolution de votre fenêtre de navigateur vous permet d'accéder à l'expérience</strong>.
+          </p>
+
+          <v-btn v-if="disableStart === true" @click="startExperiment" color="#007acc" large disabled>Poursuivre l'expérience</v-btn>
+          <v-btn v-if="disableStart === false" @click="startExperiment" color="#007acc" large>Poursuivre l'expérience</v-btn>
+        </div>
+      </v-flex>
+
+      <v-flex v-if="haveHelp === true" xs12 sm12>
+        <div style="margin-top:10%">
+          <p style="font-size: 1.4em;">
+            Cette expérience vous propose le visuel deux images, celle de gauche considérée comme dégradée et celle de gauche, sa référence (image sans dégradation visible).
+
+            <br />
+            Vous devez régler la qualité de l'image de gauche pour qu'elle soit la plus proche de l'image de droite.
+          </p>
+
+          <br />
+          <br />
+
+          <p style="font-size: 1.4em;text-align:left">
+            Pour cela, deux actions vous sont proposées :
+            <br />
+            <ul>
+              <li> <strong>clic gauche de la souris :</strong> permet d'améliorer la qualité de l'image à l'endroit où de la dégradation vous est encore visible.</li>
+              <li> <strong>clic droit de la souris :</strong> permet de revenir à une qualité inférieure de l'image si l'amélioration apportée n'était pas nécessaire (aucune amélioration visible apportée après clic droit).</li>
+            </ul>
+          </p>
+
+          <v-btn @click="startExperiment" color="#007acc" large>Continuer l'expérience</v-btn>
+        </div>
+      </v-flex>
+
+      <v-flex v-if="haveBreak === true" xs12 sm12>
+        <div style="margin-top:10%">
+          <p style="font-size: 1.4em;">
+            Nous vous remercions d'avoir participé à cette expérience.
+            Elle va nous permettre d'améliorer les calculs d'images. Si vous le souhaitez, vous pouvez revenir sur l'expérience via le <a :href="launcherURI" target="_blank">launcher</a> pour revoir de nouvelles images.
+          </p>
+
+          <v-btn @click="startExperiment" color="#007acc" large>Continuer l'expérience</v-btn>
+        </div>
+
+        <!-- Add of newsletter component -->
+        <Newsletter />
+      </v-flex>
+
+      <v-flex v-if="runExpe === true" xs12 sm6 :style="{ 'max-width': maxWidth + 'px', 'min-width': maxWidth + 'px', 'margin-right': 20 + 'px' }">
         <v-card dark color="primary" :max-width="maxWidth" :min-width="maxWidth">
           <!-- <v-card-text class="px-0">Experiment image</v-card-text> -->
 
@@ -61,19 +248,25 @@
           </v-container>
         </v-card>
       </v-flex>
-      <v-flex sm6 xs12 :style="{ 'max-width': maxWidth + 'px', 'min-width': maxWidth + 'px' }">
+      <v-flex @click="dialogRefError = true" v-if="runExpe === true" sm6 xs12 :style="{ 'max-width': maxWidth + 'px', 'min-width': maxWidth + 'px' }">
         <v-card dark color="primary" :max-width="maxWidth" :min-width="maxWidth">
           <!-- <v-card-text>Reference image</v-card-text> -->
           <v-img v-if="referenceImage" :src="referenceImage" :max-height="maxHeight" :max-width="maxWidth" :min-width="maxWidth" />
         </v-card>
       </v-flex>
       <!-- Experiment validation button -->
-      <v-layout justify-end align-content-end>
+      <v-layout v-if="runExpe === true" justify-end align-content-end>
         <v-text-field
           v-model="comment"
-          label="Add a comment here"
+          label="Ajouter un commentaire ici"
         />
-        <v-btn @click="finishExperiment" color="primary" large right>Finish experiment</v-btn>
+
+
+        <v-btn @click="userHelp" color="#737373" large right>Besoin d'aide ?</v-btn>
+
+        <v-btn @click="userBreak" color="#cc7a00" large right>Arrêter ou faire une pause</v-btn>
+
+        <v-btn @click="finishExperiment" color="#008000" large right>Valider & passer à l'image suivante</v-btn>
       </v-layout>
       <!--/ Experiment validation button -->
     </template>
@@ -81,14 +274,19 @@
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+
+import { getCalibrationScene } from '@/config.utils'
 import ExperimentBlock from '@/components/ExperimentBlock.vue'
 import ExperimentBaseExtracts from '@/mixins/ExperimentBaseExtracts'
 import ExtractConfiguration from '@/components/ExperimentsComponents/ExtractConfiguration.vue'
+import Newsletter from '@/components/ExperimentsComponents/Newsletter.vue'
 
 export default {
   components: {
     ExperimentBlock,
-    ExtractConfiguration
+    ExtractConfiguration,
+    Newsletter
   },
   mixins: [ExperimentBaseExtracts],
 
@@ -96,22 +294,38 @@ export default {
     return {
       referenceImage: null,
       maxWidth: null,
-      maxHeight: null
+      maxHeight: null,
+      launcherURI: null,
+      explanation: false,
+      haveBreak: false,
+      haveHelp: false,
+      calibrationScene: false,
+      runExpe: false,
+      calibrationCheck: false,
+      disableStart: false,
+      dialogRefError: false
     }
   },
-
+  computed: {
+    ...mapGetters(['getHostURI', 'getAllExperimentProgress'])
+  },
   async mounted() {
+    // load calibration for this experiment
+    let calibrationScene = getCalibrationScene(this.experimentName)
+
     // Load config for this scene to local state
     this.loadConfig()
 
     // Load progress from store into local state
     this.loadProgress()
 
+    this.launcherURI = this.getHostURI + '/launcher/'
+
     let reference = null
 
     // Load scene data from the API
     await Promise.all([
-      this.getImage('max').then(res => (reference = res)),
+      this.getImage('max').then(res => (reference = res)).catch(e => console.log(e)),
       this.getQualitiesList()
     ])
 
@@ -128,6 +342,73 @@ export default {
     await this.setExtractConfig(this.extractConfig, this.$refs.configurator)
 
     this.saveProgress()
+    this.loadExperimentState()
+
+    // check window size
+    this.checkWindow()
+    window.addEventListener('resize', this.checkWindow)
+
+    // check if calibration is already done
+    if (this.sceneName === calibrationScene) {
+      // load current user progression
+      this.progression = this.getAllExperimentProgress()
+      let done = this.progression[this.experimentName][this.sceneName].done
+
+      // change variable state for calibration if already done
+      if (done === true) {
+        this.calibrationCheck = true
+        this.runExpe = true
+        this.explanation = false
+        this.calibrationScene = true
+      }
+    }
+  },
+  methods: {
+    startExperiment() {
+      this.runExpe = true
+      this.explanation = false
+      this.haveBreak = false
+      this.haveHelp = false
+    },
+    userBreak() {
+      this.haveBreak = true
+      this.explanation = false
+      this.runExpe = false
+      this.haveHelp = false
+    },
+    userHelp() {
+      this.haveHelp = true
+      this.haveBreak = false
+      this.explanation = false
+      this.runExpe = false
+    },
+    loadExperimentState() {
+      this.haveHelp = false
+      this.explanation = false
+      this.haveBreak = false
+      this.runExpe = false
+
+      if (this.sceneName === '50_shades_of_grey') {
+        this.explanation = true
+        this.calibrationScene = true
+      }
+      else {
+        this.runExpe = true
+      }
+    },
+    checkWindow() {
+      // check window screen size
+      if (window.innerWidth < 1920 || window.innerHeight < 900) {
+        this.disableStart = true
+        this.explanation = true
+        this.runExpe = false
+        this.haveBreak = false
+        this.haveHelp = false
+      }
+      else {
+        this.disableStart = false
+      }
+    }
   }
 }
 </script>

+ 1 - 1
src/views/Experiments/MatchExtractsWithReferenceAll.vue

@@ -111,7 +111,7 @@ export default {
 
     // Load scene data from the API
     await Promise.all([
-      this.getImage('max').then(res => (reference = res)),
+      this.getImage('max').then(res => (reference = res)).catch(e => console.log(e)),
       this.getQualitiesList()
     ])
 

+ 21 - 1
src/views/SelectExperimentScene.vue

@@ -69,7 +69,7 @@ import Loader from '@/components/Loader.vue'
 import { mapGetters, mapActions } from 'vuex'
 import Experiments from '@/router/experiments'
 import { API_ROUTES, shuffleArray } from '@/functions'
-import { getExperimentSceneList } from '@/config.utils'
+import { getExperimentSceneList, getCalibrationScene, getCalibrationFrequency } from '@/config.utils'
 
 export default {
   name: 'SelectExperimentScene',
@@ -105,6 +105,10 @@ export default {
     }
   },
   async mounted() {
+    // get information from calibration scene
+    let calibrationScene = getCalibrationScene(this.experimentName)
+    let calibrationSceneFreq = getCalibrationFrequency(this.experimentName)
+
     // reload scene list to update
     await this.loadScenesList
 
@@ -124,6 +128,7 @@ export default {
     for (const aScene of scenesList) {
       const { data: thumb } = await fetch(`${this.getHostURI}${API_ROUTES.getImage(aScene, 'max')}`)
         .then(res => res.json())
+        .catch(e => console.log(e))
 
       let sceneObj = {
         name: thumb.sceneName,
@@ -146,11 +151,26 @@ export default {
         }
       }
     }
+
     // Randomize each group
     todo = shuffleArray(todo)
     working = shuffleArray(working)
     done = shuffleArray(done)
 
+    // here push in session (if not already there) current user advancement
+    if (window.sessionStorage.getItem('sin3d-nb-scenes') === null) {
+      window.sessionStorage.setItem('sin3d-nb-scenes', 1)
+      this.$router.push(`/experiments/${this.experimentName}/${calibrationScene}`)
+    }
+
+    // check if necessary to show calibration before new scenes
+    if (window.sessionStorage.getItem('sin3d-nb-scenes') !== null) {
+      let nScenes = Number(window.sessionStorage.getItem('sin3d-nb-scenes'))
+
+      if (nScenes % calibrationSceneFreq === 0)
+        this.$router.push(`/experiments/${this.experimentName}/${calibrationScene}`)
+    }
+
     // for the experiment user is redirect to current working on
     if (working.length > 0) {
       this.$router.push(working[0].experimentLink)

+ 35 - 0
utils/extract_experiment.py

@@ -0,0 +1,35 @@
+# db connection
+from pymongo import MongoClient
+import json, os
+
+connection = MongoClient()
+
+db = connection['sin3d']
+data_collection = db['datas']
+
+output_results_folder = 'results'
+experiments_identifier = ['sin3d-PrISE-3D']
+
+experiment_results = data_collection.find({
+    'data.msg.experimentName': 'MatchExtractsWithReference', 
+    'data.msgId': 'EXPERIMENT_VALIDATED',
+    'data.experimentId':{
+        '$in': experiments_identifier
+    }
+    # '$not': { '$gt': 1.99 }
+})
+
+if not os.path.exists(output_results_folder):
+    os.makedirs(output_results_folder)
+
+results_filename = 'experiments_results.json'
+results_filepath = os.path.join(output_results_folder, results_filename)
+
+export_data = []
+
+for result in experiment_results:
+    export_data.append(result['data'])
+
+print('Save results into', results_filepath)
+with open(results_filepath, 'w') as f:
+    f.write(json.dumps(export_data, indent=4))

+ 57 - 0
utils/extract_stats_freq_and_min.py

@@ -0,0 +1,57 @@
+# main imports
+import os, sys
+import argparse
+import json
+import numpy as np
+
+
+def main():
+    """
+    main function which is ran when launching script
+    """ 
+    parser = argparse.ArgumentParser(description="Extract scenes data and save thresholds into .csv")
+
+    parser.add_argument('--file', type=str, help='image to convert', required=True)
+    parser.add_argument('--output', type=str, help='output csv filename', required=True)
+
+    args = parser.parse_args()
+
+    p_file   = args.file
+    p_output = args.output
+
+    f = open(p_file)
+    json_data = json.load(f)
+
+    dict_data = {}
+
+    for element in json_data:
+
+        scene = element['msg']['sceneName']
+
+        if scene not in dict_data:
+            dict_data[scene] = {}
+
+        extracts = element['msg']['extracts']
+
+        for extract in extracts:
+            if extract['index'] not in dict_data[scene]:
+                dict_data[scene][extract['index']] = [extract['quality']]
+            else:
+                dict_data[scene][extract['index']].append(extract['quality'])
+            
+    
+    output_file = open(p_output, 'w')
+
+    for scene in dict_data:
+        output_file.write(scene + ';')
+        
+        for extract in dict_data[scene]:
+            thresholds_data = dict_data[scene][extract]
+            output_file.write(str(int(np.min(thresholds_data))) + ';')
+
+        output_file.write('\n')
+
+
+
+if __name__ == "__main__":
+    main()

+ 64 - 0
utils/extract_stats_freq_and_min_all.py

@@ -0,0 +1,64 @@
+# main imports
+import os, sys
+import argparse
+import json
+import numpy as np
+
+
+def main():
+    """
+    main function which is ran when launching script
+    """ 
+    parser = argparse.ArgumentParser(description="Extract scenes data and save thresholds into .csv")
+
+    parser.add_argument('--file', type=str, help='image to convert', required=True)
+    parser.add_argument('--output', type=str, help='output csv filename', required=True)
+
+    args = parser.parse_args()
+
+    p_file   = args.file
+    p_output = args.output
+
+    f = open(p_file)
+    json_data = json.load(f)
+
+    dict_data = {}
+
+    for element in json_data:
+
+        scene = element['msg']['sceneName']
+
+        if scene not in dict_data:
+            dict_data[scene] = {}
+
+        extracts = element['msg']['extracts']
+
+        for extract in extracts:
+            if extract['index'] not in dict_data[scene]:
+                dict_data[scene][extract['index']] = [extract['quality']]
+            else:
+                dict_data[scene][extract['index']].append(extract['quality'])
+            
+    
+    output_file = open(p_output, 'w')
+    # output_file.write('scene;n_users;min_scene;\n')
+
+    for scene in dict_data:
+        output_file.write(scene + ';')
+        
+        all_thresholds = []
+        n_users = 0
+        for extract in dict_data[scene]:
+            thresholds_data = dict_data[scene][extract]
+            
+            all_thresholds.append(int(np.min(thresholds_data)))
+            n_users = len(thresholds_data)
+
+        output_file.write(str(n_users) + ';' + str(np.min(all_thresholds)) + ';')
+
+        output_file.write('\n')
+
+
+
+if __name__ == "__main__":
+    main()

+ 7 - 121
yarn.lock

@@ -1243,11 +1243,6 @@
   resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
   integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
 
-abbrev@1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
-  integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
-
 accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
   version "1.3.7"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -3214,7 +3209,7 @@ debug@3.1.0, debug@~3.1.0:
   dependencies:
     ms "2.0.0"
 
-debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
+debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5:
   version "3.2.6"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
   integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
@@ -3394,7 +3389,7 @@ destroy@~1.0.4:
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
   integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
 
-detect-libc@^1.0.2, detect-libc@^1.0.3:
+detect-libc@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
   integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
@@ -4644,13 +4639,6 @@ fs-extra@^8.1.0:
     jsonfile "^4.0.0"
     universalify "^0.1.0"
 
-fs-minipass@^1.2.5:
-  version "1.2.7"
-  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
-  integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
-  dependencies:
-    minipass "^2.6.0"
-
 fs-minipass@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@@ -5260,7 +5248,7 @@ human-signals@^1.1.1:
   resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
   integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
 
-iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
+iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -5299,13 +5287,6 @@ ignore-by-default@^1.0.0:
   resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
   integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk=
 
-ignore-walk@^3.0.1:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37"
-  integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==
-  dependencies:
-    minimatch "^3.0.4"
-
 ignore@^3.3.3, ignore@^3.3.5:
   version "3.3.10"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
@@ -6666,14 +6647,6 @@ minimist@^1.2.0:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
   integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
 
-minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
-  integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
-  dependencies:
-    safe-buffer "^5.1.2"
-    yallist "^3.0.0"
-
 minipass@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.1.tgz#7607ce778472a185ad6d89082aa2070f79cedcd5"
@@ -6681,13 +6654,6 @@ minipass@^3.0.0:
   dependencies:
     yallist "^4.0.0"
 
-minizlib@^1.2.1:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
-  integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
-  dependencies:
-    minipass "^2.9.0"
-
 minizlib@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.0.tgz#fd52c645301ef09a63a2c209697c294c6ce02cf3"
@@ -6906,15 +6872,6 @@ natural-compare@^1.4.0:
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
 
-needle@^2.2.1:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/needle/-/needle-2.3.2.tgz#3342dea100b7160960a450dc8c22160ac712a528"
-  integrity sha512-DUzITvPVDUy6vczKKYTnWc/pBZ0EnjMJnQ3y+Jo5zfKFimJs7S3HFCxCRZYB9FUZcrzUQr3WsmvZgddMEIZv6w==
-  dependencies:
-    debug "^3.2.6"
-    iconv-lite "^0.4.4"
-    sax "^1.2.4"
-
 negotiator@0.6.2:
   version "0.6.2"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@@ -6992,22 +6949,6 @@ node-libs-browser@^2.2.1:
     util "^0.11.0"
     vm-browserify "^1.0.1"
 
-node-pre-gyp@*:
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83"
-  integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==
-  dependencies:
-    detect-libc "^1.0.2"
-    mkdirp "^0.5.1"
-    needle "^2.2.1"
-    nopt "^4.0.1"
-    npm-packlist "^1.1.6"
-    npmlog "^4.0.2"
-    rc "^1.2.7"
-    rimraf "^2.6.1"
-    semver "^5.3.0"
-    tar "^4.4.2"
-
 node-releases@^1.1.47:
   version "1.1.48"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.48.tgz#7f647f0c453a0495bcd64cbd4778c26035c2f03a"
@@ -7020,14 +6961,6 @@ noop-logger@^0.1.1:
   resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
   integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=
 
-nopt@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
-  integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
-  dependencies:
-    abbrev "1"
-    osenv "^0.1.4"
-
 normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -7080,27 +7013,6 @@ normalize-url@^4.1.0:
   resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
   integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
 
-npm-bundled@^1.0.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b"
-  integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==
-  dependencies:
-    npm-normalize-package-bin "^1.0.1"
-
-npm-normalize-package-bin@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2"
-  integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
-
-npm-packlist@^1.1.6:
-  version "1.4.8"
-  resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
-  integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
-  dependencies:
-    ignore-walk "^3.0.1"
-    npm-bundled "^1.0.1"
-    npm-normalize-package-bin "^1.0.1"
-
 npm-run-path@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -7115,7 +7027,7 @@ npm-run-path@^4.0.0:
   dependencies:
     path-key "^3.0.0"
 
-npmlog@^4.0.1, npmlog@^4.0.2, npmlog@^4.1.2:
+npmlog@^4.0.1, npmlog@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -7329,11 +7241,6 @@ os-browserify@^0.3.0:
   resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
   integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
 
-os-homedir@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
-  integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
-
 os-locale@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
@@ -7343,19 +7250,11 @@ os-locale@^3.0.0:
     lcid "^2.0.0"
     mem "^4.0.0"
 
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
+os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
 
-osenv@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
-  integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
-  dependencies:
-    os-homedir "^1.0.0"
-    os-tmpdir "^1.0.0"
-
 p-cancelable@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
@@ -8862,7 +8761,7 @@ saslprep@^1.0.0:
   dependencies:
     sparse-bitfield "^3.0.3"
 
-sax@^1.2.4, sax@~1.2.4:
+sax@~1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -9719,19 +9618,6 @@ tar-stream@^2.0.0:
     inherits "^2.0.3"
     readable-stream "^3.1.1"
 
-tar@^4.4.2:
-  version "4.4.13"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
-  integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
-  dependencies:
-    chownr "^1.1.1"
-    fs-minipass "^1.2.5"
-    minipass "^2.8.6"
-    minizlib "^1.2.1"
-    mkdirp "^0.5.0"
-    safe-buffer "^5.1.2"
-    yallist "^3.0.3"
-
 tar@^5.0.5:
   version "5.0.5"
   resolved "https://registry.yarnpkg.com/tar/-/tar-5.0.5.tgz#03fcdb7105bc8ea3ce6c86642b9c942495b04f93"
@@ -10735,7 +10621,7 @@ yallist@^2.1.2:
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
   integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
 
-yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
+yallist@^3.0.2:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
   integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==