<commit_before>"use strict"

const asset = require('./asset')
const colorDepth = require('./converter/colorDepth')
const clamp = require('lodash/clamp')
const glob = require('glob')
const createMappoSession = require('./createMappoSession')
const convertRaw32bitDataToImageBitmap = require('./convertRaw32bitDataToImageBitmap')

const mappoSession = createMappoSession({
  fileSystem: {
    files: glob.sync(process.cwd() + '/**/*.map', {nocase: true}),
  },
  launchFolder: process.cwd(),
})

const mappoState = {
  mapFilename: null,
  mapData: null,
  palFilename: null,
  palData: null,
  vspFilename: null,
  vspData: null,
  tilesetBitmap: null,
}

const mapList = document.querySelector('.map-list')
mappoSession.getMapFilenames().forEach(mapFilename => {
  const li = document.createElement('li')
  li.innerText = mapFilename
  li.addEventListener('click', event => {
    console.group()
    console.log('clicked', mapFilename)
    console.groupEnd()
  })
  mapList.appendChild(li)
})

console.log('launchFolder', process.cwd())

mappoState.mapFilename = 'data/v1/TEST.MAP'
mappoState.mapData = asset.fromDisk(mappoState.mapFilename, asset.v1map)

console.log(mappoState.mapFilename, mappoState.mapData)
console.log('nummovescripts', mappoState.mapData.nummovescripts)
console.log('msbufsize', mappoState.mapData.msbufsize)
console.log('msofstbl', JSON.stringify(mappoState.mapData.msofstbl))
console.log('msbuf', JSON.stringify(mappoState.mapData.msbuf))
console.log('numscripts', mappoState.mapData.numscripts)
console.log('scriptofstbl', JSON.stringify(mappoState.mapData.scriptofstbl))
console.log('mapvcs', mappoState.mapData.mapvc.length)

mappoState.palFilename = 'data/v1/VERGE.PAL'
mappoState.palData = asset.fromDisk(mappoState.palFilename, asset.v1pal)

mappoState.vspFilename = 'data/v1/' + mappoState.mapData.vsp0name
mappoState.vspData = asset.fromDisk(mappoState.vspFilename, asset.v1vsp)
console.log('vsp', mappoState.vspFilename, mappoState.vspData)

const raw32bitData = colorDepth.convert8to32({
  palette: mappoState.palData.pal.map(v => v * 4),
  raw8bitData: mappoState.vspData.vsp0,
})

const canvas = document.querySelector('.mappo-viewport')
const tileColumns = 20
const tileRows = ~~((mappoState.vspData.numtiles + 19) / 20)
const context = canvas.getContext('2d')

const viewportWidth = 320
const viewportHeight = 240
const scale = 3

canvas.width = viewportWidth
canvas.height = viewportHeight

const renderTileHighlight = ({x, y, width=16, height=16}) => {
  context.strokeStyle = 'white'
  context.globalCompositeOperation = 'exclusion'
  context.lineWidth = 2
  context.strokeRect(~~x, ~~y, 16, 16)
}

const renderTile = (tileIndex, x, y) => {
  context.drawImage(mappoState.tilesetBitmap, 0, tileIndex * 16, 16, 16, x, y, 16, 16)
}

const getTileIndex = (layer, tileX, tileY) => {
  return layer[(tileY * mappoState.mapData.xsize) + tileX]
}

const renderLayer = (layer, x, y, transparent=false) => {
  x = ~~x
  y = ~~y

  const topLeftTileX = x >> 4
  const topLeftTileY = y >> 4
  const subTileX = x % 16
  const subTileY = y % 16

  for (let tileY = 0; tileY < (viewportHeight + 31) >> 4; tileY++) {
    for (let tileX = 0; tileX < (viewportWidth + 31) >> 4; tileX++) {
      const tileIndex = getTileIndex(
        layer,
        topLeftTileX + tileX,
        topLeftTileY + tileY
      )

      if (transparent && !tileIndex) {
        continue
      }

      renderTile(tileIndex, (tileX * 16) - subTileX, (tileY * 16) - subTileY)
    }
  }
}

let cameraX = 0
let cameraY = 0
let cameraMoveX = 0
let cameraMoveY = 0
const cameraScrollAmount = 1

const KEYCODE_UP = 38
const KEYCODE_DOWN = 40
const KEYCODE_LEFT = 37
const KEYCODE_RIGHT = 39
const keyPressed = {}

document.addEventListener('keydown', event => keyPressed[event.keyCode] = true)
document.addEventListener('keyup', event => keyPressed[event.keyCode] = false)

let mousedown = false
let mousein = false
canvas.addEventListener('mousedown', event => {
  mousedown = true
})

let hoverCanvasCoord = null
let autoScrollX = 0
let autoScrollY = 0
canvas.addEventListener('mousemove', event => {
  if (mousedown) {
    moveCamera(
      -event.movementX / scale,
      -event.movementY / scale
    )
  }

  autoScrollX = 0
  autoScrollY = 0
  const autoScrollThreshold = 16;
  if (mousein) {
    event.offsetX < autoScrollThreshold * scale && (autoScrollX = -1);
    event.offsetX >= (viewportWidth - autoScrollThreshold) * scale && (autoScrollX = +1);
    event.offsetY < autoScrollThreshold * scale && (autoScrollY = -1);
    event.offsetY >= (viewportHeight - autoScrollThreshold) * scale && (autoScrollY = +1);
  }

  hoverCanvasCoord = {
    x: ~~(event.offsetX / scale),
    y: ~~(event.offsetY / scale),
  }
})

canvas.addEventListener('mouseup', event => {
  mousedown = false
})

canvas.addEventListener('mouseenter', event => {
  mousein = true
})

canvas.addEventListener('mouseout', event => {
  mousein = false
  cameraMoveX = 0
  cameraMoveY = 0
  autoScrollX = 0
  autoScrollY = 0
  hoverCanvasCoord = null
})

const moveCamera = (moveX, moveY) => {
  cameraX = clamp(cameraX + moveX, 0, (mappoState.mapData.xsize * 16) - viewportWidth)
  cameraY = clamp(cameraY + moveY, 0, (mappoState.mapData.ysize * 16) - viewportHeight)
}

const tick = () => {
  context.globalCompositeOperation = 'source-over'
  context.fillStyle = 'black'
  context.fillRect(0, 0, viewportWidth, viewportHeight)

  if (mappoState.tilesetBitmap) {
    renderLayer(mappoState.mapData.map0, cameraX, cameraY)
    renderLayer(
      mappoState.mapData.map1,
      cameraX * mappoState.mapData.pmultx / mappoState.mapData.pdivx,
      cameraY * mappoState.mapData.pmultx / mappoState.mapData.pdivx,
      true
    )

    if (hoverCanvasCoord) {
      renderTileHighlight({
        x: hoverCanvasCoord.x - ((~~cameraX + hoverCanvasCoord.x) % 16),
        y: hoverCanvasCoord.y - ((~~cameraY + hoverCanvasCoord.y) % 16),
      })
    }

    cameraMoveX = 0
    cameraMoveY = 0;
    (keyPressed[KEYCODE_UP] || autoScrollY < 0) && (cameraMoveY = -cameraScrollAmount);
    (keyPressed[KEYCODE_DOWN] || autoScrollY > 0) && (cameraMoveY = +cameraScrollAmount);
    (keyPressed[KEYCODE_LEFT] || autoScrollX < 0) && (cameraMoveX = -cameraScrollAmount);
    (keyPressed[KEYCODE_RIGHT] || autoScrollX > 0) && (cameraMoveX = +cameraScrollAmount);
    moveCamera(cameraMoveX, cameraMoveY)
  }

  window.requestAnimationFrame(tick)
}

tick()

convertRaw32bitDataToImageBitmap({
  context,
  raw32bitData,
  width: 16,
  height: 16,
  numTiles: mappoState.vspData.numtiles,
}).then(tilesetBitmap => {
  mappoState.tilesetBitmap = tilesetBitmap
})
<commit_msg>load maps by clicking their filenames<commit_after>"use strict"

const asset = require('./asset')
const colorDepth = require('./converter/colorDepth')
const clamp = require('lodash/clamp')
const glob = require('glob')
const createMappoSession = require('./createMappoSession')
const convertRaw32bitDataToImageBitmap = require('./convertRaw32bitDataToImageBitmap')

const mappoSession = createMappoSession({
  fileSystem: {
    files: glob.sync(process.cwd() + '/**/*.map', {nocase: true}),
  },
  launchFolder: process.cwd(),
})

const mappoState = {
  mapFilename: null,
  mapData: null,
  palFilename: null,
  palData: null,
  vspFilename: null,
  vspData: null,
  tilesetBitmap: null,
}

const mapList = document.querySelector('.map-list')
mappoSession.getMapFilenames().forEach(mapFilename => {
  const li = document.createElement('li')
  li.innerText = mapFilename
  li.addEventListener('click', event => loadMap(mapFilename))
  mapList.appendChild(li)
})

console.log('launchFolder', process.cwd())

mappoState.palFilename = 'data/v1/VERGE.PAL'
mappoState.palData = asset.fromDisk(mappoState.palFilename, asset.v1pal)

const loadMap = mapFilename => {
console.group()

mappoState.mapFilename = mapFilename
mappoState.mapData = asset.fromDisk(mappoState.mapFilename, asset.v1map)

console.log(mappoState.mapFilename, mappoState.mapData)
console.log('nummovescripts', mappoState.mapData.nummovescripts)
console.log('msbufsize', mappoState.mapData.msbufsize)
console.log('msofstbl', JSON.stringify(mappoState.mapData.msofstbl))
console.log('msbuf', JSON.stringify(mappoState.mapData.msbuf))
console.log('numscripts', mappoState.mapData.numscripts)
console.log('scriptofstbl', JSON.stringify(mappoState.mapData.scriptofstbl))
console.log('mapvcs', mappoState.mapData.mapvc.length)

mappoState.vspFilename = 'data/v1/' + mappoState.mapData.vsp0name
mappoState.vspData = asset.fromDisk(mappoState.vspFilename, asset.v1vsp)
console.log('vsp', mappoState.vspFilename, mappoState.vspData)

const raw32bitData = colorDepth.convert8to32({
  palette: mappoState.palData.pal.map(v => v * 4),
  raw8bitData: mappoState.vspData.vsp0,
})

convertRaw32bitDataToImageBitmap({
  context,
  raw32bitData,
  width: 16,
  height: 16,
  numTiles: mappoState.vspData.numtiles,
}).then(tilesetBitmap => {
  mappoState.tilesetBitmap = tilesetBitmap
})

console.groupEnd()
}

const canvas = document.querySelector('.mappo-viewport')
const context = canvas.getContext('2d')

const viewportWidth = 320
const viewportHeight = 240
const scale = 3

canvas.width = viewportWidth
canvas.height = viewportHeight

const renderTileHighlight = ({x, y, width=16, height=16}) => {
  context.strokeStyle = 'white'
  context.globalCompositeOperation = 'exclusion'
  context.lineWidth = 2
  context.strokeRect(~~x, ~~y, 16, 16)
}

const renderTile = (tileIndex, x, y) => {
  context.drawImage(mappoState.tilesetBitmap, 0, tileIndex * 16, 16, 16, x, y, 16, 16)
}

const getTileIndex = (layer, tileX, tileY) => {
  return layer[(tileY * mappoState.mapData.xsize) + tileX]
}

const renderLayer = (layer, x, y, transparent=false) => {
  x = ~~x
  y = ~~y

  const topLeftTileX = x >> 4
  const topLeftTileY = y >> 4
  const subTileX = x % 16
  const subTileY = y % 16

  for (let tileY = 0; tileY < (viewportHeight + 31) >> 4; tileY++) {
    for (let tileX = 0; tileX < (viewportWidth + 31) >> 4; tileX++) {
      const tileIndex = getTileIndex(
        layer,
        topLeftTileX + tileX,
        topLeftTileY + tileY
      )

      if (transparent && !tileIndex) {
        continue
      }

      renderTile(tileIndex, (tileX * 16) - subTileX, (tileY * 16) - subTileY)
    }
  }
}

let cameraX = 0
let cameraY = 0
let cameraMoveX = 0
let cameraMoveY = 0
const cameraScrollAmount = 1

const KEYCODE_UP = 38
const KEYCODE_DOWN = 40
const KEYCODE_LEFT = 37
const KEYCODE_RIGHT = 39
const keyPressed = {}

document.addEventListener('keydown', event => keyPressed[event.keyCode] = true)
document.addEventListener('keyup', event => keyPressed[event.keyCode] = false)

let mousedown = false
let mousein = false
canvas.addEventListener('mousedown', event => {
  mousedown = true
})

let hoverCanvasCoord = null
let autoScrollX = 0
let autoScrollY = 0
canvas.addEventListener('mousemove', event => {
  if (mousedown) {
    moveCamera(
      -event.movementX / scale,
      -event.movementY / scale
    )
  }

  autoScrollX = 0
  autoScrollY = 0
  const autoScrollThreshold = 16;
  if (mousein) {
    event.offsetX < autoScrollThreshold * scale && (autoScrollX = -1);
    event.offsetX >= (viewportWidth - autoScrollThreshold) * scale && (autoScrollX = +1);
    event.offsetY < autoScrollThreshold * scale && (autoScrollY = -1);
    event.offsetY >= (viewportHeight - autoScrollThreshold) * scale && (autoScrollY = +1);
  }

  hoverCanvasCoord = {
    x: ~~(event.offsetX / scale),
    y: ~~(event.offsetY / scale),
  }
})

canvas.addEventListener('mouseup', event => {
  mousedown = false
})

canvas.addEventListener('mouseenter', event => {
  mousein = true
})

canvas.addEventListener('mouseout', event => {
  mousein = false
  cameraMoveX = 0
  cameraMoveY = 0
  autoScrollX = 0
  autoScrollY = 0
  hoverCanvasCoord = null
})

const moveCamera = (moveX, moveY) => {
  cameraX = clamp(cameraX + moveX, 0, (mappoState.mapData.xsize * 16) - viewportWidth)
  cameraY = clamp(cameraY + moveY, 0, (mappoState.mapData.ysize * 16) - viewportHeight)
}

const tick = () => {
  context.globalCompositeOperation = 'source-over'
  context.fillStyle = 'black'
  context.fillRect(0, 0, viewportWidth, viewportHeight)

  if (mappoState.tilesetBitmap) {
    renderLayer(mappoState.mapData.map0, cameraX, cameraY)
    renderLayer(
      mappoState.mapData.map1,
      cameraX * mappoState.mapData.pmultx / mappoState.mapData.pdivx,
      cameraY * mappoState.mapData.pmultx / mappoState.mapData.pdivx,
      true
    )

    if (hoverCanvasCoord) {
      renderTileHighlight({
        x: hoverCanvasCoord.x - ((~~cameraX + hoverCanvasCoord.x) % 16),
        y: hoverCanvasCoord.y - ((~~cameraY + hoverCanvasCoord.y) % 16),
      })
    }

    cameraMoveX = 0
    cameraMoveY = 0;
    (keyPressed[KEYCODE_UP] || autoScrollY < 0) && (cameraMoveY = -cameraScrollAmount);
    (keyPressed[KEYCODE_DOWN] || autoScrollY > 0) && (cameraMoveY = +cameraScrollAmount);
    (keyPressed[KEYCODE_LEFT] || autoScrollX < 0) && (cameraMoveX = -cameraScrollAmount);
    (keyPressed[KEYCODE_RIGHT] || autoScrollX > 0) && (cameraMoveX = +cameraScrollAmount);
    moveCamera(cameraMoveX, cameraMoveY)
  }

  window.requestAnimationFrame(tick)
}

tick()
<|endoftext|>


<commit_before>'use strict';

app.controller('SearchController',
  ['$scope', '$location', '$window', 'ResultsService', 'SuggestionsService',
  'TrustabilityService', 'ScreenshotService', '$translate',
  'DisambiguatorService', 'TranslationService',
  function($scope, $location, $window, resultsService, suggestionsService,
    trustabilityService, screenshotService, $translate,
    disambiguatorService, translationService) {
    $scope.pageTitle = 'Search';
    $scope.pageIcon = 'fa-globe';
    $scope.pageTitleColor = 'text-dark-blue';
    $scope.form = {};
    $scope.translatedQueries = {};
    $scope.targetLang = $scope.kConfig.lang;
    $scope.searchActive = true;
    $scope.filters = $scope.filters || {};
    $scope.filters = deserialize($location.search().filters);
    $scope.$emit('searchActive');
    $scope.highlight = undefined;
    $scope.isDefinitionCollapsed = false;
    $scope.isSmallWidth = $window.innerWidth < 768;

    angular.element($window).bind('resize', function() {
      $scope.isSmallWidth = false;
      if ($window.innerWidth < 768) {
        $scope.isSmallWidth = true;
      }
    });

    $scope.submit = function(targetlang) {
      if ($scope.form.param !== undefined) {
        // Prevent useless submit (same request)
        if ($scope.form.param === $location.search().q && !targetlang) {
          return;
        }

        $location.search('q', $scope.form.param);
        $location.search('lang', targetlang);

        if ($scope.translatedQueries[targetlang]) {
          $location.search('translated', $scope.translatedQueries[targetlang]);
        }

        if (targetlang === $scope.kConfig.lang || !targetlang) {
          $location.search('lang', null);
          $location.search('translated', null);
        }

      }
    };

    $scope.activateFilter = function(filter) {
      $scope.filters[filter] = !$scope.filters[filter];
      $location.search('filters', serialize($scope.filters));
    };

    $scope.searchSection = function(param) {
      if (param) {
        $location.search('section', param.toLowerCase());
      } else {
        $location.search('section', null);
      }
    };

    $scope.showScreenshot = function($index) {
      if ($index === undefined) {
        $scope.screenshot = undefined;
        $scope.highlight = undefined;
        return;
      }
      $scope.highlight = $index;
      $scope.screenshot = $scope.results.groups[$index].doclist.docs[0];
      $scope.screenshot.preview =
        screenshotService.getScreenSrcFromUrl($scope.screenshot.url);
    };

    $scope.getSuggestion = function(val, lang) {
      if (!lang) {
        lang = $scope.kConfig.lang;
      }

      var array = [];
      return suggestionsService.getSpellcheck(val, lang)
        .then(function(res) {
          var results = suggestionsService.cureSpellcheck(res.data);
          if (results.length) {
            array = array.concat(results);
          }
          return suggestionsService.getSuggestions(val, lang);
        })
        .then(function(res) {
          array = array.concat(res.data.suggestions);
          return suggestionsService.getQuestions(val, lang);
        })
        .then(function(res) {
          array = array.concat(res.data);
          return array;
        });
    };

    $scope.handleResults = function() {

    };

    var q = $location.search().q;
    var section = $location.search().section;
    var filters = $location.search().filters;
    var translated = $location.search().translated;
    $scope.targetLang = $location.search().lang;
    var allowTranslate = ($scope.targetLang && translated);

    if (q) {
      // By default use User language.

      var queryLanguage = $scope.kConfig.lang;
      $scope.form.param = q;
      $scope.param = q;
      $scope.highlight = $scope.param;
      $scope.getColor = getColor;

      if ($scope.lang.translatableTargets) {
        translationService.translateMany(q, $scope.kConfig.lang,
          $scope.lang.translatableTargets)
          .then(function(data) {
            data.forEach(function(langTargetResponse) {
              var key = langTargetResponse.lang;
              var translation =
                langTargetResponse.translation;
              $scope.translatedQueries[key] = translation;
            });
          });
      }

      disambiguatorService.getFatheadContent(q)
        .then(function(res) {
          if (!res.data.results[0]) {
            throw new Error('No fathead');
          }
          $scope.fathead = extractDefinitionFromDisambiguator(q, res.data);

          // Check if result is misspelt
          if (!$scope.fathead) {
            throw new Error('No fathead');
          }

          // Translate fathead to User language
          if ($scope.kConfig.lang !== 'en') {
            translationService.translate(
              $scope.fathead.content, 'en', $scope.kConfig.lang)
              .then(function(data) {
                $scope.fathead.content =
                    data.translation;
              });
          }

          if ($scope.fathead.content.length > 600) {
            $scope.isDefinitionCollapsed = true;
          }
        })
        .catch(err => {
          suggestionsService.getAutocorrect(q, $scope.kConfig.lang)
            .then(function(res) {
              var results = suggestionsService.cureAutocorrect(q, res.data);
              if (results) {
                $scope.fathead = {
                  type: 'views/fatheads/suggestions.html',
                  content: results,
                };
              }
              return;
            });
        });

      // Translated content request
      if (allowTranslate) {
        queryLanguage = $scope.targetLang;
        q = translated;
        $scope.translatedHighlight = translated;
      }

      // Paginated results
      resultsService.getResults({
        query: q || '',
        lang: queryLanguage,
        rows: 10,
        page: 1,
        section: section,
        filters: filters,
      }).then(function(res) {
        $scope.results = $scope.treatResults(res, 1, queryLanguage, section);
      });
    } else {
      $scope.card = 'views/partials/card.html';
      $translate('SEARCH_CARD').then(function(res) {
        $scope.cardcontent = res;
      });
    }

    $scope.changePage = function() {
      var queryLanguage = $scope.targetLang || $scope.kConfig.lang;
      resultsService.getResults({
        query: q || '',
        lang: queryLanguage,
        rows: 10,
        page: $scope.results.currentPage,
        section: section,
        filters: filters,
      }).then(function(res) {
        $window.scrollTo(0,0);
        $scope.results = $scope.treatResults(res,
          $scope.results.currentPage, queryLanguage, section);
      });
    };

    $scope.treatResults = function(res, page, lang, section) {
      if (!lang) {
        lang = $scope.kConfig.lang;
      }
      var results = res.data.grouped.domain;
      results.all = section || 'all';
      results.nbPages = res.data.grouped.domain.matches / 10;
      results.currentPage = page;
      results.maxSize = 10;
      results.sections = res.data.facet_counts
        .facet_fields.khresmoi_sections_sectionType_facet;
      res.data.grouped.domain.groups.forEach(function(link) {
        // Handle extra data and mutators
        // Region free title
        link.doclist.docs[0].title =
          link.doclist.docs[0]['title_' + lang];

        // Cards definitions
        link.doclist.docs[0].cards = [];

        // HonCode certification
        link.doclist.docs[0].cards[0] =
          'views/cards/trustability.html';
        if (link.doclist.docs[0].is_certified_facet) {
          if (link.doclist.docs[0].is_certified_facet[0] === 'true') {
            link.doclist.docs[0].cards[0] =
              'views/cards/certification.html';
          }
        }

        // Readability
        var diff = link.doclist.docs[0].readability_difficult_facet;
        var easy = link.doclist.docs[0].readability_easy_facet;
        link.doclist.docs[0].cards[1] =
          'views/cards/readability.html';
        if (easy && diff) {
          link.doclist.docs[0].readability = 60;
        } else if (easy) {
          link.doclist.docs[0].readability = 90;
        } else if (diff) {
          link.doclist.docs[0].readability = 30;
        } else {
          link.doclist.docs[0].readability = 0;
        }

        // Trustability
        trustabilityService.getTrustabilityValueFromHost(link.groupValue)
          .then(function(data) {
            link.doclist.docs[0].trustability = data;
          });

        // Cookies
        link.doclist.docs[0].cards[2] = 'views/cards/cookies.html';

        if (lang !== $scope.kConfig.lang) {
          // Translations
          // Title
          translationService
            .translate(link.doclist.docs[0].title, lang, $scope.kConfig.lang)
            .then(function(res) {
              if (!res.translation) {return;}

              var translatedTitle = res.translation;
              link.doclist.docs[0].translatedTitle = translatedTitle;
            });

          // Snippet
          translationService
            .translate(link.doclist.docs[0].snippet, lang, $scope.kConfig.lang)
            .then(function(res) {
              if (!res.translation) {return;}
              var translatedSnippet = res.translation;
              link.doclist.docs[0].translatedSnippet = translatedSnippet;
            });
        }
      });
      return results;
    };
  },]);

function serialize(obj) {
  var results = '';
  for (var key in obj) {
    if (obj.hasOwnProperty(key) && obj[key]) {
      results += key + ' ';
    }
  }
  results = results.trim();
  if (results === '') {
    results = null;
  }
  return results;
}

function deserialize(text) {
  var results = {};
  if (!text) {
    return results;
  }
  var array = text.split(' ');
  for (var i = 0; i < array.length; i++) {
    results[array[i]] = true;
  }
  return results;
}

var getColor = function(normalizedValue) {
  if (normalizedValue < 33) {
    return '#fd0000';
  }
  if (normalizedValue < 66) {
    return '#fdef00';
  }
  return '#00dd00';
};

var extractDefinitionFromDisambiguator = function(query, data) {
  if (!data || !data.results) {
    return null;
  }

  for (var i = 0; i < data.results.length; i++) {
    var result = data.results[i];
    var definition = honTrim(result.definition);
    var url = result.uri && result.uri.namespace + result.uri.localName;

    var foundExactLabel = result.label &&
      equalsIgnoreCase(honTrim(result.label), query);

    if (!foundExactLabel && result.labels) {
      for (var j = 0; j < result.labels.length; j++) {
        var label = honTrim(result.labels[j]);
        if (equalsIgnoreCase(label, query)) {
          foundExactLabel = true;
          break;
        }
      }
    }
    if (foundExactLabel && definition && url && foundExactLabel) {
      var cleanedQuery = query.replace(/^./, query.charAt(0).toUpperCase());
      var list = definition.split(/[A-Z]+:/);
      var def = list[1] ? list[1] : list[0];
      def = def.replace(/,$/, '').toLowerCase();
      def = capitalizeSentences(def,1);
      return {
        type: 'views/fatheads/definition.html',
        title: cleanedQuery,
        content: def,
        url: url,
      };
    }
  }
};

var honTrim = function(str) {
  return !str ? str : str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};

var equalsIgnoreCase = function(first, second) {
  if (!first) {
    return !second;
  }
  if (!second) {
    return false;
  }
  return first.toUpperCase() === second.toUpperCase();
};

var capitalizeSentences  = function(capText, capLock) {
  if (capLock === 1 || capLock === true) {
    capText = capText.toLowerCase();
  }

  var wordSplit = '. ';
  var wordArray = capText.split(wordSplit);
  var numWords  = wordArray.length;

  for (var x = 0; x < numWords; x++) {
    wordArray[x] = wordArray[x].replace(
      wordArray[x].charAt(0),wordArray[x].charAt(0).toUpperCase()
    );
    if (x === 0) {
      capText = wordArray[x] + '. ';
    } else if (x !== numWords - 1) {
      capText = capText + wordArray[x] + '. ';
    }else if (x === numWords - 1) {
      capText = capText + wordArray[x];
    }
  }
  return capText;
};
<commit_msg>add cookie scale<commit_after>'use strict';

app.controller('SearchController',
  ['$scope', '$location', '$window', 'ResultsService', 'SuggestionsService',
  'TrustabilityService', 'ScreenshotService', '$translate',
  'DisambiguatorService', 'TranslationService', 'COOKIESCALE',
  function($scope, $location, $window, resultsService, suggestionsService,
    trustabilityService, screenshotService, $translate,
    disambiguatorService, translationService, cookieScale) {
    $scope.pageTitle = 'Search';
    $scope.pageIcon = 'fa-globe';
    $scope.pageTitleColor = 'text-dark-blue';
    $scope.form = {};
    $scope.translatedQueries = {};
    $scope.targetLang = $scope.kConfig.lang;
    $scope.searchActive = true;
    $scope.filters = $scope.filters || {};
    $scope.filters = deserialize($location.search().filters);
    $scope.$emit('searchActive');
    $scope.highlight = undefined;
    $scope.isDefinitionCollapsed = false;
    $scope.isSmallWidth = $window.innerWidth < 768;

    angular.element($window).bind('resize', function() {
      $scope.isSmallWidth = false;
      if ($window.innerWidth < 768) {
        $scope.isSmallWidth = true;
      }
    });

    $scope.submit = function(targetlang) {
      if ($scope.form.param !== undefined) {
        // Prevent useless submit (same request)
        if ($scope.form.param === $location.search().q && !targetlang) {
          return;
        }

        $location.search('q', $scope.form.param);
        $location.search('lang', targetlang);

        if ($scope.translatedQueries[targetlang]) {
          $location.search('translated', $scope.translatedQueries[targetlang]);
        }

        if (targetlang === $scope.kConfig.lang || !targetlang) {
          $location.search('lang', null);
          $location.search('translated', null);
        }

      }
    };

    $scope.activateFilter = function(filter) {
      $scope.filters[filter] = !$scope.filters[filter];
      $location.search('filters', serialize($scope.filters));
    };

    $scope.searchSection = function(param) {
      if (param) {
        $location.search('section', param.toLowerCase());
      } else {
        $location.search('section', null);
      }
    };

    $scope.showScreenshot = function($index) {
      if ($index === undefined) {
        $scope.screenshot = undefined;
        $scope.highlight = undefined;
        return;
      }
      $scope.highlight = $index;
      $scope.screenshot = $scope.results.groups[$index].doclist.docs[0];
      $scope.screenshot.preview =
        screenshotService.getScreenSrcFromUrl($scope.screenshot.url);
    };

    $scope.getSuggestion = function(val, lang) {
      if (!lang) {
        lang = $scope.kConfig.lang;
      }

      var array = [];
      return suggestionsService.getSpellcheck(val, lang)
        .then(function(res) {
          var results = suggestionsService.cureSpellcheck(res.data);
          if (results.length) {
            array = array.concat(results);
          }
          return suggestionsService.getSuggestions(val, lang);
        })
        .then(function(res) {
          array = array.concat(res.data.suggestions);
          return suggestionsService.getQuestions(val, lang);
        })
        .then(function(res) {
          array = array.concat(res.data);
          return array;
        });
    };

    $scope.handleResults = function() {

    };

    var q = $location.search().q;
    var section = $location.search().section;
    var filters = $location.search().filters;
    var translated = $location.search().translated;
    $scope.targetLang = $location.search().lang;
    var allowTranslate = ($scope.targetLang && translated);

    if (q) {
      // By default use User language.

      var queryLanguage = $scope.kConfig.lang;
      $scope.form.param = q;
      $scope.param = q;
      $scope.highlight = $scope.param;
      $scope.getColor = getColor;

      if ($scope.lang.translatableTargets) {
        translationService.translateMany(q, $scope.kConfig.lang,
          $scope.lang.translatableTargets)
          .then(function(data) {
            data.forEach(function(langTargetResponse) {
              var key = langTargetResponse.lang;
              var translation =
                langTargetResponse.translation;
              $scope.translatedQueries[key] = translation;
            });
          });
      }

      disambiguatorService.getFatheadContent(q)
        .then(function(res) {
          if (!res.data.results[0]) {
            throw new Error('No fathead');
          }
          $scope.fathead = extractDefinitionFromDisambiguator(q, res.data);

          // Check if result is misspelt
          if (!$scope.fathead) {
            throw new Error('No fathead');
          }

          // Translate fathead to User language
          if ($scope.kConfig.lang !== 'en') {
            translationService.translate(
              $scope.fathead.content, 'en', $scope.kConfig.lang)
              .then(function(data) {
                $scope.fathead.content =
                    data.translation;
              });
          }

          if ($scope.fathead.content.length > 600) {
            $scope.isDefinitionCollapsed = true;
          }
        })
        .catch(err => {
          suggestionsService.getAutocorrect(q, $scope.kConfig.lang)
            .then(function(res) {
              var results = suggestionsService.cureAutocorrect(q, res.data);
              if (results) {
                $scope.fathead = {
                  type: 'views/fatheads/suggestions.html',
                  content: results,
                };
              }
              return;
            });
        });

      // Translated content request
      if (allowTranslate) {
        queryLanguage = $scope.targetLang;
        q = translated;
        $scope.translatedHighlight = translated;
      }

      // Paginated results
      resultsService.getResults({
        query: q || '',
        lang: queryLanguage,
        rows: 10,
        page: 1,
        section: section,
        filters: filters,
      }).then(function(res) {
        $scope.results = $scope.treatResults(res, 1, queryLanguage, section);
      });
    } else {
      $scope.card = 'views/partials/card.html';
      $translate('SEARCH_CARD').then(function(res) {
        $scope.cardcontent = res;
      });
    }

    $scope.changePage = function() {
      var queryLanguage = $scope.targetLang || $scope.kConfig.lang;
      resultsService.getResults({
        query: q || '',
        lang: queryLanguage,
        rows: 10,
        page: $scope.results.currentPage,
        section: section,
        filters: filters,
      }).then(function(res) {
        $window.scrollTo(0,0);
        $scope.results = $scope.treatResults(res,
          $scope.results.currentPage, queryLanguage, section);
      });
    };

    $scope.treatResults = function(res, page, lang, section) {
      if (!lang) {
        lang = $scope.kConfig.lang;
      }
      var results = res.data.grouped.domain;
      results.all = section || 'all';
      results.nbPages = res.data.grouped.domain.matches / 10;
      results.currentPage = page;
      results.maxSize = 10;
      results.sections = res.data.facet_counts
        .facet_fields.khresmoi_sections_sectionType_facet;
      res.data.grouped.domain.groups.forEach(function(link) {
        // Handle extra data and mutators
        // Region free title
        link.doclist.docs[0].title =
          link.doclist.docs[0]['title_' + lang];

        // Cards definitions
        link.doclist.docs[0].cards = [];

        // HonCode certification
        link.doclist.docs[0].cards[0] =
          'views/cards/trustability.html';
        if (link.doclist.docs[0].is_certified_facet) {
          if (link.doclist.docs[0].is_certified_facet[0] === 'true') {
            link.doclist.docs[0].cards[0] =
              'views/cards/certification.html';
          }
        }

        // Readability
        var diff = link.doclist.docs[0].readability_difficult_facet;
        var easy = link.doclist.docs[0].readability_easy_facet;
        link.doclist.docs[0].cards[1] =
          'views/cards/readability.html';
        if (easy && diff) {
          link.doclist.docs[0].readability = 60;
        } else if (easy) {
          link.doclist.docs[0].readability = 90;
        } else if (diff) {
          link.doclist.docs[0].readability = 30;
        } else {
          link.doclist.docs[0].readability = 0;
        }

        // Trustability
        trustabilityService.getTrustabilityValueFromHost(link.groupValue)
          .then(function(data) {
            link.doclist.docs[0].trustability = data;
          });

        // Cookies
        link.doclist.docs[0].cards[2] = 'views/cards/cookies.html';
        link.doclist.docs[0].cookiedensity = 'no';
        var cookies = link.doclist.docs[0].cookies;
        if (cookies) {
          if (cookies > 0 && cookies <= cookieScale.few) {
            link.doclist.docs[0].cookiedensity = 'few';
          } else if (cookies > cookieScale.few &&
            cookies <= cookieScale.more) {
            link.doclist.docs[0].cookiedensity = 'more';
          } else if (cookies > cookieScale.more) {
            link.doclist.docs[0].cookiedensity = 'max';
          }
        }

        if (lang !== $scope.kConfig.lang) {
          // Translations
          // Title
          translationService
            .translate(link.doclist.docs[0].title, lang, $scope.kConfig.lang)
            .then(function(res) {
              if (!res.translation) {return;}

              var translatedTitle = res.translation;
              link.doclist.docs[0].translatedTitle = translatedTitle;
            });

          // Snippet
          translationService
            .translate(link.doclist.docs[0].snippet, lang, $scope.kConfig.lang)
            .then(function(res) {
              if (!res.translation) {return;}
              var translatedSnippet = res.translation;
              link.doclist.docs[0].translatedSnippet = translatedSnippet;
            });
        }
      });
      return results;
    };
  },]);

function serialize(obj) {
  var results = '';
  for (var key in obj) {
    if (obj.hasOwnProperty(key) && obj[key]) {
      results += key + ' ';
    }
  }
  results = results.trim();
  if (results === '') {
    results = null;
  }
  return results;
}

function deserialize(text) {
  var results = {};
  if (!text) {
    return results;
  }
  var array = text.split(' ');
  for (var i = 0; i < array.length; i++) {
    results[array[i]] = true;
  }
  return results;
}

var getColor = function(normalizedValue) {
  if (normalizedValue < 33) {
    return '#fd0000';
  }
  if (normalizedValue < 66) {
    return '#fdef00';
  }
  return '#00dd00';
};

var extractDefinitionFromDisambiguator = function(query, data) {
  if (!data || !data.results) {
    return null;
  }

  for (var i = 0; i < data.results.length; i++) {
    var result = data.results[i];
    var definition = honTrim(result.definition);
    var url = result.uri && result.uri.namespace + result.uri.localName;

    var foundExactLabel = result.label &&
      equalsIgnoreCase(honTrim(result.label), query);

    if (!foundExactLabel && result.labels) {
      for (var j = 0; j < result.labels.length; j++) {
        var label = honTrim(result.labels[j]);
        if (equalsIgnoreCase(label, query)) {
          foundExactLabel = true;
          break;
        }
      }
    }
    if (foundExactLabel && definition && url && foundExactLabel) {
      var cleanedQuery = query.replace(/^./, query.charAt(0).toUpperCase());
      var list = definition.split(/[A-Z]+:/);
      var def = list[1] ? list[1] : list[0];
      def = def.replace(/,$/, '').toLowerCase();
      def = capitalizeSentences(def,1);
      return {
        type: 'views/fatheads/definition.html',
        title: cleanedQuery,
        content: def,
        url: url,
      };
    }
  }
};

var honTrim = function(str) {
  return !str ? str : str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};

var equalsIgnoreCase = function(first, second) {
  if (!first) {
    return !second;
  }
  if (!second) {
    return false;
  }
  return first.toUpperCase() === second.toUpperCase();
};

var capitalizeSentences  = function(capText, capLock) {
  if (capLock === 1 || capLock === true) {
    capText = capText.toLowerCase();
  }

  var wordSplit = '. ';
  var wordArray = capText.split(wordSplit);
  var numWords  = wordArray.length;

  for (var x = 0; x < numWords; x++) {
    wordArray[x] = wordArray[x].replace(
      wordArray[x].charAt(0),wordArray[x].charAt(0).toUpperCase()
    );
    if (x === 0) {
      capText = wordArray[x] + '. ';
    } else if (x !== numWords - 1) {
      capText = capText + wordArray[x] + '. ';
    }else if (x === numWords - 1) {
      capText = capText + wordArray[x];
    }
  }
  return capText;
};
<|endoftext|>


<commit_before>import Color from './Color'
import RGB from './RGB'

export default class HSL extends Color {

  toHex() {
    return this.toRgb().toHex()
  }

  toString() {
    let hsl = super.toString()
    return `hsl(${hsl})`
  }

  get hue() {
    return this.value[0]
  }

  set hue (val) {
    this.value[0] = Math.abs(val % 360)
  }

  toRgb() {
      let h, s, l,
          r, g, b,
          temp1, temp2, hsl, rgb

      // clone array
      hsl = this.value.slice(0)
      hsl = this.degPercPercToHsl(hsl)
      h = hsl[0]
      s = hsl[1]
      l = hsl[2]

      // greyscale
      if (s === 0) {
        r = Math.round(l * 255)
        g = Math.round(l * 255)
        b = Math.round(l * 255)
      } else {
        temp2 = (l < 0.5) ? l * (1 + s) : (l + s) - (s * l)
        temp1 = 2 * l - temp2

        r = Math.round(255 * this.hueToRgb(temp1, temp2, h + ( 1 / 3 )))
        g = Math.round(255 * this.hueToRgb(temp1, temp2, h))
        b = Math.round(255 * this.hueToRgb(temp1, temp2, h - 0.3333))
      }
      rgb = new RGB([parseInt(r, 10), parseInt(g, 10), parseInt(b, 10)])
      return rgb
  }

  hueToRgb(temp1, temp2, hue) {
      var rgbValue = 0
      if (hue < 0) {
          hue += 1
      }
      if (hue > 1) {
          hue -= 1
      }

      if (6 * hue < 1 ) { // red
        rgbValue = temp1 + (temp2 - temp1) * 6 * hue
      } else if (2 * hue < 1 ) { // green
        rgbValue = temp2
      } else if (3 * hue < 2 ) { // blue
        rgbValue = temp1 + (temp2 - temp1) * (0.6666 - hue) * 6
      }
      return rgbValue
  }

  degPercPercToHsl(hsl) {
    hsl[0] /= 360
    hsl[1] /= 100
    hsl[2] /= 100
    return hsl
  }

}

<commit_msg>add saturation & lightness to hsl<commit_after>import Color from './Color'
import RGB from './RGB'

export default class HSL extends Color {

  toHex() {
    return this.toRgb().toHex()
  }

  toString() {
    let hsl = super.toString()
    return `hsl(${hsl})`
  }

  get hue() {
    return this.value[0]
  }

  set hue(val) {
    this.value[0] = Math.abs(val % 360)
  }

  get saturation() {
    return this.value[1]
  }

  set saturation(val) {
    this.value[1] = Math.abs(val % 100)
  }

  get lightness() {
    return this.value[2]
  }

  set lightness(val) {
    this.value[2] = Math.abs(val % 100)
  }

  toRgb() {
      let h, s, l,
          r, g, b,
          temp1, temp2, hsl, rgb

      // clone array
      hsl = this.value.slice(0)
      hsl = this.degPercPercToHsl(hsl)
      h = hsl[0]
      s = hsl[1]
      l = hsl[2]

      // greyscale
      if (s === 0) {
        r = Math.round(l * 255)
        g = Math.round(l * 255)
        b = Math.round(l * 255)
      } else {
        temp2 = (l < 0.5) ? l * (1 + s) : (l + s) - (s * l)
        temp1 = 2 * l - temp2

        r = Math.round(255 * this.hueToRgb(temp1, temp2, h + ( 1 / 3 )))
        g = Math.round(255 * this.hueToRgb(temp1, temp2, h))
        b = Math.round(255 * this.hueToRgb(temp1, temp2, h - 0.3333))
      }
      rgb = new RGB([parseInt(r, 10), parseInt(g, 10), parseInt(b, 10)])
      return rgb
  }

  hueToRgb(temp1, temp2, hue) {
      var rgbValue = 0
      if (hue < 0) {
          hue += 1
      }
      if (hue > 1) {
          hue -= 1
      }

      if (6 * hue < 1 ) { // red
        rgbValue = temp1 + (temp2 - temp1) * 6 * hue
      } else if (2 * hue < 1 ) { // green
        rgbValue = temp2
      } else if (3 * hue < 2 ) { // blue
        rgbValue = temp1 + (temp2 - temp1) * (0.6666 - hue) * 6
      }
      return rgbValue
  }

  degPercPercToHsl(hsl) {
    hsl[0] /= 360
    hsl[1] /= 100
    hsl[2] /= 100
    return hsl
  }

}

<|endoftext|>


<commit_before>(function(){
'use strict';

// (part of the sf.virtualScroll module).
var mod = angular.module('ui.virtual-repeat', []);

var DONT_WORK_AS_VIEWPORTS = ['TABLE', 'TBODY', 'THEAD', 'TR', 'TFOOT'];
var DONT_WORK_AS_CONTENT = ['TABLE', 'TBODY', 'THEAD', 'TR', 'TFOOT'];
var DONT_SET_DISPLAY_BLOCK = ['TABLE', 'TBODY', 'THEAD', 'TR', 'TFOOT'];

// Utility to clip to range
function clip(value, min, max){
  if (angular.isArray(value)) {
    return angular.forEach(value, function(v) {
      return clip(v, min, max);
    });
  }

  return Math.max(min, Math.min(value, max));
}

mod.directive('uiVirtualRepeat', ['$log', '$rootElement', function($log, $rootElement){

  // Turn the expression supplied to the directive:
  //
  //     a in b
  //
  // into `{ value: "a", collection: "b" }`
  function parseRepeatExpression(expression) {
    var match = expression.match(/^\s*([\$\w]+)\s+in\s+([\S\s]*)$/);
    if (! match) {
      throw new Error("Expected uiVirtualRepeat in form of '_item_ in _collection_' but got '" + expression + "'.");
    }
    return {
      value: match[1],
      collection: match[2]
    };
  }

  // Utility to filter out elements by tag name
  function isTagNameInList(element, list) {
    var t,
        tag = element.tagName.toUpperCase();

    for (t = 0; t < list.length; t++) {
      if (list[t] === tag) {
        return true;
      }
    }
    return false;
  }

  // Utility to find the viewport/content elements given the start element:
  function findViewportAndContent(startElement) {
    /*jshint eqeqeq:false, curly:false */
    var root = $rootElement[0];
    var e, n;

    // Somewhere between the grandparent and the root node
    for (e = startElement.parent().parent()[0]; e !== root; e = e.parentNode) {
      // is an element
      if (e.nodeType != 1) break;
      // that isn't in the blacklist (tables etc.),
      if (isTagNameInList(e, DONT_WORK_AS_VIEWPORTS)) continue;
      // has a single child element (the content),
      if (e.childElementCount != 1) continue;
      // which is not in the blacklist
      if (isTagNameInList(e.firstElementChild, DONT_WORK_AS_CONTENT)) continue;
      // and no text.
      for (n = e.firstChild; n; n = n.nextSibling) {
        if (n.nodeType == 3 && /\S/g.test(n.textContent)) {
          break;
        }
      }

      if (n == null) {
        // That element should work as a viewport.
        return {
          viewport: angular.element(e),
          content: angular.element(e.firstElementChild)
        };
      }
    }

    throw new Error("No suitable viewport element");
  }

  // Apply explicit height and overflow styles to the viewport element.
  //
  // If the viewport has a max-height (inherited or otherwise), set max-height.
  // Otherwise, set height from the current computed value or use
  // window.innerHeight as a fallback
  //
  function setViewportCss (viewport) {
    var viewportCss = {'overflow': 'auto'};

    var style = window.getComputedStyle ?
                window.getComputedStyle(viewport[0]) :
                viewport[0].currentStyle;

    var maxHeight = style && style.getPropertyValue('max-height');
    var height = style && style.getPropertyValue('height');

    if (maxHeight && maxHeight !== '0px') {
      viewportCss.maxHeight = maxHeight;
    }
    else if (height && height !== '0px') {
      viewportCss.height = height;
    }
    else {
      viewportCss.height = window.innerHeight;
    }

    viewport.css(viewportCss);
  }

  // Apply explicit styles to the content element to prevent pesky padding
  // or borders messing with our calculations:
  function setContentCss(content) {
    var contentCss = {
      margin: 0,
      padding: 0,
      border: 0,
      'box-sizing': 'border-box'
    };

    content.css(contentCss);
  }

  // TODO: compute outerHeight (padding + border unless box-sizing is border)
  function computeRowHeight(element) {
    var style = window.getComputedStyle ?
                window.getComputedStyle(element) :
                element.currentStyle;

    var maxHeight = style && style.getPropertyValue('max-height');
    var height = style && style.getPropertyValue('height');

    if (height && height !== '0px' && height !== 'auto') {
      // $log.info('Row height is "%s" from css height', height);
    }
    else if (maxHeight && maxHeight !== '0px' && maxHeight !== 'none') {
      height = maxHeight;
      // $log.info('Row height is "%s" from css max-height', height);
    }
    else if (element.clientHeight) {
      height = element.clientHeight + 'px';
      // $log.info('Row height is "%s" from client height', height);
    }
    else {
      //throw new Error("Unable to compute height of row");
      return;
    }

    angular.element(element).css('height', height);

    return parseInt(height, 10);
  }

  // The compile gathers information about the declaration. There's not much
  // else we could do in the compile step as we need a viewport parent that
  // is exculsively ours - this is only available at link time.
  function uiVirtualRepeatCompile(element, attr, linker) {
    var ident = parseRepeatExpression(attr.uiVirtualRepeat);

    // ----

    // Set up the initial value for our watch expression (which is just the
    // start and length of the active rows and the collection length) and
    // adds a listener to handle child scopes based on the active rows.
    function sfVirtualRepeatPostLink(scope, iterStartElement, attrs) {

      var rendered = [];
          scope.rendered = rendered;

      var rowHeight = 0;
      var sticky = false;
      var dom = findViewportAndContent(iterStartElement);
      // The list structure is controlled by a few simple (visible) variables:
      var state = 'ngModel' in attrs ? scope.$eval(attrs.ngModel) : {};
      //  - The index of the first active element
      state.firstActive = 0;
      //  - The index of the first visible element
      state.firstVisible = 0;
      //  - The number of elements visible in the viewport.
      state.visible = 0;
      // - The number of active elements
      state.active = 0;
      // - The total number of elements
      state.total = 0;
      // - The point at which we add new elements
      state.lowWater = state.lowWater || 10;
      // - The point at which we remove old elements
      state.highWater = state.highWater || 20;
      // TODO: now watch the water marks

      setContentCss(dom.content);
      setViewportCss(dom.viewport);
      // When the user scrolls, we move the `state.firstActive`
      dom.viewport.bind('scroll', sfVirtualRepeatOnScroll);

      // The watch on the collection is just a watch on the length of the
      // collection. We don't care if the content changes.
      scope.$watch(sfVirtualRepeatWatchExpression, sfVirtualRepeatListener, true);

      // and that's the link done! All the action is in the handlers...

      // ----

      // Apply explicit styles to the item element
      function setElementCss (element) {
        var elementCss = {
          // no margin or it'll screw up the height calculations.
          margin: '0'
        };

        if (!isTagNameInList(element[0], DONT_SET_DISPLAY_BLOCK)) {
          // display: block if it's safe to do so
          elementCss.display = 'block';
        }

        if (rowHeight) {
          elementCss.height = rowHeight + 'px';
        }

        element.css(elementCss);
      }

      function makeNewScope (idx, collection, containerScope) {
        var childScope = containerScope.$new();
        childScope[ident.value] = collection[idx];
        childScope.$index = idx;
        childScope.$first = (idx === 0);
        childScope.$last = (idx === (collection.length - 1));
        childScope.$middle = !(childScope.$first || childScope.$last);
        childScope.$watch(function updateChildScopeItem(){
          childScope[ident.value] = collection[idx];
        });
        return childScope;
      }

      function addElements (start, end, collection, containerScope, insPoint) {
        var frag = document.createDocumentFragment();
        var newElements = [], element, idx, childScope;

        for (idx = start; idx !== end; idx++) {
          childScope = makeNewScope(idx, collection, containerScope);
          element = linker(childScope, angular.noop);
          setElementCss(element);
          newElements.push(element);
          frag.appendChild(element[0]);
        }

        insPoint.after(frag);
        return newElements;
      }

      function recomputeActive() {
        // We want to set the start to the low water mark unless the current
        // start is already between the low and high water marks.
        var start = clip(state.firstActive, state.firstVisible - state.lowWater, state.firstVisible - state.highWater);
        // Similarly for the end
        var end = clip(state.firstActive + state.active,
                       state.firstVisible + state.visible + state.lowWater,
                       state.firstVisible + state.visible + state.highWater );
        state.firstActive = Math.max(0, start);
        state.active = Math.min(end, state.total) - state.firstActive;
      }

      function sfVirtualRepeatOnScroll(evt) {
        if (!rowHeight) {
          return;
        }

        // Enter the angular world for the state change to take effect.
        scope.$apply(function() {
          state.firstVisible = Math.floor(evt.target.scrollTop / rowHeight);
          state.visible = Math.ceil(dom.viewport[0].clientHeight / rowHeight);

          // $log.log('scroll to row %o', state.firstVisible);
          sticky = evt.target.scrollTop + evt.target.clientHeight >= evt.target.scrollHeight;

          recomputeActive();
          // $log.log(' state is now %o', state);
          // $log.log(' sticky = %o', sticky);
        });
      }

      function sfVirtualRepeatWatchExpression(scope) {
        var coll = scope.$eval(ident.collection);

        if (coll.length !== state.total) {
          state.total = coll.length;
          recomputeActive();
        }

        return {
          start: state.firstActive,
          active: state.active,
          len: coll.length
        };
      }

      function destroyActiveElements (action, count) {
        var dead,
            ii,
            remover = Array.prototype[action];

        for (ii = 0; ii < count; ii++) {
          dead = remover.call(rendered);
          dead.scope().$destroy();
          dead.remove();
        }
      }

      // When the watch expression for the repeat changes, we may need to add
      // and remove scopes and elements
      function sfVirtualRepeatListener(newValue, oldValue, scope) {
        var oldEnd = oldValue.start + oldValue.active,
            collection = scope.$eval(ident.collection),
            newElements;

        if (newValue === oldValue) {
          // $log.info('initial listen');
          newElements = addElements(newValue.start, oldEnd, collection, scope, iterStartElement);
          rendered = newElements;

          if (rendered.length) {
            rowHeight = computeRowHeight(newElements[0][0]);
          }
        }
        else {
          var newEnd = newValue.start + newValue.active;
          var forward = newValue.start >= oldValue.start;
          var delta = forward ? newValue.start - oldValue.start
                              : oldValue.start - newValue.start;
          var endDelta = newEnd >= oldEnd ? newEnd - oldEnd : oldEnd - newEnd;
          var contiguous = delta < (forward ? oldValue.active : newValue.active);
          // $log.info('change by %o,%o rows %s', delta, endDelta, forward ? 'forward' : 'backward');

          if (!contiguous) {
            // $log.info('non-contiguous change');
            destroyActiveElements('pop', rendered.length);
            rendered = addElements(newValue.start, newEnd, collection, scope, iterStartElement);
          }
          else {
            if (forward) {
              // $log.info('need to remove from the top');
              destroyActiveElements('shift', delta);
            }
            else if (delta) {
              // $log.info('need to add at the top');
              newElements = addElements(
                newValue.start,
                oldValue.start,
                collection, scope, iterStartElement);
              rendered = newElements.concat(rendered);
            }

            if (newEnd < oldEnd) {
              // $log.info('need to remove from the bottom');
              destroyActiveElements('pop', oldEnd - newEnd);
            }
            else if (endDelta) {
              var lastElement = rendered[rendered.length-1];
              // $log.info('need to add to the bottom');
              newElements = addElements(
                oldEnd,
                newEnd,
                collection, scope, lastElement);

              rendered = rendered.concat(newElements);
            }
          }

          if (!rowHeight && rendered.length) {
            rowHeight = computeRowHeight(rendered[0][0]);
          }

          dom.content.css({'padding-top': newValue.start * rowHeight + 'px'});
        }

        dom.content.css({'height': newValue.len * rowHeight + 'px'});

        if (sticky) {
          dom.viewport[0].scrollTop = dom.viewport[0].clientHeight + dom.viewport[0].scrollHeight;
        }

        scope.rendered = rendered;
      }

      return;
    }

    return {
      post: sfVirtualRepeatPostLink
    };
  }

  var directive =  {
    require: '?ngModel',
    transclude: 'element',
    priority: 1000,
    // terminal: true,
    compile: uiVirtualRepeatCompile,
    controller: ['$scope', function ($scope) {
      $scope.goo = 'fasfd';
      this.visibleRows = 0;
      this.visibleRows = this.visibleRows + 1;
    }]
  };

  return directive;
}]);

}());<commit_msg>All-caps the acronym CSS<commit_after>(function(){
'use strict';

// (part of the sf.virtualScroll module).
var mod = angular.module('ui.virtual-repeat', []);

var DONT_WORK_AS_VIEWPORTS = ['TABLE', 'TBODY', 'THEAD', 'TR', 'TFOOT'];
var DONT_WORK_AS_CONTENT = ['TABLE', 'TBODY', 'THEAD', 'TR', 'TFOOT'];
var DONT_SET_DISPLAY_BLOCK = ['TABLE', 'TBODY', 'THEAD', 'TR', 'TFOOT'];

// Utility to clip to range
function clip(value, min, max){
  if (angular.isArray(value)) {
    return angular.forEach(value, function(v) {
      return clip(v, min, max);
    });
  }

  return Math.max(min, Math.min(value, max));
}

mod.directive('uiVirtualRepeat', ['$log', '$rootElement', function($log, $rootElement){

  // Turn the expression supplied to the directive:
  //
  //     a in b
  //
  // into `{ value: "a", collection: "b" }`
  function parseRepeatExpression(expression) {
    var match = expression.match(/^\s*([\$\w]+)\s+in\s+([\S\s]*)$/);
    if (! match) {
      throw new Error("Expected uiVirtualRepeat in form of '_item_ in _collection_' but got '" + expression + "'.");
    }
    return {
      value: match[1],
      collection: match[2]
    };
  }

  // Utility to filter out elements by tag name
  function isTagNameInList(element, list) {
    var t,
        tag = element.tagName.toUpperCase();

    for (t = 0; t < list.length; t++) {
      if (list[t] === tag) {
        return true;
      }
    }
    return false;
  }

  // Utility to find the viewport/content elements given the start element:
  function findViewportAndContent(startElement) {
    /*jshint eqeqeq:false, curly:false */
    var root = $rootElement[0];
    var e, n;

    // Somewhere between the grandparent and the root node
    for (e = startElement.parent().parent()[0]; e !== root; e = e.parentNode) {
      // is an element
      if (e.nodeType != 1) break;
      // that isn't in the blacklist (tables etc.),
      if (isTagNameInList(e, DONT_WORK_AS_VIEWPORTS)) continue;
      // has a single child element (the content),
      if (e.childElementCount != 1) continue;
      // which is not in the blacklist
      if (isTagNameInList(e.firstElementChild, DONT_WORK_AS_CONTENT)) continue;
      // and no text.
      for (n = e.firstChild; n; n = n.nextSibling) {
        if (n.nodeType == 3 && /\S/g.test(n.textContent)) {
          break;
        }
      }

      if (n == null) {
        // That element should work as a viewport.
        return {
          viewport: angular.element(e),
          content: angular.element(e.firstElementChild)
        };
      }
    }

    throw new Error("No suitable viewport element");
  }

  // Apply explicit height and overflow styles to the viewport element.
  //
  // If the viewport has a max-height (inherited or otherwise), set max-height.
  // Otherwise, set height from the current computed value or use
  // window.innerHeight as a fallback
  //
  function setViewportCSS(viewport) {
    var viewportCSS = {'overflow': 'auto'};

    var style = window.getComputedStyle ?
                window.getComputedStyle(viewport[0]) :
                viewport[0].currentStyle;

    var maxHeight = style && style.getPropertyValue('max-height');
    var height = style && style.getPropertyValue('height');

    if (maxHeight && maxHeight !== '0px') {
      viewportCSS.maxHeight = maxHeight;
    }
    else if (height && height !== '0px') {
      viewportCSS.height = height;
    }
    else {
      viewportCSS.height = window.innerHeight;
    }

    viewport.css(viewportCSS);
  }

  // Apply explicit styles to the content element to prevent pesky padding
  // or borders messing with our calculations:
  function setContentCSS(content) {
    var contentCSS = {
      margin: 0,
      padding: 0,
      border: 0,
      'box-sizing': 'border-box'
    };

    content.css(contentCSS);
  }

  // TODO: compute outerHeight (padding + border unless box-sizing is border)
  function computeRowHeight(element) {
    var style = window.getComputedStyle ?
                window.getComputedStyle(element) :
                element.currentStyle;

    var maxHeight = style && style.getPropertyValue('max-height');
    var height = style && style.getPropertyValue('height');

    if (height && height !== '0px' && height !== 'auto') {
      // $log.info('Row height is "%s" from css height', height);
    }
    else if (maxHeight && maxHeight !== '0px' && maxHeight !== 'none') {
      height = maxHeight;
      // $log.info('Row height is "%s" from css max-height', height);
    }
    else if (element.clientHeight) {
      height = element.clientHeight + 'px';
      // $log.info('Row height is "%s" from client height', height);
    }
    else {
      //throw new Error("Unable to compute height of row");
      return;
    }

    angular.element(element).css('height', height);

    return parseInt(height, 10);
  }

  // The compile gathers information about the declaration. There's not much
  // else we could do in the compile step as we need a viewport parent that
  // is exculsively ours - this is only available at link time.
  function uiVirtualRepeatCompile(element, attr, linker) {
    var ident = parseRepeatExpression(attr.uiVirtualRepeat);

    // ----

    // Set up the initial value for our watch expression (which is just the
    // start and length of the active rows and the collection length) and
    // adds a listener to handle child scopes based on the active rows.
    function sfVirtualRepeatPostLink(scope, iterStartElement, attrs) {

      var rendered = [];
          scope.rendered = rendered;

      var rowHeight = 0;
      var sticky = false;
      var dom = findViewportAndContent(iterStartElement);
      // The list structure is controlled by a few simple (visible) variables:
      var state = 'ngModel' in attrs ? scope.$eval(attrs.ngModel) : {};
      //  - The index of the first active element
      state.firstActive = 0;
      //  - The index of the first visible element
      state.firstVisible = 0;
      //  - The number of elements visible in the viewport.
      state.visible = 0;
      // - The number of active elements
      state.active = 0;
      // - The total number of elements
      state.total = 0;
      // - The point at which we add new elements
      state.lowWater = state.lowWater || 10;
      // - The point at which we remove old elements
      state.highWater = state.highWater || 20;
      // TODO: now watch the water marks

      setContentCSS(dom.content);
      setViewportCSS(dom.viewport);
      // When the user scrolls, we move the `state.firstActive`
      dom.viewport.bind('scroll', sfVirtualRepeatOnScroll);

      // The watch on the collection is just a watch on the length of the
      // collection. We don't care if the content changes.
      scope.$watch(sfVirtualRepeatWatchExpression, sfVirtualRepeatListener, true);

      // and that's the link done! All the action is in the handlers...

      // ----

      // Apply explicit styles to the item element
      function setElementCSS(element) {
        var elementCSS = {
          // no margin or it'll screw up the height calculations.
          margin: '0'
        };

        if (!isTagNameInList(element[0], DONT_SET_DISPLAY_BLOCK)) {
          // display: block if it's safe to do so
          elementCSS.display = 'block';
        }

        if (rowHeight) {
          elementCSS.height = rowHeight + 'px';
        }

        element.css(elementCSS);
      }

      function makeNewScope (idx, collection, containerScope) {
        var childScope = containerScope.$new();
        childScope[ident.value] = collection[idx];
        childScope.$index = idx;
        childScope.$first = (idx === 0);
        childScope.$last = (idx === (collection.length - 1));
        childScope.$middle = !(childScope.$first || childScope.$last);
        childScope.$watch(function updateChildScopeItem(){
          childScope[ident.value] = collection[idx];
        });
        return childScope;
      }

      function addElements (start, end, collection, containerScope, insPoint) {
        var frag = document.createDocumentFragment();
        var newElements = [], element, idx, childScope;

        for (idx = start; idx !== end; idx++) {
          childScope = makeNewScope(idx, collection, containerScope);
          element = linker(childScope, angular.noop);
          setElementCSS(element);
          newElements.push(element);
          frag.appendChild(element[0]);
        }

        insPoint.after(frag);
        return newElements;
      }

      function recomputeActive() {
        // We want to set the start to the low water mark unless the current
        // start is already between the low and high water marks.
        var start = clip(state.firstActive, state.firstVisible - state.lowWater, state.firstVisible - state.highWater);
        // Similarly for the end
        var end = clip(state.firstActive + state.active,
                       state.firstVisible + state.visible + state.lowWater,
                       state.firstVisible + state.visible + state.highWater );
        state.firstActive = Math.max(0, start);
        state.active = Math.min(end, state.total) - state.firstActive;
      }

      function sfVirtualRepeatOnScroll(evt) {
        if (!rowHeight) {
          return;
        }

        // Enter the angular world for the state change to take effect.
        scope.$apply(function() {
          state.firstVisible = Math.floor(evt.target.scrollTop / rowHeight);
          state.visible = Math.ceil(dom.viewport[0].clientHeight / rowHeight);

          // $log.log('scroll to row %o', state.firstVisible);
          sticky = evt.target.scrollTop + evt.target.clientHeight >= evt.target.scrollHeight;

          recomputeActive();
          // $log.log(' state is now %o', state);
          // $log.log(' sticky = %o', sticky);
        });
      }

      function sfVirtualRepeatWatchExpression(scope) {
        var coll = scope.$eval(ident.collection);

        if (coll.length !== state.total) {
          state.total = coll.length;
          recomputeActive();
        }

        return {
          start: state.firstActive,
          active: state.active,
          len: coll.length
        };
      }

      function destroyActiveElements (action, count) {
        var dead,
            ii,
            remover = Array.prototype[action];

        for (ii = 0; ii < count; ii++) {
          dead = remover.call(rendered);
          dead.scope().$destroy();
          dead.remove();
        }
      }

      // When the watch expression for the repeat changes, we may need to add
      // and remove scopes and elements
      function sfVirtualRepeatListener(newValue, oldValue, scope) {
        var oldEnd = oldValue.start + oldValue.active,
            collection = scope.$eval(ident.collection),
            newElements;

        if (newValue === oldValue) {
          // $log.info('initial listen');
          newElements = addElements(newValue.start, oldEnd, collection, scope, iterStartElement);
          rendered = newElements;

          if (rendered.length) {
            rowHeight = computeRowHeight(newElements[0][0]);
          }
        }
        else {
          var newEnd = newValue.start + newValue.active;
          var forward = newValue.start >= oldValue.start;
          var delta = forward ? newValue.start - oldValue.start
                              : oldValue.start - newValue.start;
          var endDelta = newEnd >= oldEnd ? newEnd - oldEnd : oldEnd - newEnd;
          var contiguous = delta < (forward ? oldValue.active : newValue.active);
          // $log.info('change by %o,%o rows %s', delta, endDelta, forward ? 'forward' : 'backward');

          if (!contiguous) {
            // $log.info('non-contiguous change');
            destroyActiveElements('pop', rendered.length);
            rendered = addElements(newValue.start, newEnd, collection, scope, iterStartElement);
          }
          else {
            if (forward) {
              // $log.info('need to remove from the top');
              destroyActiveElements('shift', delta);
            }
            else if (delta) {
              // $log.info('need to add at the top');
              newElements = addElements(
                newValue.start,
                oldValue.start,
                collection, scope, iterStartElement);
              rendered = newElements.concat(rendered);
            }

            if (newEnd < oldEnd) {
              // $log.info('need to remove from the bottom');
              destroyActiveElements('pop', oldEnd - newEnd);
            }
            else if (endDelta) {
              var lastElement = rendered[rendered.length-1];
              // $log.info('need to add to the bottom');
              newElements = addElements(
                oldEnd,
                newEnd,
                collection, scope, lastElement);

              rendered = rendered.concat(newElements);
            }
          }

          if (!rowHeight && rendered.length) {
            rowHeight = computeRowHeight(rendered[0][0]);
          }

          dom.content.css({'padding-top': newValue.start * rowHeight + 'px'});
        }

        dom.content.css({'height': newValue.len * rowHeight + 'px'});

        if (sticky) {
          dom.viewport[0].scrollTop = dom.viewport[0].clientHeight + dom.viewport[0].scrollHeight;
        }

        scope.rendered = rendered;
      }

      return;
    }

    return {
      post: sfVirtualRepeatPostLink
    };
  }

  var directive =  {
    require: '?ngModel',
    transclude: 'element',
    priority: 1000,
    // terminal: true,
    compile: uiVirtualRepeatCompile,
    controller: ['$scope', function ($scope) {
      $scope.goo = 'fasfd';
      this.visibleRows = 0;
      this.visibleRows = this.visibleRows + 1;
    }]
  };

  return directive;
}]);

}());<|endoftext|>


<commit_before>// climate control template -- start with this when an HVAC unit can be managed independently
// search for TBD to see what to change

// load the module that knows how to discover/communicate with a bulb
var samsung     = require('samsung-airconditioner')
  , util        = require('util')
  , devices     = require('./../../core/device')
  , steward     = require('./../../core/steward')
  , utility     = require('./../../core/utility')
  , climate     = require('./../device-climate')
  , sensor      = require('./../device-sensor')
  ;

// var logger = climate.logger;

// define the prototype that will be instantiated when the HVAC unit is discovered
// later, we will create a ...perform function, and a ...update function.
var Thermostat = exports.Device = function(deviceID, deviceUID, info) {
  var logger2 = utility.logger('discovery');
  var self = this;

  self.whatami = info.deviceType;
  self.deviceID = deviceID.toString();
  self.deviceUID = deviceUID;
  self.name = info.device.name;
  self.getName();

  self.hvac = info.hvac;
  self.info = {};

  self.hvac.on('stateChange', function(state) {
    var translated_state = {};
    for (key in state) {
      switch (key) {
        case "AC_FUN_POWER":
          if (state[key] == 'On') {
            translated_state['hvac'] = 'on';
          }
          if (state[key] == 'Off') {
            translated_state['hvac'] = 'off';
          }
          break;
        case "AC_FUN_OPMODE":
          if (state[key] == 'Cool') {
            translated_state['hvac'] = 'cool';
          }
          if (state[key] == 'Heat') {
            translated_state['hvac'] = 'heat';
          }
          if (state[key] == 'Dry') {
            translated_state['hvac'] = 'dry';
          }
          if (state[key] == 'Wind') {
            translated_state['hvac'] = 'fan';
          }
          if (state[key] == "Auto") {
            translated_state['hvac'] = 'on';
          }
          break;
        case "AC_FUN_TEMPSET":
          state['goalTemperature'] = state[key];
          break;
        case "AC_FUN_WINDLEVEL":
          if (state[key] == "High") {
            translated_state['fan'] = 'high';
          }
          if (state[key] == "Mid") {
            translated_state['fan'] = 'mid';
          }
          if (state[key] == "Low") {
            translated_state['fan'] = 'low';
          }
          if (state[key] == "Auto") {
            translated_state['fan'] = 'auto';
          }
          break;
        case "AC_ADD_SPI":
          break;
        case "AC_FUN_TEMPNOW":
          translated_state['temperature'] = parseInt(state[key], 10);
          break;
        case "AC_ADD_AUTOCLEAN":
          break;
        case "AC_FUN_COMODE":
          // Convience mode
          // Smart, Quiet, Off, etc
          break;
        case "AC_FUN_DIRECTION":
          // SwingUD, Fixed or Rotation
          break;
      }
    }

//    logger2.info('device/' + self.deviceID, state);
    logger2.info('device/' + self.deviceID, translated_state);

    self.update(self, translated_state, self.status);
    self.changed();
  });

  self.update(self, {
    'hvac': 'off',
    'fan': 'off',
    'temperature': 0
  }, 'present');
  self.changed();

  utility.broker.subscribe('actors', function(request, taskID, actor, perform, parameter) {
    if (actor !== ('device/' + self.deviceID)) return;

    if (request === 'perform') return self.perform(self, taskID, perform, parameter);
  });

  self.setup();
};
util.inherits(Thermostat, climate.Device);

Thermostat.prototype.setup = function () {
  var self = this;
  var logger2 = utility.logger('discovery');

  // var db = require('./../../core/database').db
  // db.on('trace', function(e) {
  //   console.log(e);
  // });

  var state = self.getState(function (err, state) {
    if (!state) {
      state = {};
    }

    if (!state.token) {
      self.hvac.get_token(function(err, token) {
        if (!!err) {
          self.update(self, {}, 'reset');

          return logger2.info('device/' + self.deviceID, 'Get Token error: ' + err.message);
        }

        logger2.info('device/' + self.deviceID, "Token found:" + token);

        state.token = token;

        self.setState(state);

        self.hvac.login(token, function () {
        }).on('loginSuccess', function () {
          self.update(self, {}, 'present');
          self.hvac.status();
          logger2.info('device/' + self.deviceID, "Logged on");
        });
      }).on('waiting', function() {
        self.alert('Please power on the device within the next 30 seconds');
        logger2.info('device/' + self.deviceID, 'Please power on the device within the next 30 seconds');
      });
    } else {
      self.hvac.login(state.token, function () {
      }).on('loginSuccess', function () {
        self.update(self, {}, 'present');
        self.hvac.status();
        logger2.info('device/' + self.deviceID, "Logged on");
      });
    }
  });

};

Thermostat.prototype.update = function(self, params, status) {
  var param, updateP;

  updateP = false;
  if ((!!status) && (status !== self.status)) {
    self.status = status;
    updateP = true;
  }
  for (param in params) {
    if ((!params.hasOwnProperty(param)) || (!params[param]) || (self.info[param] === params[param])) continue;

    self.info[param] = params[param];
    updateP = true;
  }
  if (updateP) {
    self.changed();
    sensor.update(self.deviceID, params);
  }
};

Thermostat.operations = {
  set: function(self, params) {

    var performed = false;

    var attempt_perform = function(key, fn) {
      if (typeof params[key] !== 'undefined') {
        fn(params[key]);
        performed = true;
      }
    };

    attempt_perform('hvac', function(value) {
      switch (value) {
        case 'off':
          self.hvac.onoff(false);
          break;
        case 'on':
          self.hvac.onoff(true);
          break;
        case 'cool':
          self.hvac.mode('Cool');
          break;
        case 'heat':
          self.hvac.mode('Heat');
          break;
        case 'dry':
          self.hvac.mode('Dry');
          break;
        case 'fan':
          self.hvac.mode('wind');
          break;
      }
    });

    attempt_perform('fan', function(value) {
      var time;

      switch (value) {
        // Available options for convenient mode
        // var modes = ['Off', 'Quiet', 'Sleep', 'Smart', 'SoftCool', 'TurboMode', 'WindMode1', 'WindMode2', 'WindMode3']
        case 'off':
          // self.hvac.set_convenient_mode('Off');
          break;
        case 'on':
          // self.hvac.set_convenient_mode('Quiet');
          break;
        case 'auto':
          // self.hvac.set_convenient_mode('WindMode1');
          break;

        default:
          time = parseInt(value, 10);
          if (isNaN(time)) break;
          // TBD: set the fan duration. adjust time from milliseconds to whatever
          break;
      }
    });

    attempt_perform('goalTemperature', function(value) {
      var goalTemperature;

      goalTemperature = parseInt(value, 10);
      if (isNaN(goalTemperature)) {
        return;
      }

      if (goalTemperature > 30 || goalTemperature < 16) {
        return;
      }

      self.hvac.set_temperature(goalTemperature);
    });

    return performed;
  }
};

Thermostat.prototype.perform = function(self, taskID, perform, parameter) {
  var params;

  try { params = JSON.parse(parameter); } catch(e) { params = {}; }

  if (!!Thermostat.operations[perform]) {
    if (Thermostat.operations[perform](this, params)) {
      return steward.performed(taskID);
    }
  }

  return devices.perform(self, taskID, perform, parameter);
};

var checkParam = function(key, params, result, allowNumeric, map) {
  if (typeof params[key] !== 'undefined') {

    var defined = typeof map[params[key]] !== 'undefined';

    if (((!defined) && (!allowNumeric)) || ((!defined) && allowNumeric && isNaN(parseInt(params[key], 10)))) {
      result.invalid.push(key);
    }

    if (key == 'goalTemperature') {
      var temperature = parseInt(params[key], 10);

      if (temperature > 30 || temperature < 16) {
        result.invalid.push(key);
      }
    }
  }
};

var validate_perform = function(perform, parameter) {
  var params = {}
    , result = { invalid: [], requires: [] }
    ;

  if (!!parameter) try { params = JSON.parse(parameter); } catch(ex) { result.invalid.push('parameter'); }

  if (!!Thermostat.operations[perform]) {
    if (perform === 'set') {
      if (!params) {
        result.requires.push('parameter');
        return result;
      }

      checkParam('hvac', params, result, false, { heat: 1, cool: 1, fan: 1, off: 1 });
      checkParam('fan', params, result, true, { off: 1, on: 1, auto: 1 });
      checkParam('goalTemperature', params, result, true, {});
    }
    return result;
  }

  return devices.validate_perform(perform, parameter);
};

exports.start = function() {
  var logger2 = utility.logger('discovery');

  steward.actors.device.climate.samsung = steward.actors.device.climate.samsung ||
      { $info     : { type: '/device/climate/samsung' } };

  steward.actors.device.climate.samsung.control =
      { $info     : { type       : '/device/climate/samsung/control'
                    , observe    : [ ]
                    , perform    : [ ]
                    , properties : { name            : true
                                   , status          : [ 'present', 'reset' ]
                                   , lastSample      : 'timestamp'
                                   , temperature     : 'celsius'
                                   , humidity        : 'percentage'
                                   , hvac            : [ 'cool', 'heat', 'fan', 'dry', 'off', 'on', ]
                                   , fan             : [ 'on', 'off', 'high', 'mid', 'low', 'auto', 'milliseconds' ]
                                   , goalTemperature : 'celsius'
                                   }
                    }
        , $validate : { perform    : validate_perform }
      };
  devices.makers['/device/climate/samsung/control'] = Thermostat;
// TBD: when the hardware driver discovers a new HVAC unit, it will call us.
// TBD: or if the low-level driver needs to be polled, then create a 'scan' function and call it periodically.

  new samsung().on('discover', function(aircon) {
    // TODO This is done to avoid detecting ourselves listening for the SSDP response.
    // There should be a better way :(
    if (!aircon.options.duid) {
      return;
    }
    var info;
    info = { source     : 'samsung'
           , hvac       : aircon
           , device     : { url          : null
                          , name         : aircon.options.info["NICKNAME"]
                          , manufacturer : aircon.manufacturer || 'Samsung'
                          , model        : { name        : aircon.options.info["MODELCODE"]
                                           , description : ''
                                           }
                          , unit         : { serial      : aircon.props.duid
                                           , udn         : 'Samsung:' + aircon.props.duid
                                           }
                          }

         };

    info.url = info.device.url;
    info.deviceType = '/device/climate/samsung/control';
    info.id = info.device.unit.udn;

    devices.discover(info, function (err, deviceID) {
      if (!!err) { logger2.error('samsung', { diagnostic: err.message });};
    });

  }).on('error', function(err) {
    logger2.error('samsung', { diagnostic: err.message });
  }).logger = logger2;
};
<commit_msg>Implement fan time<commit_after>// climate control template -- start with this when an HVAC unit can be managed independently
// search for TBD to see what to change

// load the module that knows how to discover/communicate with a bulb
var samsung     = require('samsung-airconditioner')
  , util        = require('util')
  , devices     = require('./../../core/device')
  , steward     = require('./../../core/steward')
  , utility     = require('./../../core/utility')
  , climate     = require('./../device-climate')
  , sensor      = require('./../device-sensor')
  ;

// var logger = climate.logger;

// define the prototype that will be instantiated when the HVAC unit is discovered
// later, we will create a ...perform function, and a ...update function.
var Thermostat = exports.Device = function(deviceID, deviceUID, info) {
  var logger2 = utility.logger('discovery');
  var self = this;

  self.whatami = info.deviceType;
  self.deviceID = deviceID.toString();
  self.deviceUID = deviceUID;
  self.name = info.device.name;
  self.getName();

  self.hvac = info.hvac;
  self.info = {};

  self.hvac.on('stateChange', function(state) {
// { AC_FUN_ENABLE: 'Enable',
//   AC_FUN_COMODE: 'Off',
//   AC_FUN_SLEEP: '0',
//   AC_FUN_WINDLEVEL: 'High',
//   AC_FUN_DIRECTION: 'Fixed',
//   AC_ADD_AUTOCLEAN: 'Off',
//   AC_ADD_APMODE_END: '0',
//   AC_ADD_STARTWPS: 'Direct',
//   AC_ADD_SPI: 'Off',
//   AC_SG_WIFI: 'Connected',
//   AC_SG_INTERNET: 'Connected',
    var translated_state = {};
    for (key in state) {
      switch (key) {
        case "AC_FUN_POWER":
          if (state[key] == 'On') {
            translated_state['hvac'] = 'on';
          }
          if (state[key] == 'Off') {
            translated_state['hvac'] = 'off';
          }
          break;
        case "AC_FUN_OPMODE":
          if (state[key] == 'Cool') {
            translated_state['hvac'] = 'cool';
          }
          if (state[key] == 'Heat') {
            translated_state['hvac'] = 'heat';
          }
          if (state[key] == 'Dry') {
            translated_state['hvac'] = 'dry';
          }
          if (state[key] == 'Wind') {
            translated_state['hvac'] = 'fan';
          }
          if (state[key] == "Auto") {
            translated_state['hvac'] = 'on';
          }
          break;
        case "AC_FUN_TEMPSET":
          state['goalTemperature'] = state[key];
          break;
        case 'AC_FUN_SLEEP':
          translated_state['fan'] = parseInt(state[key], 10) * 1000 * 60;
          break;
        // TODO: this isn't what the hvac fan interace wants
        // case "AC_FUN_WINDLEVEL":
        //   if (state[key] == "High") {
        //     translated_state['fan'] = 'high';
        //   }
        //   if (state[key] == "Mid") {
        //     translated_state['fan'] = 'mid';
        //   }
        //   if (state[key] == "Low") {
        //     translated_state['fan'] = 'low';
        //   }
        //   if (state[key] == "Auto") {
        //     translated_state['fan'] = 'auto';
        //   }
        //   break;
        case "AC_ADD_SPI":
          break;
        case "AC_FUN_TEMPNOW":
          translated_state['temperature'] = parseInt(state[key], 10);
          break;
        case "AC_ADD_AUTOCLEAN":
          break;
        case "AC_FUN_COMODE":
          // Convience mode
          // Smart, Quiet, Off, etc
          break;
        case "AC_FUN_DIRECTION":
          // SwingUD, Fixed or Rotation
          break;
      }
    }

//    logger2.info('device/' + self.deviceID, state);
    logger2.info('device/' + self.deviceID, translated_state);

    self.update(self, translated_state, self.status);
    self.changed();
  });

  self.update(self, {
    'hvac': 'off',
    'fan': 0,
    'temperature': 0
  }, 'present');
  self.changed();

  utility.broker.subscribe('actors', function(request, taskID, actor, perform, parameter) {
    if (actor !== ('device/' + self.deviceID)) return;

    if (request === 'perform') return self.perform(self, taskID, perform, parameter);
  });

  self.setup();
};
util.inherits(Thermostat, climate.Device);

Thermostat.prototype.setup = function () {
  var self = this;
  var logger2 = utility.logger('discovery');

  // var db = require('./../../core/database').db
  // db.on('trace', function(e) {
  //   console.log(e);
  // });

  var state = self.getState(function (err, state) {
    if (!state) {
      state = {};
    }

    if (!state.token) {
      self.hvac.get_token(function(err, token) {
        if (!!err) {
          self.update(self, {}, 'reset');

          return logger2.info('device/' + self.deviceID, 'Get Token error: ' + err.message);
        }

        logger2.info('device/' + self.deviceID, "Token found:" + token);

        state.token = token;

        self.setState(state);

        self.hvac.login(token, function () {
        }).on('loginSuccess', function () {
          self.update(self, {}, 'present');
          self.hvac.status();
          logger2.info('device/' + self.deviceID, "Logged on");
        });
      }).on('waiting', function() {
        self.alert('Please power on the device within the next 30 seconds');
        logger2.info('device/' + self.deviceID, 'Please power on the device within the next 30 seconds');
      });
    } else {
      self.hvac.login(state.token, function () {
      }).on('loginSuccess', function () {
        self.update(self, {}, 'present');
        self.hvac.status();
        logger2.info('device/' + self.deviceID, "Logged on");
      });
    }
  });

};

Thermostat.prototype.update = function(self, params, status) {
  var param, updateP;

  updateP = false;
  if ((!!status) && (status !== self.status)) {
    self.status = status;
    updateP = true;
  }
  for (param in params) {
    if ((!params.hasOwnProperty(param)) || (!params[param]) || (self.info[param] === params[param])) continue;

    self.info[param] = params[param];
    updateP = true;
  }
  if (updateP) {
    self.changed();
    sensor.update(self.deviceID, params);
  }
};

Thermostat.operations = {
  set: function(self, params) {

    var performed = false;

    var attempt_perform = function(key, fn) {
      if (typeof params[key] !== 'undefined') {
        fn(params[key]);
        performed = true;
      }
    };

    attempt_perform('hvac', function(value) {
      switch (value) {
        case 'off':
          self.hvac.onoff(false);
          break;
        case 'on':
          self.hvac.onoff(true);
          break;
        case 'cool':
          self.hvac.mode('Cool');
          break;
        case 'heat':
          self.hvac.mode('Heat');
          break;
        case 'dry':
          self.hvac.mode('Dry');
          break;
        case 'fan':
          self.hvac.mode('wind');
          break;
      }
    });

    attempt_perform('fan', function(value) {
      var time;

      switch (value) {
        // Available options for convenient mode
        // var modes = ['Off', 'Quiet', 'Sleep', 'Smart', 'SoftCool', 'TurboMode', 'WindMode1', 'WindMode2', 'WindMode3']
        // case 'off':
        //   // self.hvac.set_convenient_mode('Off');
        //   break;
        // case 'on':
        //   // self.hvac.set_convenient_mode('Quiet');
        //   break;
        // case 'auto':
        //   // self.hvac.set_convenient_mode('WindMode1');
        //   break;

        default:
          time = parseInt(value, 10);
          if (isNaN(time)) break;
          self.hvac.set_sleep_mode(time/1000/60);
          // TBD: set the fan duration. adjust time from milliseconds to whatever
          break;
      }
    });

    attempt_perform('goalTemperature', function(value) {
      var goalTemperature;

      goalTemperature = parseInt(value, 10);
      if (isNaN(goalTemperature)) {
        return;
      }

      if (goalTemperature > 30 || goalTemperature < 16) {
        return;
      }

      self.hvac.set_temperature(goalTemperature);
    });

    return performed;
  }
};

Thermostat.prototype.perform = function(self, taskID, perform, parameter) {
  var params;

  try { params = JSON.parse(parameter); } catch(e) { params = {}; }

  if (!!Thermostat.operations[perform]) {
    if (Thermostat.operations[perform](this, params)) {
      return steward.performed(taskID);
    }
  }

  return devices.perform(self, taskID, perform, parameter);
};

var checkParam = function(key, params, result, allowNumeric, map) {
  if (typeof params[key] !== 'undefined') {

    var defined = typeof map[params[key]] !== 'undefined';

    if (((!defined) && (!allowNumeric)) || ((!defined) && allowNumeric && isNaN(parseInt(params[key], 10)))) {
      result.invalid.push(key);
    }

    if (key == 'goalTemperature') {
      var temperature = parseInt(params[key], 10);

      if (temperature > 30 || temperature < 16) {
        result.invalid.push(key);
      }
    }
  }
};

var validate_perform = function(perform, parameter) {
  var params = {}
    , result = { invalid: [], requires: [] }
    ;

  if (!!parameter) try { params = JSON.parse(parameter); } catch(ex) { result.invalid.push('parameter'); }

  if (!!Thermostat.operations[perform]) {
    if (perform === 'set') {
      if (!params) {
        result.requires.push('parameter');
        return result;
      }

      checkParam('hvac', params, result, false, { heat: 1, cool: 1, fan: 1, off: 1 });
      checkParam('fan', params, result, true, { off: 1, on: 1, auto: 1 });
      checkParam('goalTemperature', params, result, true, {});
    }
    return result;
  }

  return devices.validate_perform(perform, parameter);
};

exports.start = function() {
  var logger2 = utility.logger('discovery');

  steward.actors.device.climate.samsung = steward.actors.device.climate.samsung ||
      { $info     : { type: '/device/climate/samsung' } };

  steward.actors.device.climate.samsung.control =
      { $info     : { type       : '/device/climate/samsung/control'
                    , observe    : [ ]
                    , perform    : [ ]
                    , properties : { name            : true
                                   , status          : [ 'present', 'reset' ]
                                   , lastSample      : 'timestamp'
                                   , temperature     : 'celsius'
                                   , humidity        : 'percentage'
                                   , hvac            : [ 'cool', 'heat', 'fan', 'dry', 'off', 'on', ]
                                   , fan             : [ 'on', 'off', 'high', 'mid', 'low', 'auto', 'milliseconds' ]
                                   , goalTemperature : 'celsius'
                                   }
                    }
        , $validate : { perform    : validate_perform }
      };
  devices.makers['/device/climate/samsung/control'] = Thermostat;
// TBD: when the hardware driver discovers a new HVAC unit, it will call us.
// TBD: or if the low-level driver needs to be polled, then create a 'scan' function and call it periodically.

  new samsung().on('discover', function(aircon) {
    // TODO This is done to avoid detecting ourselves listening for the SSDP response.
    // There should be a better way :(
    if (!aircon.options.duid) {
      return;
    }
    var info;
    info = { source     : 'samsung'
           , hvac       : aircon
           , device     : { url          : null
                          , name         : aircon.options.info["NICKNAME"]
                          , manufacturer : aircon.manufacturer || 'Samsung'
                          , model        : { name        : aircon.options.info["MODELCODE"]
                                           , description : ''
                                           }
                          , unit         : { serial      : aircon.props.duid
                                           , udn         : 'Samsung:' + aircon.props.duid
                                           }
                          }

         };

    info.url = info.device.url;
    info.deviceType = '/device/climate/samsung/control';
    info.id = info.device.unit.udn;

    devices.discover(info, function (err, deviceID) {
      if (!!err) { logger2.error('samsung', { diagnostic: err.message });};
    });

  }).on('error', function(err) {
    logger2.error('samsung', { diagnostic: err.message });
  }).logger = logger2;
};
<|endoftext|>