diff --git a/CHANGELOG.md b/CHANGELOG.md index b9fce2d68882135a9660df76b9add3ee947fb38c..f45fab7fc1eed7aa7873bffb81b08410e143d70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## 1.2.0 - 2018-05-21 +### Added +- Classroom count route ## 1.1.1 - 2018-05-10 ### Changed diff --git a/src/libs/routes/api.js b/src/libs/routes/api.js index be86d04106e232beb341a1385a408754cb3f2a89..9f8bdc4ca84f1d28b27f0a4d549b43f6e9a90287 100644 --- a/src/libs/routes/api.js +++ b/src/libs/routes/api.js @@ -62,6 +62,8 @@ const siope = require(`${libs}/routes/siope`); const outOfSchool = require(`${libs}/routes/outOfSchool`); +const classroomCount = require(`${libs}/routes/classroomCount`); + api.get('/', (req, res) => { res.json({ msg: 'SimCAQ API is running' }); }); @@ -95,5 +97,6 @@ api.use('/infrastructure', infrastructure); api.use('/distribution_factor', distributionFactor); api.use('/siope', siope); api.use('/out_of_school', outOfSchool); +api.use('/classroom_count', classroomCount); module.exports = api; diff --git a/src/libs/routes/classroom.js b/src/libs/routes/classroom.js index 246e3d9bc7ae1a54c54a3afcc1104e0190107a13..2a8ee3913344bd78c5ac43545c5ac7597be7a1ec 100644 --- a/src/libs/routes/classroom.js +++ b/src/libs/routes/classroom.js @@ -1,4 +1,4 @@ - const express = require('express'); +const express = require('express'); const classroomApp = express.Router(); diff --git a/src/libs/routes/classroomCount.js b/src/libs/routes/classroomCount.js new file mode 100644 index 0000000000000000000000000000000000000000..b7c2aacda27984638fbcb1baae88d570aa5d357b --- /dev/null +++ b/src/libs/routes/classroomCount.js @@ -0,0 +1,495 @@ +const express = require('express'); + +const classroomCountApp = express.Router(); + +const libs = `${process.cwd()}/libs`; + +const squel = require('squel'); + +const query = require(`${libs}/middlewares/query`).query; + +const response = require(`${libs}/middlewares/response`); + +const ReqQueryFields = require(`${libs}/middlewares/reqQueryFields`); + +const id2str = require(`${libs}/middlewares/id2str`); + +const addMissing = require(`${libs}/middlewares/addMissing`); + +const config = require(`${libs}/config`); + +const cache = require('apicache').options({ debug: config.debug, statusCodes: {include: [200]} }).middleware; + +let rqf = new ReqQueryFields(); + +rqf.addField({ + name: 'filter', + field: false, + where: true +}).addField({ + name: 'dims', + field: true, + where: false +}).addValueToField({ + name: 'city', + table: 'municipio', + tableField: 'nome', + resultField: 'city_name', + where: { + relation: '=', + type: 'integer', + field: 'municipio_id', + table: '@' + }, + join: { + primary: 'id', + foreign: 'municipio_id', + foreignTable: '@' + } +}, 'filter').addValueToField({ + name: 'city', + table: 'municipio', + tableField: ['nome', 'id'], + resultField: ['city_name', 'city_id'], + where: { + relation: '=', + type: 'integer', + field: 'id' + }, + join: { + primary: 'id', + foreign: 'municipio_id', + foreignTable: '@' + } +}, 'dims').addValueToField({ + name: 'state', + table: 'estado', + tableField: 'nome', + resultField: 'state_name', + where: { + relation: '=', + type: 'integer', + field: 'estado_id', + table: '@' + }, + join: { + primary: 'id', + foreign: 'estado_id', + foreignTable: '@' + } +}, 'filter').addValueToField({ + name: 'state', + table: 'estado', + tableField: ['nome', 'id'], + resultField: ['state_name', 'state_id'], + where: { + relation: '=', + type: 'integer', + field: 'id', + }, + join: { + primary: 'id', + foreign: 'estado_id', + foreignTable: '@' + } +}, 'dims').addValue({ + name: 'region', + table: 'regiao', + tableField: 'nome', + resultField: 'region_name', + where: { + relation: '=', + type: 'integer', + field: 'id' + }, + join: { + primary: 'id', + foreign: 'regiao_id', + foreignTable: '@' + } +}).addValue({ + name: 'min_year', + table: '@', + tableField: 'ano_censo', + resultField: 'year', + where: { + relation: '>=', + type: 'integer', + field: 'ano_censo' + } +}).addValue({ + name: 'max_year', + table: '@', + tableField: 'ano_censo', + resultField: 'year', + where: { + relation: '<=', + type: 'integer', + field: 'ano_censo' + } +}).addValue({ + name: 'school_year', + table: '@', + tableField: 'serie_ano_id', + resultField: 'school_year_id', + where: { + relation: '=', + type: 'integer', + field: 'serie_ano_id' + } +}).addValue({ + name: 'location', + table: '@', + tableField: 'localizacao_id', + resultField: 'location_id', + where: { + relation: '=', + type: 'integer', + field: 'localizacao_id' + } +}).addValue({ + name: 'period', + table: '@', + tableField: 'turma_turno_id', + resultField: 'period_id', + where: { + relation: '=', + type: 'integer', + field: 'turma_turno_id' + } +}); + +classroomCountApp.post('/', rqf.parse(), (req, res, next) => { + let classSize = JSON.parse(req.body.class_size) || null; + let integralTime = JSON.parse(req.body.integral_time) || null; + + console.log(classSize, integralTime); + + if(classSize == null || integralTime == null) { + res.statusCode = 400; + return res.json({err: {message: "There was an error processing class_size or integral_time. Check your JSON sintax and be sure you're sending both paramenters."}}); + } + req.classSize = classSize; + req.integralTime = integralTime; + + req.dims.state = true; + req.dims.city = true; + req.dims.period = true; + req.dims.school_year = true; + req.dims.location = true; + + req.sql.field('COUNT(*)', 'total') + .field("'Brasil'", 'name') + .field('matricula.ano_censo', 'year') + .from('matricula') + .group('matricula.ano_censo') + .order('matricula.ano_censo') + .where('matricula.tipo<=3'); + + next(); +}, rqf.build(), query, id2str.transform(), (req, res, next) => { + req.enrollment = req.result; + + // Gera a relação etapa de ensino X ano escolar + req.educationSchoolYear = {}; + for(let i = 10; i < 80; ++i) { + if(id2str.schoolYear(i) !== id2str.schoolYear(99)) { + let educationLevelId = Math.floor(i/10); + + let classSize = req.classSize.find((el) => {return el.id === educationLevelId}); + let integralTime = req.integralTime.find((el) => {return el.id === educationLevelId}); + + let numberStudentClass = (typeof classSize !== 'undefined') ? classSize.numberStudentClass : null; + let offerGoal = (typeof integralTime !== 'undefined') ? integralTime.offerGoal : null; + + req.educationSchoolYear[i] = { + id: educationLevelId, + name: id2str.educationLevelShort(educationLevelId), + numberStudentClass, + offerGoal + }; + } + } + + req.resetSql(); + next(); +}, rqf.parse(), (req, res, next) => { + + req.dims.state = true; + req.dims.city = true; + req.dims.location = true; + + req.sql.field('SUM(escola.num_salas)', 'total') + .field("'Brasil'", 'name') + .field('escola.ano_censo', 'year') + .from('escola') + .group('escola.ano_censo') + .order('escola.ano_censo') + .where('escola.situacao_de_funcionamento = 1 AND escola.local_func_predio_escolar = 1'); + + next(); +}, rqf.build(), query, id2str.transform(), (req, res, next) => { + delete req.dims; + delete req.filter; + next(); +}, rqf.parse(), rqf.build(), (req, res, next) => { + req.classroom = req.result; + let classroom = []; + + // req.result = [{classroom: req.classroom, enrollment: req.enrollment}]; return next(); + + // Cria estrutura de resposta requisitada: + let i = 0; + let j = 0; + let result = []; + let hashSet = new Set(); + while (i < req.classroom.length) { + let classroom = req.classroom[i]; + // Cria hash única para cada espacialidade, dado um ano + let hash = '' + classroom.year + classroom.state_id + classroom.city_id; + // Estrutura do objeto do resultado final + let obj = { + year: classroom.year, + name: classroom.name, + state_id: classroom.state_id, + state_name: classroom.state_name, + city_id: classroom.city_id, + city_name: classroom.city_name, + locations: [] + }; + + let currentClassroomObj = null; + if( !hashSet.has(hash) ) { + hashSet.add(hash); + result.push(obj); + currentClassroomObj = obj; + } else { // Se a hash já existe, já temos a cidade nos resultados. Como está ordenado, é o último valor nos resultados + currentClassroomObj = result[result.length - 1]; + } + + // Inserimos a localidade no array de locations da sala + let location = { + location_id: classroom.location_id, + location_name: classroom.location_name, + total_classroom: parseInt(classroom.total, 10), + total_classroom_be_built: 0, + education_level: [] + }; + currentClassroomObj.locations.push(location); + + // Partimos para as etapas de ensino/anos escolares + + let enrollmentMatch = true; + j = 0; + let educationLevelSet = new Set(); + while(enrollmentMatch && j < req.enrollment.length) { + let enrollment = req.enrollment[j]; + if(typeof enrollment === 'undefined') { + enrollmentMatch = false; + continue; + } + if(enrollment.city_id != classroom.city_id) { // Se as cidades não são iguais, já passamos do range + enrollmentMatch = false; + continue; + } + + if(enrollment.year != classroom.year || enrollment.location_id != classroom.location_id) { // Se ano ou localização são diferentes, passa para o próximo + ++j; + continue; + } + + // Remove se o período é nulo (não dá pra usar no cálculo) + if(enrollment.period_id == null) { + req.enrollment.splice(j, 1); + continue; + } + + // Temos uma matrícula com cidade, ano e localidades certos + // "Consome" a matrícula (remove do vetor de matrículas) + req.enrollment.splice(j, 1); + + // Cria a etapa de ensino adequada + let enrollmentEducationLevel = req.educationSchoolYear[enrollment.school_year_id]; + // Se não há um número de alunos por turna para a etapa de ensino, ignoramos a entrada + if(enrollmentEducationLevel.numberStudentClass == null) continue; + + let educationLevel = null; + if(!educationLevelSet.has(enrollmentEducationLevel.id)) { + educationLevelSet.add(enrollmentEducationLevel.id); + + educationLevel = { + education_level_short_id: enrollmentEducationLevel.id, + education_level_short_name: enrollmentEducationLevel.name, + enrollment: { + total_enrollment_day: 0, + total_enrollment_night: 0, + full_period_classes: 0, + day_classes: 0, + night_classes: 0, + total_classrooms_needed: 0 + } + }; + + // Para manter a ordem da etapa de ensino + if (location.education_level.length == 0) { + location.education_level.push(educationLevel); + } else { + let k = location.education_level.length - 1; + let el = location.education_level[k]; + while (k >= 0) { + if(educationLevel.education_level_short_id < el.education_level_short_id) { + --k; + if(k>=0) el = location.education_level[k]; + } else break; + } + k++; + location.education_level.splice(k, 0, educationLevel); + } + } else { + let k = 0; + let el = location.education_level[k]; + while(k < location.education_level.length) { + if(el.education_level_short_id != enrollmentEducationLevel.id) { + ++k; + if(k<location.education_level.length) el = location.education_level[k]; + } else break; + } + if(k >= location.education_level.length) --k; + educationLevel = location.education_level[k]; + } + + // Soma os totais de matrícula da etapa de ensino + educationLevel.enrollment.total_enrollment_day += (enrollment.period_id < 3 && enrollment.period_id != null) ? enrollment.total : 0; + educationLevel.enrollment.total_enrollment_night += (enrollment.period_id == 3) ? enrollment.total : 0; + + // Calcula o número de turmas parcial + // Turmas de período integral + educationLevel.enrollment.full_period_classes = Math.ceil((educationLevel.enrollment.total_enrollment_day * (enrollmentEducationLevel.offerGoal/100)) / enrollmentEducationLevel.numberStudentClass); + + // Turmas diurnas + educationLevel.enrollment.day_classes = Math.ceil((educationLevel.enrollment.total_enrollment_day / enrollmentEducationLevel.numberStudentClass) - educationLevel.enrollment.full_period_classes); + + // Turmas noturnas + educationLevel.enrollment.night_classes = Math.ceil((educationLevel.enrollment.total_enrollment_night / enrollmentEducationLevel.numberStudentClass)); + + // Total de salas + educationLevel.enrollment.total_classrooms_needed = (educationLevel.enrollment.full_period_classes + educationLevel.enrollment.day_classes); + + if(educationLevel.enrollment.night_classes > educationLevel.enrollment.day_classes) educationLevel.enrollment.total_classrooms_needed += (educationLevel.enrollment.night_classes - educationLevel.enrollment.day_classes); + + } + + // Calculamos o total classroom be built para o município usando reduce + location.total_classroom_be_built = location.education_level.reduce((total, atual) => { + return total + atual.enrollment.total_classrooms_needed; + }, 0) - location.total_classroom; + + if(location.total_classroom_be_built < 0) location.total_classroom_be_built = 0; + + ++i; + } + + // Agregar por estado e brasil + let reduction = null; + if(req.dims.state || !req.dims.city) { // Se um dos dois acontecer, sabemos que devemos agregar + let i = 0; + reduction = []; + let reductionSet = new Set(); + while (i < result.length) { + let city = result[i]; + let obj = { + year: city.year, + name: city.name + } + + if(req.dims.state) { + obj.state_id = city.state_id; + obj.state_name = city.state_name; + } + + obj.locations = []; + + let hash = '' + city.year; + if(req.dims.state) hash += '' + city.state_id; + + let currentObj = null; + if(!reductionSet.has(hash)) { + reductionSet.add(hash); + reduction.push(obj); + currentObj = obj; + } else { // Está ordenado, podemos pegar o último + currentObj = reduction[reduction.length - 1]; + } + + // Fazer "merge" do array locations da cidade com o da agregação + if(currentObj.locations.length == 0) { + currentObj.locations = [...city.locations]; + } else { + let j = 0; + let k = 0; + let cityLocation = null; + let currentLocation = null; + while((typeof cityLocation !== 'undefined') && (typeof currentLocation !== 'undefined')) { + cityLocation = city.locations[j]; + currentLocation = currentObj.locations[k]; + if(cityLocation.location_id < currentLocation.location_id) { + ++j; + cityLocation = city.locations[j]; + continue; + } else if(cityLocation.location_id > currentLocation.location_id) { + ++k; + currentLocation = currentObj.locations[k]; + continue; + } + + // Fazer merge do array education_level + if(currentLocation.education_level.length == 0) { + currentLocation.education_level = [...cityLocation.education_level]; + } else { + let l = 0; + while(l < cityLocation.education_level.length) { + let cityEducation = cityLocation.education_level[l]; + let m = 0; + let currentEducation = currentLocation.education_level[m]; + while(m < currentLocation.education_level.length && cityEducation.education_level_short_id > currentEducation.education_level_short_id) { + ++m; + currentEducation = currentLocation.education_level[m]; + } + if(m >= currentLocation.education_level.length) --m; + currentEducation = currentLocation.education_level[m]; + + if(currentEducation.education_level_short_id == cityEducation.education_level_short_id) { + currentEducation.enrollment.total_enrollment_day += cityEducation.enrollment.total_enrollment_day; + currentEducation.enrollment.total_enrollment_night += cityEducation.enrollment.total_enrollment_night; + currentEducation.enrollment.full_period_classes += cityEducation.enrollment.full_period_classes; + currentEducation.enrollment.day_classes += cityEducation.enrollment.day_classes; + currentEducation.enrollment.night_classes += cityEducation.enrollment.night_classes; + currentEducation.enrollment.total_classrooms_needed += cityEducation.enrollment.total_classrooms_needed; + } else { + if(currentEducation.education_level_short_id < cityEducation.education_level_short_id) { + currentLocation.education_level.splice(++m, 0, cityEducation); + } else { + currentLocation.education_level.splice(m, 0, cityEducation); + } + } + ++l; + } + } + + currentLocation.total_classroom += cityLocation.total_classroom; + currentLocation.total_classroom_be_built += cityLocation.total_classroom_be_built; + + ++j; + cityLocation = city.locations[j]; + } + } + ++i; + } + } + + req.result = reduction || result; + + next(); +}, response('classroom_count')); + +module.exports = classroomCountApp; \ No newline at end of file