Parcourir la source

Merge branch 'release/v0.2.4'

rigwild il y a 5 ans
Parent
commit
983a4eb177

+ 1 - 1
cleanExtracts.js

@@ -61,4 +61,4 @@ if (argv.includes('--execute')) {
   setup(process.env.IMAGES_PATH, false, true)
 }
 
-module.exports = setup
+module.exports = { setup, extractsRemoverServiceLogger: fileLogger }

+ 37 - 15
experimentConfig.default.js

@@ -1,13 +1,7 @@
 export const mixins = {
   ExperimentBase: {
-    defaultConfig: {
-      lockConfig: true
-    },
-    scenesConfig: {
-      // bathroom: {
-      //   lockConfig: true
-      // }
-    }
+    defaultConfig: {},
+    scenesConfig: {}
   },
 
   ExperimentBaseAreSameImages: {
@@ -23,7 +17,8 @@ export const mixins = {
 
   ExperimentBaseExtracts: {
     defaultConfig: {
-      showHoverBorder: false,
+      lockConfig: false,
+      showHoverBorder: true,
       extractConfig: {
         x: 4,
         y: 4
@@ -31,6 +26,7 @@ export const mixins = {
     },
     scenesConfig: {
       // bathroom: {
+      //   lockConfig: false,
       //   showHoverBorder: false,
       //   extractConfig: {
       //     x: 4,
@@ -44,18 +40,44 @@ export const mixins = {
 
 export const experiments = {
   MatchExtractsWithReference: {
-    mixin: mixins.ExperimentBaseExtracts,
+    mixins: [mixins.ExperimentBaseExtracts],
     defaultConfig: {},
-    scenesConfig: {}
+    scenesConfig: {},
+    availableScenes: {
+      whitelist: null,
+      blacklist: null
+      // No whitelist = Select all scenes
+      // Whitelist = Only select some scenes
+      // Blacklist = remove scenes
+      // whitelist: ['Appart1opt02', 'contemporary', 'bathroom', 'SdbDroite'],
+      // blacklist: ['Appart1opt02']
+    }
   },
   AreSameImagesRandom: {
-    mixin: mixins.ExperimentBaseAreSameImages,
+    mixins: [mixins.ExperimentBaseAreSameImages],
     defaultConfig: {},
-    scenesConfig: {}
+    scenesConfig: {},
+    availableScenes: {
+      whitelist: null,
+      blacklist: null
+    }
   },
   AreSameImagesReference: {
-    mixin: mixins.ExperimentBaseAreSameImages,
+    mixins: [mixins.ExperimentBaseAreSameImages],
     defaultConfig: {},
-    scenesConfig: {}
+    scenesConfig: {},
+    availableScenes: {
+      whitelist: null,
+      blacklist: null
+    }
+  },
+  AreSameImagesReferenceOneExtract: {
+    mixins: [mixins.ExperimentBaseAreSameImages, mixins.ExperimentBaseExtracts],
+    defaultConfig: {},
+    scenesConfig: {},
+    availableScenes: {
+      whitelist: null,
+      blacklist: null
+    }
   }
 }

+ 2 - 2
index.js

@@ -3,7 +3,7 @@
 import { CronJob } from 'cron'
 
 import server from './server'
-import cleanExtracts from './cleanExtracts'
+import { setup as cleanExtracts, extractsRemoverServiceLogger } from './cleanExtracts'
 import { imagesPath, deleteExtractsCronTime } from './config'
 
 const argv = process.argv.slice(2)
@@ -11,7 +11,7 @@ const argv = process.argv.slice(2)
 // 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)
-  console.log('Started the extracts remover service.')
+  extractsRemoverServiceLogger.info('Started the extracts remover service.')
 }
 
 // Start the server

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "expe-web",
-  "version": "0.2.3",
+  "version": "0.2.4",
   "private": true,
   "scripts": {
     "server:start": "node -r esm index.js",

+ 38 - 0
src/components/ExperimentsComponents/ExtractsToImage.vue

@@ -0,0 +1,38 @@
+<template>
+  <div>
+    <template v-for="i in extractConfig.y">
+      <v-layout row wrap :key="`row-${i}`">
+        <v-flex
+          v-for="(anExtract, index) in extractsSliced(i)"
+          :key="`extract-${i}-${extractConfig.x}-${extractConfig.y}-${index}-${anExtract.quality}`"
+          class="pa-0"
+        >
+          <v-card flat tile class="d-flex height100">
+            <v-img :src="anExtract" />
+          </v-card>
+        </v-flex>
+      </v-layout>
+    </template>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ExtractsToImage',
+  props: {
+    extractConfig: {
+      type: Object,
+      required: true
+    },
+    extracts: {
+      type: Array,
+      required: true
+    }
+  },
+  computed: {
+    extractsSliced() {
+      return vForIndex => this.extracts.slice(this.extractConfig.x * (vForIndex - 1), (this.extractConfig.x * vForIndex))
+    }
+  }
+}
+</script>

+ 3 - 3
src/components/ResetAppButton.vue

@@ -2,7 +2,7 @@
   <div class="text-xs-center">
     <v-dialog
       v-model="showDialog"
-      width="600"
+      width="800"
       :fullscreen="$vuetify.breakpoint.smAndDown"
     >
       <template v-slot:activator="{ on }">
@@ -23,7 +23,7 @@
         <v-card-actions>
           <v-btn color="secondary" flat @click="showDialog = false">Cancel</v-btn>
           <v-spacer />
-          <v-flex xs6>
+          <v-flex xs8>
             <v-select
               v-model="selectedItems"
               :items="items"
@@ -74,7 +74,7 @@ export default {
       selectedItems: [],
       items: [
         { text: 'GDPR consent', value: 'gdprConsent' },
-        { text: 'Host configuration', value: 'hostConfig' },
+        { text: 'Host configuration and User/Experiment ID', value: 'hostConfig' },
         { text: 'Progression', value: 'progression' }
       ]
     }

+ 31 - 12
src/config.utils.js

@@ -1,10 +1,14 @@
 import deepmerge from 'deepmerge'
+import store from '@/store'
 import { experiments } from '@/../experimentConfig'
 
 // Merge a default config with a specific scene config
 const buildConfig = ({ defaultConfig = {}, scenesConfig = {} }, sceneName) =>
   deepmerge(defaultConfig, scenesConfig[sceneName] || {})
 
+const buildMultiConfig = (confArr, sceneName) =>
+  deepmerge.all(confArr.map(aConfig => buildConfig(aConfig, sceneName)))
+
 /**
 * Build a configuration file by merging the default config with the asked scene.
 * The asked scene config will overwrite the default config.
@@ -20,21 +24,36 @@ export const getExperimentConfig = (experimentName, sceneName) => {
     throw new Error(`Could not find the experiment "${experimentName}" in the config file.`)
 
   // Build parent mixin config
-  const mixinConfig = buildConfig(experiments[experimentName].mixin, sceneName)
+  const mixinConfig = buildMultiConfig(experiments[experimentName].mixins, sceneName)
   // Build global config
   const globalConfig = buildConfig(experiments[experimentName], sceneName)
   // Merge configs
   return deepmerge(mixinConfig, globalConfig)
 }
 
-// /**
-//  * Read config to get the list of available scenes for a given experiment
-//  *
-//  * @param {Object} experimentName The selected experiment
-//  * @param {String[]} scenesList List of scenes
-//  * @returns {String[]} The list of available scenes for this experiment
-//  */
-// export const getExperimentSceneList = (experimentName, scenesList) => {
-//   // TODO: scene blacklist, scene array, all
-//   return []
-// }
+/**
+ * Read config to get the list of available scenes for a given experiment.
+ * If no whitelist is supplied, it will take all the available scenes.
+ * If a blacklist is supplied, it will remove its scenes from the list of scenes.
+ *
+ * @param {Object} experimentName The selected experiment
+ * @returns {String[]} The list of available scenes for this experiment
+ */
+export const getExperimentSceneList = experimentName => {
+  if (!experiments[experimentName])
+    throw new Error(`Could not find the experiment "${experimentName}" in the config file.`)
+
+  let configuredScenesList = []
+
+  const confObj = experiments[experimentName].availableScenes
+  const scenesList = store.state.scenesList
+
+  // Apply whitelist
+  if (confObj.whitelist) configuredScenesList = scenesList.filter(x => confObj.whitelist.includes(x))
+  else configuredScenesList = scenesList
+
+  // Apply blacklist
+  if (confObj.blacklist) configuredScenesList = configuredScenesList.filter(x => !confObj.blacklist.includes(x))
+
+  return configuredScenesList
+}

+ 2 - 4
src/mixins/ExperimentBase.vue

@@ -24,9 +24,7 @@ export default {
 
       loadingMessage: null,
       loadingErrorMessage: null,
-      qualities: null,
-
-      lockConfig: null
+      qualities: null
     }
   },
   computed: {
@@ -86,7 +84,7 @@ export default {
       obj.loadingErrorMessage = undefined
       this.sendMessage({ msgId: experimentMsgId.VALIDATED, msg: obj })
       this.setExperimentDone({ experimentName: this.experimentName, sceneName: this.sceneName, done: true })
-      this.$router.push(`/experiments/${this.experimentName}`)
+      this.$router.push(`/experiments/${this.experimentName}/${this.sceneName}/validated`)
     },
 
 

+ 17 - 14
src/mixins/ExperimentBaseAreSameImages.vue

@@ -19,8 +19,8 @@ export default {
       maxTestCount: null,
       testCount: 1,
 
-      leftImage: { link: null, quality: null },
-      rightImage: { link: null, quality: null }
+      image1: null,
+      image2: null
     }
   },
   computed: {
@@ -35,11 +35,11 @@ export default {
     // Get images links for a test
     async getTest(leftQuality, rightQuality) {
       const res = await Promise.all([this.getImage(leftQuality), this.getImage(rightQuality)])
-      const [leftImage, rightImage] = res.map(x => {
+      const [image1, image2] = res.map(x => {
         x.link = `${this.getHostURI}${x.link}`
         return x
       })
-      return { leftImage, rightImage }
+      return { image1, image2 }
     },
 
     // Get a test with random qualities
@@ -58,6 +58,7 @@ export default {
       const randomQuality = this.qualities[rand(0, this.qualities.length - 1)]
 
       const res = [this.qualities[this.qualities.length - 1], randomQuality]
+      this.referenceImagePosition = isReferenceLeft ? 'left' : 'right'
       const table = isReferenceLeft ? res : res.reverse()
       return this.getTest(table[0], table[1])
     },
@@ -65,26 +66,28 @@ export default {
     /** An action was triggered, load a new test and save progression
      * @param {Boolean} areTheSame Are the images the same
      * @param {Function} getTestFn Function to be called to get the next tests
+     * @param {Function} additionalData Object to concat to log
      * @returns {void}
      */
-    async areTheSameAction(areTheSame, getTestFn) {
+    async areTheSameAction(areTheSame, getTestFn, additionalData) {
       this.loadingMessage = 'Loading new test...'
       this.loadingErrorMessage = null
       try {
         this.testCount++
 
-        const obj = {
-          leftImage: this.leftImage,
-          rightImage: this.rightImage,
+        const obj = Object.assign({
+          image1: this.image1,
+          image2: this.image2,
           areTheSame,
           experimentName: this.experimentName,
-          sceneName: this.sceneName
-        }
+          sceneName: this.sceneName,
+          referenceImagePosition: this.referenceImagePosition || undefined
+        }, additionalData || {})
         this.sendMessage({ msgId: experimentMsgId.DATA, msg: obj })
 
-        const { leftImage, rightImage } = await getTestFn()
-        this.leftImage = leftImage
-        this.rightImage = rightImage
+        const { image1, image2 } = await getTestFn()
+        this.image1 = image1
+        this.image2 = image2
 
         // Experiment end
         if (this.testCount > this.maxTestCount) return this.finishExperiment()
@@ -108,7 +111,7 @@ export default {
       }
       this.sendMessage({ msgId: experimentMsgId.VALIDATED, msg: obj })
       this.setExperimentDone({ experimentName: this.experimentName, sceneName: this.sceneName, done: true })
-      this.$router.push(`/experiments/${this.experimentName}`)
+      this.$router.push(`/experiments/${this.experimentName}/${this.sceneName}/validated`)
     }
   }
 }

+ 95 - 20
src/mixins/ExperimentBaseExtracts.vue

@@ -22,8 +22,10 @@ export default {
         y: null
       },
       extracts: [],
+      extractsInfos: null,
 
-      showHoverBorder: null
+      showHoverBorder: null,
+      lockConfig: null
     }
   },
   computed: {
@@ -45,6 +47,19 @@ export default {
       return data
     },
 
+    // Convert a simple API extracts object to get more informations
+    getExtractFullObject(extractsApiObj) {
+      return extractsApiObj.extracts.map((url, i) => ({
+        link: this.getHostURI + url,
+        quality: extractsApiObj.info.image.quality,
+        zone: i + 1,
+        index: i,
+        nextQuality: findNearestUpper(extractsApiObj.info.image.quality, this.qualities),
+        precQuality: findNearestLower(extractsApiObj.info.image.quality, this.qualities),
+        loading: false
+      }))
+    },
+
     // Config was updated, load extracts and save progression
     async setExtractConfig(config, configuratorRef) {
       if (!config) return
@@ -54,17 +69,10 @@ export default {
       try {
         this.extractConfig.x = config.x
         this.extractConfig.y = config.y
-        const data = await this.getExtracts()
-        const hostURI = this.getHostURI
-        this.extracts = data.extracts.map((url, i) => ({
-          link: hostURI + url,
-          quality: data.info.image.quality,
-          zone: i + 1,
-          index: i,
-          nextQuality: findNearestUpper(data.info.image.quality, this.qualities),
-          precQuality: findNearestLower(data.info.image.quality, this.qualities),
-          loading: false
-        }))
+        this.extractConfig.quality = config.quality
+        const data = await this.getExtracts(config.quality || undefined)
+        this.extractsInfos = data.info
+        this.extracts = this.getExtractFullObject(data)
 
         // If there is a configurator, retract it
         if (configuratorRef) configuratorRef.setVisibility(false)
@@ -81,12 +89,14 @@ export default {
 
     // An action was triggered, load extracts and save progression
     async extractAction(event, extractObj) {
-      console.log(event, extractObj)
       const { index, nextQuality, precQuality, quality } = extractObj
 
-      let newQuality
-      if (event.button === 0) newQuality = precQuality // Left click
-      if (event.button === 2) newQuality = nextQuality // Right click
+      let action, newQuality
+      if (event.button === 0) action = 'needLess' // Left click
+      if (event.button === 2) action = 'needMore' // Right click
+
+      if (action === 'needLess') newQuality = precQuality
+      if (action === 'needMore') newQuality = nextQuality
 
       // Do not load a new extract if same quality
       if (newQuality === quality) return
@@ -94,6 +104,9 @@ export default {
       // Set loading state
       this.extracts[index].loading = true
       try {
+        const collectedData = this.getClickDataObject(event, extractObj, action)
+        this.sendMessage({ msgId: experimentMsgId.DATA, msg: collectedData })
+
         // Loading new extract
         const data = await this.getExtracts(newQuality)
         this.extracts[index].link = this.getHostURI + data.extracts[index]
@@ -101,9 +114,6 @@ export default {
         this.extracts[index].nextQuality = findNearestUpper(data.info.image.quality, this.qualities)
         this.extracts[index].precQuality = findNearestLower(data.info.image.quality, this.qualities)
         this.extracts[index].loading = false
-
-        // Sending event to WebSocket server
-      // this.sendMessage({ msgId: experimentMsgId.DATA, msg: obj })
       }
       catch (err) {
         // TODO: toast message if fail
@@ -115,6 +125,71 @@ export default {
       }
     },
 
+    getClickDataObject(event, extractObj, action) {
+      const { index } = extractObj
+
+      const clientSideData = {
+        extractSize: {
+          width: event.target.clientWidth,
+          height: event.target.clientHeight
+        },
+        imageSize: {
+          width: event.target.clientWidth * this.extractConfig.x,
+          height: event.target.clientHeight * this.extractConfig.y
+        },
+        clickPosition: {
+          extract: {
+            x: event.offsetX,
+            y: event.offsetY
+          },
+          image: {
+            x: event.offsetX + (this.extracts[index].index % this.extractConfig.x) * event.target.clientWidth,
+            y: event.offsetY + (Math.floor(this.extracts[index].index / this.extractConfig.x)) * event.target.clientHeight
+          }
+        }
+      }
+
+      const calculatedRealData = {}
+      calculatedRealData.extractSize = {
+        width: this.extractsInfos.extractsSize.width,
+        height: this.extractsInfos.extractsSize.height
+      }
+      calculatedRealData.imageSize = {
+        width: this.extractsInfos.image.metadata.width,
+        height: this.extractsInfos.image.metadata.height
+      }
+      calculatedRealData.clickPosition = {
+        extract: {
+          x: Math.floor((calculatedRealData.imageSize.width * clientSideData.clickPosition.extract.x) / clientSideData.imageSize.width),
+          y: Math.floor((calculatedRealData.imageSize.height * clientSideData.clickPosition.extract.y) / clientSideData.imageSize.height)
+        },
+        image: {
+          x: Math.floor((calculatedRealData.imageSize.width * clientSideData.clickPosition.image.x) / clientSideData.imageSize.width),
+          y: Math.floor((calculatedRealData.imageSize.height * clientSideData.clickPosition.image.y) / clientSideData.imageSize.height)
+        }
+      }
+
+      // Sending event to WebSocket server
+      const loggedObj = {
+        experimentName: this.experimentName,
+        sceneName: this.sceneName,
+        extractConfig: this.extractConfig,
+        clickedExtract: {
+          link: this.extracts[index].link,
+          quality: this.extracts[index].quality,
+          nextQuality: this.extracts[index].nextQuality,
+          precQuality: this.extracts[index].precQuality,
+          zone: this.extracts[index].zone,
+          index: this.extracts[index].index
+        },
+        action,
+        clientSideData,
+        calculatedRealData
+      }
+
+      return loggedObj
+    },
+
     // Finish an experiment, sending full data to the server
     // Don't forget to surcharge this function when using this mixin to add more data
     finishExperiment() {
@@ -135,7 +210,7 @@ export default {
       }
       this.sendMessage({ msgId: experimentMsgId.VALIDATED, msg: obj })
       this.setExperimentDone({ experimentName: this.experimentName, sceneName: this.sceneName, done: true })
-      this.$router.push(`/experiments/${this.experimentName}`)
+      this.$router.push(`/experiments/${this.experimentName}/${this.sceneName}/validated`)
     }
   }
 }

+ 9 - 0
src/router/experiments.js

@@ -25,5 +25,14 @@ export default [
     meta: {
       fullName: 'Are images the same ? (One is reference image, the other is random quality)'
     }
+  },
+  {
+    path: '/experiments/AreSameImagesReferenceOneExtract/:sceneName',
+    name: 'AreSameImagesReferenceOneExtract',
+    component: () => import('@/views/Experiments/AreSameImagesReferenceOneExtract'),
+    props: true,
+    meta: {
+      fullName: 'Are images the same ? (Both are reference images but one contains a random quality extract)'
+    }
   }
 ]

+ 10 - 4
src/router/index.js

@@ -1,7 +1,7 @@
 import Vue from 'vue'
 import Router from 'vue-router'
-import GdprNotice from '@/views/GdprNotice.vue'
-import HostConfig from '@/views/HostConfig.vue'
+import GdprNotice from '@/views/GdprNotice'
+import HostConfig from '@/views/HostConfig'
 import Experiments from './experiments'
 
 Vue.use(Router)
@@ -25,12 +25,18 @@ export default new Router({
     {
       path: '/experiments',
       name: 'ExperimentsList',
-      component: () => import('@/views/ExperimentsList.vue')
+      component: () => import('@/views/ExperimentsList')
     },
     {
       path: '/experiments/:experimentName',
       name: 'SelectExperimentScene',
-      component: () => import('@/views/SelectExperimentScene.vue'),
+      component: () => import('@/views/SelectExperimentScene'),
+      props: true
+    },
+    {
+      path: '/experiments/:experimentName/:sceneName/validated',
+      name: 'ExperimentValidated',
+      component: () => import('@/views/ExperimentValidated'),
       props: true
     },
     ...Experiments

+ 14 - 2
src/store/actions.js

@@ -14,7 +14,9 @@ export default {
     if (!state.uuid) commit('setAppUniqueId')
   },
 
-  resetApp({ commit }, { gdprConsent = false, hostConfig = false, progression = false }) {
+  resetApp({ commit, state }, { gdprConsent = false, hostConfig = false, progression = false }) {
+    if (hostConfig && state.socket.isConnected)
+      this._vm.$disconnect()
     commit('resetApp', { gdprConsent, hostConfig, progression })
   },
 
@@ -51,6 +53,10 @@ export default {
       })
   },
 
+  setUserExperimentId({ commit }, { userId, experimentId }) {
+    commit('setUserExperimentId', { userId, experimentId })
+  },
+
   async connectToWs({ state, getters }) {
     if (state.socket.isConnected) return /*eslint-disable-line */
     else if (getters.isHostConfigured) {
@@ -80,7 +86,13 @@ export default {
   },
 
   sendMessage({ state }, { msgId, msg = undefined }) {
-    Vue.prototype.$socket.send(JSON.stringify({ uuid: state.uuid, msgId, msg }))
+    Vue.prototype.$socket.send(JSON.stringify({
+      uuid: state.uuid,
+      userId: state.userId,
+      experimentId: state.experimentId,
+      msgId,
+      msg
+    }))
   },
 
   async loadScenesList({ getters: { isHostConfigured, getHostURI }, commit }) {

+ 2 - 0
src/store/index.js

@@ -13,6 +13,8 @@ const vuexLocal = new VuexPersistence({
   key: 'webexpe-state',
   reducer: state => ({
     uuid: state.uuid,
+    userId: state.userId,
+    experimentId: state.experimentId,
     gdprConsent: state.gdprConsent,
     hostConfig: state.hostConfig,
     scenesList: state.scenesList,

+ 9 - 2
src/store/mutations.js

@@ -36,6 +36,8 @@ export default {
     const defaultStateObj = defaultState()
     if (gdprConsent) {
       state.gdprConsent = false
+      delete state.userId
+      delete state.experimentId
       delete state.hostConfig
       delete state.progression
       delete state.scenesList
@@ -43,9 +45,9 @@ export default {
     }
 
     if (hostConfig) {
-      if (state.socket.isConnected)
-        this._vm.$disconnect()
       state.hostConfig = defaultStateObj.hostConfig
+      state.userId = defaultStateObj.userId
+      state.experimentId = defaultStateObj.experimentId
     }
     if (progression) {
       // Reset progression and recreate the progression object
@@ -58,6 +60,11 @@ export default {
     state.hostConfig = newConfig
   },
 
+  setUserExperimentId(state, { userId, experimentId }) {
+    state.userId = userId
+    state.experimentId = experimentId
+  },
+
   setListScenes(state, scenes) {
     state.scenesList = scenes
     createProgressionObj(state, scenes)

+ 2 - 0
src/store/state.js

@@ -1,6 +1,8 @@
 // Deep copy to not mutate it with the store (default state is needed when reloading after a refresh)
 export default () => JSON.parse(JSON.stringify({
   uuid: null,
+  userId: null,
+  experimentId: null,
   gdprConsent: false,
   hostConfig: {
     ssl: null,

+ 81 - 0
src/views/ExperimentValidated.vue

@@ -0,0 +1,81 @@
+<template>
+  <div>
+    <h2>Experiment "{{ experimentFullName }}"</h2>
+
+    <v-card>
+      <v-card-title primary-title>
+        <v-spacer />
+        <div class="headline">Experiment validated for the scene "{{ sceneName }}"</div>
+        <v-spacer />
+      </v-card-title>
+      <v-card-actions>
+        <v-spacer />
+        <v-btn flat exact to="/experiments/">
+          <v-icon left>home</v-icon>
+          Select another experiment
+        </v-btn>
+
+        <v-btn flat exact :to="`/experiments/${experimentName}`">
+          <v-icon left>arrow_back</v-icon>
+          Go back to scene selection
+        </v-btn>
+
+        <v-btn v-if="hasScenesLeft" flat exact :to="`/experiments/${experimentName}/${getRandomScene}`">
+          <v-icon left>shuffle</v-icon>
+          Continue with a random scene
+        </v-btn>
+        <v-spacer />
+      </v-card-actions>
+    </v-card>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import Experiments from '@/router/experiments'
+import { getExperimentSceneList } from '@/config.utils'
+import { rand } from '@/functions'
+
+export default {
+  name: 'ExperimentValidated',
+  props: {
+    experimentName: {
+      type: String,
+      required: true
+    },
+    sceneName: {
+      type: String,
+      required: true
+    }
+  },
+  data() {
+    return {
+      experimentFullName: null,
+      availableScenes: []
+    }
+  },
+  computed: {
+    ...mapState(['progression']),
+
+    hasScenesLeft() {
+      return this.availableScenes.length > 0
+    },
+    getRandomScene() {
+      return this.availableScenes[rand(0, this.availableScenes.length - 1)]
+    }
+  },
+  mounted() {
+    const scenesList = getExperimentSceneList(this.experimentName)
+
+    // Find the selected experiment full name
+    this.experimentFullName = Experiments.find(x => x.name === this.experimentName).meta.fullName
+
+    // Get a list of available and not already validated scenes for this experiment
+    this.availableScenes = Object.keys(this.progression[this.experimentName])
+      .filter(aScene =>
+        scenesList.includes(aScene) &&
+        this.progression[this.experimentName] &&
+        !this.progression[this.experimentName][aScene].done)
+  }
+}
+</script>

+ 6 - 6
src/views/Experiments/AreSameImagesRandom.vue

@@ -23,7 +23,7 @@
             <v-card dark color="primary">
               <v-card-text class="px-0">Image 1</v-card-text>
 
-              <v-img v-if="leftImage && leftImage.link" :src="leftImage.link">
+              <v-img v-if="image1 && image1.link" :src="image1.link">
                 <template v-slot:placeholder>
                   <v-layout fill-height align-center justify-center ma-0>
                     <v-progress-circular indeterminate color="grey lighten-5" />
@@ -36,7 +36,7 @@
             <v-card dark color="primary">
               <v-card-text>Image 2</v-card-text>
 
-              <v-img v-if="rightImage && rightImage.link" :src="rightImage.link" @load="scrollToChoiceButtons">
+              <v-img v-if="image2 && image2.link" :src="image2.link" @load="scrollToChoiceButtons">
                 <template v-slot:placeholder>
                   <v-layout fill-height align-center justify-center ma-0>
                     <v-progress-circular indeterminate color="grey lighten-5" />
@@ -99,10 +99,10 @@ export default {
     await this.getQualitiesList()
 
     // Load a test if not already one loaded
-    if (!this.leftImage || !this.leftImage.link || !this.rightImage || !this.rightImage.link) {
-      const { leftImage, rightImage } = await this.getRandomTest()
-      this.leftImage = leftImage
-      this.rightImage = rightImage
+    if (!this.image1 || !this.image1.link || !this.image2 || !this.image2.link) {
+      const { image1, image2 } = await this.getRandomTest()
+      this.image1 = image1
+      this.image2 = image2
     }
 
     this.saveProgress()

+ 8 - 7
src/views/Experiments/AreSameImagesReference.vue

@@ -23,7 +23,7 @@
             <v-card dark color="primary">
               <v-card-text class="px-0">Image 1</v-card-text>
 
-              <v-img v-if="leftImage && leftImage.link" :src="leftImage.link">
+              <v-img v-if="image1 && image1.link" :src="image1.link">
                 <template v-slot:placeholder>
                   <v-layout fill-height align-center justify-center ma-0>
                     <v-progress-circular indeterminate color="grey lighten-5" />
@@ -36,7 +36,7 @@
             <v-card dark color="primary">
               <v-card-text>Image 2</v-card-text>
 
-              <v-img v-if="rightImage && rightImage.link" :src="rightImage.link" @load="scrollToChoiceButtons">
+              <v-img v-if="image2 && image2.link" :src="image2.link" @load="scrollToChoiceButtons">
                 <template v-slot:placeholder>
                   <v-layout fill-height align-center justify-center ma-0>
                     <v-progress-circular indeterminate color="grey lighten-5" />
@@ -84,7 +84,8 @@ export default {
 
   data() {
     return {
-      experimentName: 'AreSameImagesReference'
+      experimentName: 'AreSameImagesReference',
+      referenceImagePosition: null
     }
   },
 
@@ -99,10 +100,10 @@ export default {
     await this.getQualitiesList()
 
     // Load a test if not already one loaded
-    if (!this.leftImage || !this.leftImage.link || !this.rightImage || !this.rightImage.link) {
-      const { leftImage, rightImage } = await this.getReferenceTest()
-      this.leftImage = leftImage
-      this.rightImage = rightImage
+    if (!this.image1 || !this.image1.link || !this.image2 || !this.image2.link) {
+      const { image1, image2 } = await this.getReferenceTest()
+      this.image1 = image1
+      this.image2 = image2
     }
 
     this.saveProgress()

+ 173 - 0
src/views/Experiments/AreSameImagesReferenceOneExtract.vue

@@ -0,0 +1,173 @@
+<template>
+  <div>
+    <v-container grid-list-md text-xs-center fluid>
+      <v-layout row wrap>
+        <v-flex xs12>
+          <v-layout justify-start>
+            <v-btn flat exact :to="`/experiments/${experimentName}`">
+              <v-icon left>arrow_back</v-icon>
+              Back to scene selection
+            </v-btn>
+          </v-layout>
+
+          <h2>Experiment "{{ $route.meta.fullName }}"</h2>
+          <h3>{{ sceneName }}</h3>
+        </v-flex>
+        <!-- Loading screen -->
+        <loader v-if="loadingMessage" :message="loadingMessage" />
+        <!--/ Loading screen -->
+
+        <!-- Experiment -->
+        <template v-else-if="!loadingErrorMessage && image1 && image2">
+          <v-flex xs12 sm6>
+            <v-card dark color="primary">
+              <v-card-text class="px-0">Image 1</v-card-text>
+
+              <v-container v-if="imageOneExtractPosition === 'left'" class="pa-1">
+                <ExtractsToImage :extracts="image1" :extract-config="extractConfig" />
+              </v-container>
+              <v-img v-else :src="image2.link" @load="scrollToChoiceButtons">
+                <template v-slot:placeholder>
+                  <v-layout fill-height align-center justify-center ma-0>
+                    <v-progress-circular indeterminate color="grey lighten-5" />
+                  </v-layout>
+                </template>
+              </v-img>
+            </v-card>
+          </v-flex>
+          <v-flex xs12 sm6>
+            <v-card dark color="primary">
+              <v-card-text>Image 2</v-card-text>
+              <v-container v-if="imageOneExtractPosition === 'right'" class="pa-1">
+                <ExtractsToImage :extracts="image1" :extract-config="extractConfig" />
+              </v-container>
+              <v-img v-else :src="image2.link" @load="scrollToChoiceButtons">
+                <template v-slot:placeholder>
+                  <v-layout fill-height align-center justify-center ma-0>
+                    <v-progress-circular indeterminate color="grey lighten-5" />
+                  </v-layout>
+                </template>
+              </v-img>
+            </v-card>
+          </v-flex>
+
+
+          <!-- Experiment validation button -->
+          <v-layout justify-center align-content-center>
+            <div id="choice">
+              <v-container grid-list-md text-xs-center fluid>
+                <h2>Test {{ testCount }} / {{ maxTestCount }}</h2>
+                <v-layout row wrap>
+                  <v-flex sm6 xs12>
+                    <v-btn @click="areTheSameActionLocal(false)" color="error" large>Images are NOT the same</v-btn>
+                  </v-flex>
+                  <v-flex sm6 xs12>
+                    <v-btn @click="areTheSameActionLocal(true)" color="success" large>Images are the same</v-btn>
+                  </v-flex>
+                </v-layout>
+              </v-container>
+            </div>
+          </v-layout>
+          <!--/ Experiment validation button -->
+        </template>
+        <!--/ Experiment -->
+      </v-layout>
+    </v-container>
+  </div>
+</template>
+
+<script>
+import ExperimentBaseExtracts from '@/mixins/ExperimentBaseExtracts'
+import ExperimentBaseAreSameImages from '@/mixins/ExperimentBaseAreSameImages'
+import Loader from '@/components/Loader'
+import ExtractsToImage from '@/components/ExperimentsComponents/ExtractsToImage'
+import { rand } from '@/functions'
+
+export default {
+  name: 'AreSameImagesReferenceOneExtract',
+  components: {
+    Loader,
+    ExtractsToImage
+  },
+  mixins: [
+    ExperimentBaseExtracts,
+    ExperimentBaseAreSameImages
+  ],
+
+  data() {
+    return {
+      experimentName: 'AreSameImagesReferenceOneExtract',
+
+      imageOneExtractPosition: null,
+      randomZoneIndex: null,
+      randomZoneQuality: null
+    }
+  },
+
+  async mounted() {
+    // Load config for this scene to local state
+    this.loadConfig()
+
+    // Load progress from store into local state
+    this.loadProgress()
+
+    // Load scene data from the API
+    await this.getQualitiesList()
+
+    // Load a test if not already one loaded
+    if (!this.image1 || !this.image2) {
+      const { image1, image2 } = await this.getReferenceOneExtractTest()
+      this.image1 = image1
+      this.image2 = image2
+    }
+
+    this.saveProgress()
+  },
+
+  methods: {
+    // Get a test with one random quality and a reference
+    async getReferenceOneExtractTest() {
+      // Randomly choose a quality for the extract
+      const randomQuality = this.qualities[rand(0, this.qualities.length - 1)]
+
+      const maxQuality = this.qualities[this.qualities.length - 1]
+
+      // Get the reference image, extracts of reference image and random quality extracts
+      const [maxExtracts, randomExtracts, maxImage] = await Promise.all([
+        this.getExtracts('max'),
+        this.getExtracts(randomQuality),
+        this.getImage(maxQuality)
+      ])
+
+      // Select which zone is the random extract (-1 to get array index)
+      const randomZoneIndex = rand(0, maxExtracts.extracts.length - 1)
+      // Apply the random quality extract
+      maxExtracts.extracts[randomZoneIndex] = randomExtracts.extracts[randomZoneIndex]
+
+      // Fix uris
+      const referenceWithOneExtract = maxExtracts.extracts.map(url => this.getHostURI + url)
+      maxImage.link = this.getHostURI + maxImage.link
+
+      // Backup test data
+      this.randomZoneIndex = randomZoneIndex
+      this.randomZoneQuality = randomQuality
+      this.imageOneExtractPosition = rand(0, 1) === 0 ? 'left' : 'right'
+
+      return {
+        image1: referenceWithOneExtract,
+        image2: maxImage
+      }
+    },
+
+    areTheSameActionLocal(areTheSame) {
+      const additionalData = {
+        imageOneExtractPosition: this.imageOneExtractPosition,
+        randomZoneIndex: this.randomZoneIndex,
+        randomZone: this.randomZoneIndex + 1,
+        randomZoneQuality: this.randomZoneQuality
+      }
+      this.areTheSameAction(areTheSame, this.getReferenceOneExtractTest, additionalData)
+    }
+  }
+}
+</script>

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

@@ -129,7 +129,7 @@ export default {
     // Load the cached configuration in the configurator component
     if (this.lockConfig === false) this.$refs.configurator.setDefaultConfig(this.extractConfig)
 
-    // Load extracts of none were cached
+    // Load extracts if none were cached
     if (this.extracts.length === 0) await this.setExtractConfig(this.extractConfig, this.$refs.configurator)
 
     this.saveProgress()

+ 6 - 4
src/views/ExperimentsList.vue

@@ -39,6 +39,7 @@
 <script>
 import { mapState } from 'vuex'
 import Experiments from '@/router/experiments'
+import { getExperimentSceneList } from '@/config.utils'
 
 export default {
   name: 'ExperimentsList',
@@ -55,20 +56,21 @@ export default {
     }
   },
   computed: {
-    ...mapState(['scenesList', 'progression'])
+    ...mapState(['progression'])
   },
   mounted() {
     this.items = Experiments.map(expe => {
+      const scenesList = getExperimentSceneList(expe.name)
       const res = {
         name: expe.meta.fullName,
         link: `/experiments/${expe.name}`
       }
       // Check cache has an entry for each scenes in this experiment
-      if (this.progression[expe.name] && Object.keys(this.progression[expe.name]).every(y => this.scenesList.includes(y))) {
+      if (this.progression && this.progression[expe.name]) {
         // Set experiment completion percentage
         const numberOfDoneScenes = Object.keys(this.progression[expe.name]).filter(y => this.progression[expe.name][y].done).length
-        const percentage = Math.round(numberOfDoneScenes / this.scenesList.length * 100)
-        res.completion = `${percentage}%`
+        const percentage = Math.round(numberOfDoneScenes / scenesList.length * 100)
+        res.completion = `${numberOfDoneScenes}/${scenesList.length} - ${percentage}%`
       }
       else res.completion = '0%'
 

+ 59 - 2
src/views/HostConfig.vue

@@ -33,9 +33,45 @@
                     required
                   />
 
+                  <v-layout row wrap>
+                    <v-flex xs5>
+                      <v-checkbox
+                        v-model="id.hasUserId"
+                        color="primary"
+                        :label="`I have an user ID`"
+                      />
+                    </v-flex>
+                    <v-spacer />
+                    <v-flex xs6>
+                      <v-text-field
+                        v-model="id.user"
+                        label="User ID"
+                        type="text"
+                        :disabled="!id.hasUserId"
+                      />
+                    </v-flex>
+                  </v-layout>
 
-                  <v-btn color="error" @click="reset">Reset Form</v-btn>
+                  <v-layout row wrap>
+                    <v-flex xs5>
+                      <v-checkbox
+                        v-model="id.hasExperimentId"
+                        color="primary"
+                        :label="`I have an experiment ID`"
+                      />
+                    </v-flex>
+                    <v-spacer />
+                    <v-flex xs6>
+                      <v-text-field
+                        v-model="id.experiment"
+                        label="Experiment ID"
+                        type="text"
+                        :disabled="!id.hasExperimentId"
+                      />
+                    </v-flex>
+                  </v-layout>
 
+                  <v-btn color="error" @click="reset">Reset Form</v-btn>
                   <v-btn color="success" @click="validate">Submit</v-btn>
 
                   <v-slide-y-transition mode="out-in">
@@ -68,6 +104,13 @@ export default {
         port: '80'
       },
 
+      id: {
+        user: null,
+        hasUserId: false,
+        experiment: null,
+        hasExperimentId: false
+      },
+
       loadingMessage: null,
       configErrorMessage: null
     }
@@ -79,12 +122,25 @@ export default {
     }
   },
 
+  mounted() {
+    if (process.env.NODE_ENV === 'development')
+      this.config = {
+        ssl: false,
+        host: 'localhost',
+        port: '5000'
+      }
+  },
+
   methods: {
-    ...mapActions(['setHostConfig']),
+    ...mapActions(['setHostConfig', 'setUserExperimentId']),
     reset() {
       this.config.ssl = true
       this.config.host = ''
       this.config.port = null
+      this.id.user = null
+      this.id.hasUserId = false
+      this.id.experiment = null
+      this.id.hasExperimentId = false
       this.configErrorMessage = null
       this.$refs.form.reset()
     },
@@ -95,6 +151,7 @@ export default {
       this.configErrorMessage = null
       try {
         await this.setHostConfig(this.config)
+        this.setUserExperimentId({ userId: this.id.user, experimentId: this.id.experiment })
       }
       catch (err) {
         console.error(err)

+ 18 - 4
src/views/SelectExperimentScene.vue

@@ -7,7 +7,8 @@
       </v-btn>
     </v-layout>
 
-    Select a scene for the experiment "{{ experimentFullName }}"
+    <h4>Select a scene for the experiment "{{ experimentFullName }}"</h4>
+    <span>Completion: {{ numberOfValidatedScenes }}/{{ numberOfScenes }} - {{ completionPercent }}%</span>
 
     <v-card>
       <v-container
@@ -65,6 +66,7 @@
 import { mapState, mapGetters } from 'vuex'
 import Experiments from '@/router/experiments'
 import { API_ROUTES, shuffleArray } from '@/functions'
+import { getExperimentSceneList } from '@/config.utils'
 
 export default {
   name: 'SelectExperimentScene',
@@ -81,10 +83,22 @@ export default {
     }
   },
   computed: {
-    ...mapState(['scenesList', 'progression']),
-    ...mapGetters(['getHostURI'])
+    ...mapState(['progression']),
+    ...mapGetters(['getHostURI']),
+
+    numberOfScenes() {
+      return this.scenes.length
+    },
+    numberOfValidatedScenes() {
+      return this.scenes.filter(x => x.progression === 'done').length
+    },
+    completionPercent() {
+      return Math.round(this.numberOfValidatedScenes / this.numberOfScenes * 100)
+    }
   },
   async mounted() {
+    const scenesList = getExperimentSceneList(this.experimentName)
+
     // Find the selected experiment full name
     this.experimentFullName = Experiments.find(x => x.name === this.experimentName).meta.fullName
 
@@ -93,7 +107,7 @@ export default {
     let working = []
     let done = []
 
-    for (const aScene of this.scenesList) {
+    for (const aScene of scenesList) {
       const { data: thumb } = await fetch(`${this.getHostURI}${API_ROUTES.getImage(aScene, 'max')}`)
         .then(res => res.json())