├── composer.json ├── .babelrc ├── .gitignore ├── Procfile ├── src ├── partials │ ├── footer.hbs │ └── header.hbs ├── sass │ ├── components │ │ ├── fonts.scss │ │ ├── variables.scss │ │ └── overlay.scss │ └── styles.scss ├── assets │ ├── mma.png │ └── mmt.png ├── backend │ ├── room_marker_map_1.json │ ├── room_marker_map.json │ ├── roomsort.php │ └── index.php ├── js │ ├── app.js │ └── components │ │ ├── rotate-device.js │ │ ├── arjs-config.js │ │ ├── roomplan.js │ │ └── utils.js └── index.hbs ├── .buildpacks ├── .eslintrc.json ├── README.md ├── package.json └── gulpfile.babel.js /composer.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .tmp/ 3 | public/ 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: vendor/bin/heroku-php-apache2 public/ 2 | -------------------------------------------------------------------------------- /src/partials/footer.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/sass/components/fonts.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Futura,"Trebuchet MS",Arial,sans-serif; 3 | } -------------------------------------------------------------------------------- /src/assets/mma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freinbichler/ar-room-availability/HEAD/src/assets/mma.png -------------------------------------------------------------------------------- /src/assets/mmt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freinbichler/ar-room-availability/HEAD/src/assets/mmt.png -------------------------------------------------------------------------------- /.buildpacks: -------------------------------------------------------------------------------- 1 | https://github.com/heroku/heroku-buildpack-nodejs.git#v106 2 | https://github.com/heroku/heroku-buildpack-php.git#v121 3 | -------------------------------------------------------------------------------- /src/backend/room_marker_map_1.json: -------------------------------------------------------------------------------- 1 | {"U HS 155":0,"U PR U10":1,"U PR U11":2,"U HS 154":3,"U HS 153":4,"U HS 151":5,"U HS 152":6,"U HS 110":7} -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "plugins": [ 4 | "import" 5 | ], 6 | "env": { 7 | "browser": true 8 | }, 9 | "rules": { 10 | "no-underscore-dangle": "off" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | import Roomplan from './components/roomplan'; 2 | import ARJSConfig from './components/arjs-config'; 3 | import RotateDevice from './components/rotate-device'; 4 | 5 | const arjsConfig = new ARJSConfig(); 6 | const rotateDevice = new RotateDevice(); 7 | const roomplan = new Roomplan(); 8 | -------------------------------------------------------------------------------- /src/sass/components/variables.scss: -------------------------------------------------------------------------------- 1 | $grid-breakpoints: ( 2 | xs: 0px, 3 | sm: 576px, 4 | md: 768px, 5 | lg: 992px, 6 | xl: 1200px 7 | ); 8 | 9 | // sass-mq 10 | $mq-breakpoints: $grid-breakpoints; 11 | $mq-show-breakpoints: (); 12 | @if($production == true) { 13 | $mq-show-breakpoints: (); 14 | } 15 | -------------------------------------------------------------------------------- /src/backend/room_marker_map.json: -------------------------------------------------------------------------------- 1 | {"U PR 363":0,"U SE 333":1,"U PR 362":2,"U LB 374":3,"U LB 372":4,"U SE 353":5,"U SE 354":6,"U SE 358":7,"U SE 357":8,"U SE 356":9,"U SE 355":10,"U LB 370":11,"U LB 365":12,"U LB 315":13,"U LB 314":14,"U LB 313":15,"U BZ 326":16,"U LB 316":17,"U LB 351":18,"U LB 364":19,"U LB 360":20,"U LB 359 elements":21,"U LB 352":22,"U BZ 301":23} -------------------------------------------------------------------------------- /src/sass/styles.scss: -------------------------------------------------------------------------------- 1 | // variables 2 | @import 'components/variables'; 3 | @import '../../node_modules/bootstrap/scss/variables'; 4 | 5 | // bootstrap 6 | @import '../../node_modules/bootstrap/scss/mixins'; 7 | @import '../../node_modules/bootstrap/scss/normalize'; 8 | @import '../../node_modules/bootstrap/scss/reboot'; 9 | @import '../../node_modules/bootstrap/scss/type'; 10 | 11 | @import '../../node_modules/sass-mq/mq'; 12 | 13 | // components 14 | @import 'components/overlay'; 15 | -------------------------------------------------------------------------------- /src/js/components/rotate-device.js: -------------------------------------------------------------------------------- 1 | export default class RotateDevice { 2 | constructor() { 3 | this.overlay = document.querySelector('.overlay-rotate-device'); 4 | 5 | this.listen(); 6 | } 7 | 8 | listen() { 9 | window.addEventListener('resize', () => { 10 | if (window.innerWidth > window.innerHeight) { 11 | this.overlay.style.display = 'none'; 12 | } else { 13 | this.overlay.style.display = 'flex'; 14 | } 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/partials/header.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AR Room Availability 7 | 8 | 9 | 10 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/js/components/arjs-config.js: -------------------------------------------------------------------------------- 1 | export default class ARJSConfig { 2 | constructor() { 3 | this.windowWidth = window.innerWidth; 4 | this.windowHeight = window.innerHeight; 5 | 6 | window.setTimeout(this.configure.bind(this), 2000); 7 | } 8 | 9 | configure() { 10 | window.dispatchEvent(new Event('resize')); 11 | document.querySelector('.a-enter-vr').style.display = 'none'; 12 | document.querySelector('a-scene').onclick = () => { 13 | window.dispatchEvent(new Event('resize')); 14 | }; 15 | window.addEventListener('resize', () => { 16 | this.setResizeTimeout(this.windowWidth, this.windowHeight); 17 | }); 18 | } 19 | 20 | setResizeTimeout(oldWidth, oldHeight) { 21 | if (oldWidth === window.innerWidth && oldHeight === window.innerHeight) return; 22 | this.windowWidth = window.innerWidth; 23 | this.windowHeight = window.innerHeight; 24 | window.setTimeout(() => { 25 | window.dispatchEvent(new Event('resize')); 26 | }, 700); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/sass/components/overlay.scss: -------------------------------------------------------------------------------- 1 | .overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | color: #FFF; 8 | z-index: 100; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | padding: 30px; 13 | flex-direction: column; 14 | 15 | &.overlay-rotate-device { 16 | background: tomato; 17 | z-index: 101; 18 | 19 | svg { 20 | width: 200px; 21 | height: auto; 22 | margin-bottom: 30px; 23 | } 24 | } 25 | 26 | &.overlay-loading { 27 | background: #1D9EE0; 28 | z-index: 102; 29 | transition: opacity .5s, visibility .5s; 30 | 31 | .spinner { 32 | display: inline-block; 33 | width: 50px; 34 | height: 50px; 35 | border: 3px solid rgba(255, 255, 255, .3); 36 | border-radius: 50%; 37 | border-top-color: #fff; 38 | animation: spin 1s ease-in-out infinite; 39 | opacity: 1; 40 | } 41 | 42 | &.hidden { 43 | opacity: 0; 44 | visibility: hidden; 45 | } 46 | } 47 | } 48 | 49 | @keyframes spin { 50 | to { transform: rotate(360deg); } 51 | } 52 | -------------------------------------------------------------------------------- /src/backend/roomsort.php: -------------------------------------------------------------------------------- 1 | id, $r2->id); 28 | } 29 | usort($rooms, 'cmp'); 30 | 31 | $marker = 0; 32 | foreach($rooms as $r) { 33 | $roomMarkerMap[$r['name']] = $marker; 34 | $marker++; 35 | } 36 | 37 | 38 | echo json_encode($roomMarkerMap); 39 | ?> 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AR Room Availability 2 | > A web-based augmented reality application to visualize room availability 3 | 4 | ## Demo 5 | See https://twitter.com/fr3ino/status/880108156982108160 6 | 7 | ## Technologies 8 | * AR.js: https://github.com/jeromeetienne/AR.js 9 | * A-Frame: https://aframe.io 10 | 11 | ## Structure 12 | All source files belong to the `src` folder. The page itself is served off the `public` folder. 13 | 14 | Gulp tasks deploy your compiled and packed styles (one `styles.css`) and scripts (`app.js`) to this public folder either uncompressed with sourcemaps as default or compressed and without sourcemaps for production (use `--production` argument to gulp tasks). 15 | 16 | ## Installation 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | After that execute `gulp serve`, point your browser to http://localhost:3000 and start adding and editing files in `src`. 22 | 23 | ## Gulp Tasks 24 | * `gulp serve` - starts Browsersync and serves your app for testing in different browsers (default: http://localhost:3000, Browsersync-UI at http://localhost:3001), after changes in SCSS, JS and HTML files in `src` the page is automatically refreshed 25 | * `gulp build` - executes all tasks, but does not start a browsersync server 26 | 27 | 28 | Add `--production` to any gulp task to activate production mode. In production mode all code will be minified and no sourcemaps are written. 29 | 30 | This boilerplate is based on https://github.com/freinbichler/es6-sass-boilerplate 31 | -------------------------------------------------------------------------------- /src/backend/index.php: -------------------------------------------------------------------------------- 1 | $marker) { 19 | $activities[$marker] = $markerObject; 20 | $activities[$marker]['room'] = $room; 21 | } 22 | 23 | foreach($json['activities'] as $a) { 24 | $roomNumber = preg_replace('/[^0-9]/', '', $a['__row']['roomName']); 25 | 26 | if($a['__row']['campus'] == '4' && $roomNumber[0] == '3') { // only third floor 27 | $marker = $roomMarkerMap[$a['__row']['roomName']]; 28 | 29 | $activity['begin'] = $a['__row']['activityBegin']; 30 | $activity['end'] = $a['__row']['activityEnd']; 31 | $activity['name'] = $a['__row']['activityName']; 32 | $activity['lecturer'] = str_replace('*', '', $a['__row']['lecturerName']); 33 | $activity['room'] = $a['__row']['roomName']; 34 | $activity['faculty'] = $a['__row']['facultyName']; 35 | $activity['campus'] = (int)$a['__row']['campus']; 36 | $activity['marker'] = $marker; 37 | 38 | array_push($activities[$marker]['activities'], $activity); 39 | } 40 | } 41 | 42 | echo json_encode((object)$activities); 43 | 44 | ?> 45 | -------------------------------------------------------------------------------- /src/index.hbs: -------------------------------------------------------------------------------- 1 | {{> header}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | Please rotate your device 20 |
21 | 22 | {{> footer}} 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ar-room-availability", 3 | "version": "1.0.0", 4 | "description": "see if rooms are empty or occupied by scanning the door plate", 5 | "main": "public/index.html", 6 | "author": "Marcel Freinbichler", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "serve": "node_modules/gulp/bin/gulp.js serve", 11 | "build": "node_modules/gulp/bin/gulp.js build --production", 12 | "postinstall": "npm run build" 13 | }, 14 | "dependencies": { 15 | "bootstrap": "4.0.0-alpha.5", 16 | "humanize-duration": "^3.10.0", 17 | "jquery": "^3.1.1", 18 | "moment": "^2.18.1", 19 | "moment-range": "^3.0.3", 20 | "sass-mq": "^3.2.3", 21 | "babel-cli": "^6.18.0", 22 | "babel-core": "^6.21.0", 23 | "babel-preset-es2015": "^6.18.0", 24 | "babelify": "^7.3.0", 25 | "browser-sync": "^2.18.5", 26 | "browserify": "^13.1.1", 27 | "browserify-shim": "^3.8.10", 28 | "del": "^2.2.2", 29 | "eslint": "^3.19.0", 30 | "eslint-config-airbnb-base": "^11.1.3", 31 | "eslint-plugin-import": "^2.2.0", 32 | "gulp": "^3.9.1", 33 | "gulp-autoprefixer": "^3.1.1", 34 | "gulp-cache": "^0.4.5", 35 | "gulp-changed": "^1.3.0", 36 | "gulp-compile-handlebars": "^0.6.1", 37 | "gulp-concat": "^2.6.0", 38 | "gulp-if": "^2.0.2", 39 | "gulp-load-plugins": "^1.4.0", 40 | "gulp-minify-css": "^1.2.0", 41 | "gulp-minify-html": "^1.0.4", 42 | "gulp-rename": "^1.2.2", 43 | "gulp-sass": "^3.0.0", 44 | "gulp-sass-variables": "^1.1.1", 45 | "gulp-size": "^2.1.0", 46 | "gulp-sourcemaps": "^1.5.2", 47 | "gulp-uglify": "^2.0.0", 48 | "gulp-util": "^3.0.8", 49 | "path": "^0.12.7", 50 | "run-sequence": "^1.1.2", 51 | "vinyl-buffer": "^1.0.0", 52 | "vinyl-source-stream": "^1.1.0", 53 | "yargs": "^6.6.0" 54 | }, 55 | "browserify": { 56 | "transform": [ 57 | "browserify-shim" 58 | ] 59 | }, 60 | "browser": {}, 61 | "browserify-shim": {}, 62 | "engines": { 63 | "node": "8.x", 64 | "npm": "5.x" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import gulploadplugins from 'gulp-load-plugins'; 3 | import runSequence from 'run-sequence'; 4 | import yargs from 'yargs'; 5 | import browserSync from 'browser-sync'; 6 | import browserify from 'browserify'; 7 | import babelify from 'babelify'; 8 | import source from 'vinyl-source-stream'; 9 | import buffer from 'vinyl-buffer'; 10 | import path from 'path'; 11 | import del from 'del'; 12 | import handlebars from 'gulp-compile-handlebars'; 13 | 14 | const $ = gulploadplugins({ 15 | lazy: true, 16 | }); 17 | 18 | const argv = yargs.argv; 19 | 20 | function handleError(error) { 21 | $.util.log(error.message); 22 | $.util.log(error.codeFrame); 23 | this.emit('end'); 24 | } 25 | 26 | // SASS Styles 27 | gulp.task('styles', () => gulp.src([ 28 | 'src/vendor/**/*.css', 29 | 'src/sass/*.scss', 30 | ]) 31 | .pipe($.changed('.tmp/styles', { extension: '.css' })) 32 | .pipe($.if(!argv.production, $.sourcemaps.init())) 33 | .pipe($.sassVariables({ 34 | $production: (argv.production === true), 35 | })) 36 | .pipe($.sass({ 37 | precision: 10, 38 | }).on('error', $.sass.logError)) 39 | .pipe($.autoprefixer({ browsers: ['> 1%', 'last 2 versions'] })) 40 | .pipe(gulp.dest('.tmp')) 41 | // Concatenate and minify styles if production mode (via gulp styles --production) 42 | .pipe($.if('*.css' && argv.production, $.minifyCss())) 43 | .pipe($.if(!argv.production, $.sourcemaps.write())) 44 | .pipe(gulp.dest('public/css')) 45 | .pipe(browserSync.stream()) 46 | .pipe($.size({ title: 'styles' }))); 47 | 48 | // Scripts - app.js is the main entry point, you have to import all required files and modules 49 | gulp.task('scripts', () => browserify({ 50 | entries: 'src/js/app.js', 51 | debug: true, 52 | }) 53 | .transform('babelify', { presets: ['es2015'] }) 54 | .bundle() 55 | .on('error', handleError) 56 | .pipe(source('app.js')) 57 | .pipe(buffer()) 58 | // .pipe($.if(!argv.production, $.sourcemaps.init({ loadMaps: true }))) 59 | // .pipe($.if(argv.production, $.uglify())) 60 | // .on('error', handleError) 61 | // .pipe($.if(!argv.production, $.sourcemaps.write())) 62 | .pipe(gulp.dest('./public/js'))); 63 | 64 | gulp.task('vendor', () => gulp.src('src/js/vendor/*').pipe(gulp.dest('public/js/vendor'))); 65 | 66 | gulp.task('static', () => gulp.src('src/**/*.{html,php,jpg,jpeg,png,gif,svg,ico,eot,ttf,woff,woff2,obj,json}').pipe(gulp.dest('public'))); 67 | 68 | gulp.task('templates', () => gulp.src([ 69 | 'src/**/*.hbs', 70 | '!src/partials/*.hbs', 71 | ]) 72 | .pipe(handlebars({}, { 73 | batch: 'src/partials', 74 | })) 75 | .pipe($.rename((path) => { 76 | path.extname = '.html'; 77 | })) 78 | .pipe(gulp.dest('public'))); 79 | 80 | // Browser-Sync 81 | gulp.task('serve', ['styles', 'scripts', 'templates', 'static', 'vendor'], () => { 82 | browserSync({ 83 | notify: false, 84 | server: ['.tmp', 'public'], 85 | https: true, 86 | }); 87 | 88 | gulp.watch(['src/sass/**/*.{scss,css}'], ['styles']); 89 | gulp.watch(['src/js/**/*.{js,es6}'], ['scripts', browserSync.reload]); 90 | gulp.watch(['src/**/*.hbs'], ['templates', browserSync.reload]); 91 | gulp.watch(['src/js/vendor/*'], ['vendor', browserSync.reload]); 92 | gulp.watch(['src/**/*.{html,php,jpg,jpeg,png,gif,svg,ico,eot,ttf,woff,woff2,obj,json}'], ['static', browserSync.reload]).on('change', (event) => { 93 | if (event.type === 'deleted') { 94 | const filePathFromSrc = path.relative(path.resolve('src'), event.path); 95 | const destFilePath = path.resolve('public', filePathFromSrc); 96 | console.log(`deleting ${destFilePath}...`); 97 | del.sync(destFilePath); 98 | } 99 | }); 100 | }); 101 | 102 | gulp.task('build', ['styles', 'scripts', 'templates', 'static', 'vendor']); 103 | -------------------------------------------------------------------------------- /src/js/components/roomplan.js: -------------------------------------------------------------------------------- 1 | import { 2 | createMarker, 3 | createText, 4 | createFrame, 5 | createBox, 6 | createBadge, 7 | calculateAvailability, 8 | loadJSON, 9 | } from './utils'; 10 | 11 | export default class Roomplan { 12 | constructor() { 13 | this.scene = document.querySelector('#a-scene'); 14 | this.markers = []; 15 | 16 | // load the marker from the server 17 | const response = loadJSON('/backend/'); 18 | response.then((m) => { 19 | const loadingScreen = document.querySelector('.overlay-loading'); 20 | loadingScreen.classList.add('hidden'); 21 | this.markerData = m; 22 | this.init(); 23 | }); 24 | } 25 | 26 | init() { 27 | const markers = this.markerData; 28 | // console.log(markers); 29 | 30 | Object.keys(markers).forEach((key) => { 31 | let counter = -2; 32 | const marker = createMarker(key); 33 | 34 | // logic for each room 35 | const { free, duration } = calculateAvailability(markers[key].activities); 36 | const textObjects = this.getTextObjects({ free, duration, marker: key }); 37 | 38 | // a frame objects 39 | const frame = createFrame(free); 40 | const textBg = createBox({ position: '0 0 0', depth: '1.15', width: '1.15', height: '0.1', color: '#fff' }); 41 | marker.appendChild(textBg); 42 | frame.map(box => (marker.appendChild(box))); 43 | textObjects.map(obj => (marker.appendChild(obj))); 44 | 45 | // if there are marker activites 46 | markers[key].activities.map((activity) => { // eslint-disable-line 47 | const now = window.moment(); 48 | // const now = window.moment('2017-06-19 08:30:00'); 49 | const toShow = !!(now.isBetween(activity.begin, activity.end) || now.isBefore(activity.begin)); 50 | // if activity is currently taking place or later in the day 51 | if (toShow) { 52 | const activityObj = this.getActivityObject({ activity, y: counter }); 53 | activityObj.map(a => (marker.appendChild(a))); 54 | counter += 1.4; 55 | } 56 | }); 57 | 58 | // append to scene and save markers to edit on refresh 59 | this.scene.appendChild(marker); 60 | this.markers.push(marker); 61 | }); 62 | 63 | // refresh every minute 64 | this.refresh(); 65 | } 66 | 67 | getTextObjects({ free, duration, marker }) { 68 | const titleText = free ? 'FREE' : 'OCCUPIED'; 69 | const durationText = duration.humanizedDuration; 70 | const titleEl = createText({ position: '1 0.25 1.2', text: titleText, color: '#fff', id: 'title' }); 71 | const durationEl = createText({ position: '1 0.25 1.6', text: durationText, color: '#fff', size: 3, id: 'duration' }); 72 | const roomEl = createText({ position: '0 0.15 0', text: this.markerData[marker].room, color: '#000', size: 2 }); 73 | return [titleEl, durationEl, roomEl]; 74 | } 75 | 76 | getActivityObject({ activity, y }) { // eslint-disable-line 77 | let badgeEl; 78 | const timeStart = window.moment(activity.begin).format('HH:mm'); 79 | const timeEnd = window.moment(activity.end).format('HH:mm'); 80 | const faculty = activity.faculty.substring(0, 3); 81 | 82 | // create a frame elements 83 | const bg = createBox({ position: `-3 0 ${y}`, depth: '1.2', width: '3', height: '0.1', color: '#fff' }); 84 | const titleEl = createText({ position: `-3.4 0.25 ${y - 0.3}`, text: activity.name, color: '#000', align: 'left', size: 1.5 }); 85 | const lecturerEl = createText({ position: `-3.4 0.25 ${y}`, text: activity.lecturer, color: '#000', align: 'left', size: 1.5 }); 86 | const timeEl = createText({ position: `-3.4 0.25 ${y + 0.3}`, text: `${timeStart} - ${timeEnd}`, color: '#000', size: 1.5, align: 'left' }); 87 | if (faculty.indexOf('MMA') !== -1 || faculty.indexOf('MMT') !== -1) { 88 | badgeEl = createBadge({ position: `-3.9 0.25 ${y}`, src: `#${faculty.toLowerCase()}` }); 89 | } else { 90 | badgeEl = createText({ position: `-3.9 0.25 ${y}`, text: faculty, color: '#000', size: 5 }); 91 | } 92 | return [titleEl, timeEl, badgeEl, lecturerEl, bg]; 93 | } 94 | 95 | stopRefresh() { 96 | clearInterval(this.timer); 97 | } 98 | 99 | refresh() { 100 | this.timer = setInterval(() => { 101 | this.markers.map((marker) => { // eslint-disable-line 102 | const titleEl = marker.querySelector('#title'); 103 | const durationEl = marker.querySelector('#duration'); 104 | const boxes = marker.querySelectorAll('#frame'); 105 | const key = marker.getAttribute('value'); 106 | const { free, duration } = calculateAvailability(this.markerData[key].activities); 107 | const color = free ? '#00AA55' : 'tomato'; 108 | const newObjs = this.getTextObjects({ free, duration, marker: key }); 109 | 110 | // refresh title & duration value 111 | titleEl.setAttribute('value', newObjs[0].getAttribute('value')); 112 | durationEl.setAttribute('value', newObjs[1].getAttribute('value')); 113 | 114 | // change frame color 115 | [...boxes].map((box) => { // eslint-disable-line 116 | box.setAttribute('color', color); 117 | }); 118 | }); 119 | }, 5000); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/js/components/utils.js: -------------------------------------------------------------------------------- 1 | import Moment from 'moment'; 2 | import { extendMoment } from 'moment-range'; 3 | import humanizeDuration from 'humanize-duration'; 4 | 5 | window.moment = extendMoment(Moment); 6 | 7 | export function setAttributes(el, attrs) { 8 | for (const key in attrs) { 9 | el.setAttribute(key, attrs[key]); 10 | } 11 | } 12 | 13 | export function createMarker(value) { 14 | const marker = document.createElement('a-marker'); 15 | setAttributes(marker, { 16 | type: 'barcode', 17 | value, 18 | }); 19 | return marker; 20 | } 21 | 22 | export function createText({ text, color = '#000', position = '-2 0 -3', size = 4, align = 'center', id }) { 23 | const textObject = document.createElement('a-text'); 24 | setAttributes(textObject, { 25 | id, 26 | value: text, 27 | width: size, 28 | font: 'aileronsemibold', 29 | 'wrap-count': 20, 30 | rotation: '-90 0 0', 31 | align, 32 | position, 33 | color, 34 | }); 35 | return textObject; 36 | } 37 | 38 | export function createBox({ position, depth, width, height, color, id }) { 39 | const box = document.createElement('a-box'); 40 | setAttributes(box, { position, depth, width, height, color, id }); 41 | return box; 42 | } 43 | 44 | export function createBadge({ position, src }) { 45 | const badge = document.createElement('a-image'); 46 | setAttributes(badge, { position, src, rotation: '-90 0 0', width: 0.5, height: 0.5 }); 47 | return badge; 48 | } 49 | 50 | export function createFrame(isFree) { 51 | const boxes = []; 52 | const color = isFree ? '#00AA55' : 'tomato'; 53 | boxes.push(createBox({ position: '-0.8 0 -1.05', depth: '4', width: '0.3', height: '0.3111', color, id: 'frame' })); 54 | boxes.push(createBox({ position: '1.05 0 1.4', depth: '1.5', width: '4', height: '0.3112', color, id: 'frame' })); 55 | boxes.push(createBox({ position: '2.9 0 -1.05', depth: '4', width: '0.3', height: '0.3111', color, id: 'frame' })); 56 | boxes.push(createBox({ position: '1.05 0 -2.9', depth: '0.3', width: '4', height: '0.3112', color, id: 'frame' })); 57 | return boxes; 58 | } 59 | 60 | export function calculateAvailabilityDuration(free, roomActivities, index) { 61 | let duration = 'all day'; 62 | const now = window.moment(); 63 | 64 | if (free) { 65 | // if room is currently free, calculate time until next activity 66 | if (roomActivities.length > 0) { 67 | // time to next activity 68 | for (const activity of roomActivities) { 69 | if (now.isBefore(activity.begin)) { 70 | duration = window.moment.range( 71 | now, 72 | window.moment(activity.begin), 73 | ); 74 | break; 75 | } 76 | } 77 | } 78 | } else { 79 | // if room is currently occupied, calculate time until the room is free again (for > 15 minutes) 80 | const currentActivity = roomActivities[index]; 81 | if (index < roomActivities.length - 1) { 82 | for (let i = index; i < roomActivities.length - 1; i++) { 83 | const range = window.moment.range( 84 | window.moment(roomActivities[i].end), 85 | window.moment(roomActivities[i + 1].begin), 86 | ); 87 | if (range.diff('minutes') > 15) { 88 | // time until end of activity block (including breaks) 89 | duration = window.moment.range( 90 | now, 91 | window.moment(roomActivities[i].end), 92 | ); 93 | break; 94 | } else if (i === roomActivities.length - 2) { 95 | // time until last activity of the day ends if end day ends with activity block 96 | duration = window.moment.range( 97 | now, 98 | window.moment(roomActivities[i + 1].end), 99 | ); 100 | } 101 | } 102 | } else { 103 | duration = window.moment.range( 104 | now, 105 | window.moment(currentActivity.end), 106 | ); 107 | } 108 | } 109 | 110 | const humanizedDuration = (typeof duration === 'string') ? duration : humanizeDuration(duration + 0, { units: ['h', 'm'], round: true }); 111 | return { duration, humanizedDuration }; 112 | } 113 | 114 | export function calculateAvailability(roomActivities) { 115 | const now = window.moment(); 116 | // const now = window.moment('2017-06-19 08:30:00'); 117 | let free = true; 118 | let index = -1; 119 | roomActivities.map((activity, i) => { // eslint-disable-line 120 | if (free) { 121 | // check if now is withing an activity 122 | if (now.isBetween(activity.begin, activity.end)) { 123 | free = false; 124 | index = i; 125 | } else if (i < roomActivities.length - 1) { 126 | // get the next activity and range to current activity 127 | const nextActivity = roomActivities[i + 1]; 128 | const range = window.moment.range( 129 | window.moment(activity.end), 130 | window.moment(nextActivity.begin), 131 | ); 132 | 133 | // check if there is a break 134 | if (now.isBetween(activity.end, nextActivity.begin) && range.diff('minutes') <= 15) { 135 | free = false; 136 | index = i; 137 | } 138 | } 139 | } 140 | }); 141 | 142 | const duration = calculateAvailabilityDuration(free, roomActivities, index); 143 | 144 | return { free, duration }; 145 | } 146 | 147 | export async function loadJSON(url) { 148 | const response = await fetch(url); 149 | return response.json(); 150 | } 151 | --------------------------------------------------------------------------------