Desenvolvimento de uma Ferramenta Vue.js para Cálculo de Sementes

Introdução Funcional

O projeto visa criar uma ferramenta simples para auxiliar no cálculo de quantidade de sementes. Embora a matemática envolvida seja básica e agricultores experientes raramente a utilizem, a demanda surgiu para automatizar o processo. Os parâmetros principais considerados são:

  • densidade: Densidade de plantio.
  • kwei: Peso de cem sementes em gramas.
  • hectareQuota: Quantidade de sementes por hectare em Kg.
  • furrowSpacing: Espaçamento entre sulcos em cm.
  • seedsPerMeterOfFurrow: Número de sementes por metro de sulco.
  • plantSpacing: Espaçamento entre plantas em cm.
  • decimalPlaces: Número de casas decimais a serem exibidas.

O parâmetro decimalPlaces foi adicionado para controle de precisão durante os testes. As relações de cálculo conhecidas são:

  • hectareQuota = densidade * kwei / 10
  • seedsPerMeterOfFurrow = (furrowSpacing / 100) * densidade
  • plantSpacing(cm) = 100 / ((furrowSpacing / 100) * densidade)

Observa-se que a relação entre o espaçamento dos sulcos e a densidade não foi fornecida, impedindo uma interconexão automática entre esses campos no momento.

Configuração do Projeto

Para agilizar o desenvolvimento e evitar a criação de uma interface de usuário complexa, foi utilizado o framwork Element UI do Vue.js. A biblioteca math.js foi empregada para os cálculos. A inicialização do projeto incluiu o Vuex e outras configurações padrão, aproveitando um template pré-existente para otimizar o tempo.

Implementação

Configuração do Roteador (router.js)

O modo de hash foi escolhido para o roteamento, com uma configuração simples de redirecionamento:


const routes = [
 {
   path: "/seed-calculator",
   name: "SeedCalculator",
   component: () => import("../views/SeedCalculator.vue"),
 },
];
   

Configuração do math.js

A biblioteca math.js foi ipmortada globalmente no arquivo main.js:


import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

import "element-ui/lib/theme-chalk/index.css";

import * as math from "mathjs/number";
Vue.prototype.$math = math;

import {
 Form,
 FormItem,
 Input,
 Button,
 Col,
 Select,
 Option,
} from "element-ui";

Vue.use(Form);
Vue.use(FormItem);
Vue.use(Input);
Vue.use(Button);
Vue.use(Col);
Vue.use(Select);
Vue.use(Option);

Vue.config.productionTip = false;

new Vue({
 router,
 store,
 render: h => h(App)
}).$mount("#app");
   

Componente de Página (SeedCalculator.vue)

O componente principal, nomeado SeedCalculator.vue, contém a interface e a lógica de cálculo:


<template>
 <div>
   <el-col :span="8">
     <el-row>
       <el-form
         :model="seedForm"
         ref="seedFormRef"
         label-width="150px"
         class="seed-form"
       >
         <el-form-item label="Casas Decimais" prop="decimalPlaces">
           <el-select v-model="seedForm.decimalPlaces" placeholder="Selecione">
             <el-option
               v-for="item in decimalOptions"
               :key="item"
               :label="item"
               :value="item"
             >
             </el-option>
           </el-select>
           <span> (Padrão: 2)</span>
         </el-form-item>
         <el-form-item label="Densidade (sementes/m²)" prop="density">
           <el-input v-model.number="seedForm.density" @input="calculateDerivedValues"></el-input>
         </el-form-item>
         <el-form-item label="Peso de 100 sementes (g)" prop="kwei">
           <el-input v-model.number="seedForm.kwei" @input="calculateDerivedValues"></el-input>
         </el-form-item>
         <el-form-item label="Quota por Hectare (Kg)" prop="hectareQuota">
           <el-input
             v-model="seedForm.hectareQuota"
             disabled
             placeholder="Densidade * Peso 100 sementes / 10"
           ></el-input>
         </el-form-item>
         <el-form-item label="Espaçamento Sulcos (cm)" prop="furrowSpacing">
           <el-input
             v-model.number="seedForm.furrowSpacing"
             @input="calculateDerivedValues"
             placeholder="Ex: 65"
           ></el-input>
           <span> (Relação com densidade não definida)</span>
         </el-form-item>
         <el-form-item label="Sementes/Metro de Sulco" prop="seedsPerMeterOfFurrow">
           <el-input
             v-model="seedForm.seedsPerMeterOfFurrow"
             disabled
             placeholder="(Espaçamento Sulcos / 100) * Densidade"
           ></el-input>
         </el-form-item>
         <el-form-item label="Espaçamento Plantas (cm)" prop="plantSpacing">
           <el-input
             v-model="seedForm.plantSpacing"
             disabled
             placeholder="100 / Sementes/Metro de Sulco"
           ></el-input>
         </el-form-item>

         <el-form-item>
           <el-button type="primary" @click="submitForm">Calcular</el-button>
           <el-button @click="resetForm">Limpar</el-button>
         </el-form-item>
       </el-form>
     </el-row>
   </el-col>
 </div>
</template>

<script>
import { create, all } from "mathjs/number";

export default {
 data() {
   const validatePositiveNumber = (rule, value, callback) => {
     if (value === null || value === "") {
       callback(new Error(rule.message));
     } else if (typeof value === 'number' && value > 0 && isFinite(value)) {
       callback();
     } else {
       callback(new Error("Deve ser um número positivo válido."));
     }
   };

   return {
     seedForm: {
       density: null,
       kwei: null,
       hectareQuota: "",
       furrowSpacing: 65,
       seedsPerMeterOfFurrow: "",
       plantSpacing: null,
       decimalPlaces: 2
     },
     rules: {
       density: [
         { required: true, message: "A densidade é obrigatória", trigger: "blur" },
         { validator: validatePositiveNumber, message: "A densidade deve ser um número positivo.", trigger: "blur" }
       ],
       kwei: [
         { required: true, message: "O peso de 100 sementes é obrigatório", trigger: "blur" },
         { validator: validatePositiveNumber, message: "O peso de 100 sementes deve ser um número positivo.", trigger: "blur" }
       ],
       furrowSpacing: [
         { required: true, message: "O espaçamento entre sulcos é obrigatório", trigger: "blur" },
         { validator: validatePositiveNumber, message: "O espaçamento entre sulcos deve ser um número positivo.", trigger: "blur" }
       ]
     },
     decimalOptions: [0, 1, 2, 3, 4],
     mathInstance: create(all)
   };
 },
 methods: {
   roundToDecimalPlaces(value, decimals) {
     if (value === null || isNaN(value)) return "";
     const factor = Math.pow(10, decimals);
     return Math.round(value * factor) / factor;
   },
   calculateDerivedValues() {
      if (this.seedForm.density === null || this.seedForm.kwei === null || this.seedForm.furrowSpacing === null) {
          this.seedForm.hectareQuota = "";
          this.seedForm.seedsPerMeterOfFurrow = "";
          this.seedForm.plantSpacing = null;
          return;
      }

     const { density, kwei, furrowSpacing, decimalPlaces } = this.seedForm;

     // Calcular Quota por Hectare
     const calculatedHectareQuota = this.mathInstance.chain(density).multiply(kwei).divide(10).done();
     this.seedForm.hectareQuota = this.roundToDecimalPlaces(calculatedHectareQuota, decimalPlaces);

     // Calcular Sementes por Metro de Sulco
     const calculatedSeedsPerMeter = this.mathInstance.chain(furrowSpacing).divide(100).multiply(density).done();
     this.seedForm.seedsPerMeterOfFurrow = this.roundToDecimalPlaces(calculatedSeedsPerMeter, decimalPlaces);

     // Calcular Espaçamento entre Plantas
      if (calculatedSeedsPerMeter > 0) {
           const calculatedPlantSpacing = this.mathInstance.chain(100).divide(calculatedSeedsPerMeter).done();
           this.seedForm.plantSpacing = this.roundToDecimalPlaces(calculatedPlantSpacing, decimalPlaces);
      } else {
          this.seedForm.plantSpacing = null;
      }
   },
   submitForm() {
     this.$refs.seedFormRef.validate((valid) => {
       if (valid) {
         this.calculateDerivedValues();
         this.$message({
           message: "Cálculos realizados com sucesso!",
           type: "success"
         });
       } else {
         this.$message.error("Por favor, corrija os erros no formulário.");
         return false;
       }
     });
   },
   resetForm() {
     this.$refs.seedFormRef.resetFields();
     this.seedForm.hectareQuota = "";
     this.seedForm.seedsPerMeterOfFurrow = "";
   }
 },
 watch: {
     'seedForm.decimalPlaces'(newVal) {
         // Recalcula os valores quando as casas decimais mudam
         this.calculateDerivedValues();
     }
 },
 mounted() {
     // Realiza o cálculo inicial se houver valores padrão preenchidos
     if (this.seedForm.density && this.seedForm.kwei && this.seedForm.furrowSpacing) {
         this.calculateDerivedValues();
     }
 }
};
</script>

<style scoped>
.seed-form {
 padding: 20px;
 border: 1px solid #eee;
 border-radius: 5px;
 box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}
.el-form-item span {
   font-size: 0.8em;
   color: #909399;
   margin-left: 10px;
}
</style>
   

Dicas Úteis

Validação Personalizada

A validação de formulário pode ser estendida com regras personalizadas. No exemplo abaixo, uma função validatePositiveNumber é definida para garantir que os campos numéricos sejam positivos e válidos:


const validatePositiveNumber = (rule, value, callback) => {
 if (value === null || value === "") {
   callback(new Error(rule.message));
 } else if (typeof value === 'number' && value > 0 && isFinite(value)) {
   callback();
 } else {
   callback(new Error("Deve ser um número positivo válido."));
 }
};
   

Esta função é então asssociada a um campo de regras específico:


rules: {
 density: [
   { required: true, message: "A densidade é obrigatória", trigger: "blur" },
   { validator: validatePositiveNumber, message: "A densidade deve ser um número positivo.", trigger: "blur" }
 ],
 // ... outras regras
}
   

Utilização do math.js

O math.js oferece diversas funções matemáticas. Para operações encadeadas, utilize o método chain():


// Exemplo: Calcular (9000 / 100) * 2
const result = this.mathInstance.chain(9000)
                 .divide(100)
                 .multiply(2)
                 .done();
   

Para formatação de números com precisão especificada:


const preciseResult = this.mathInstance.format(result, { precision: 14 });
// Ou com um número inteiro de casas decimais
const formattedResult = this.mathInstance.format(result, 4);
   

Na implementação deste projeto, foi utilizada uma função auxiliar roundToDecimalPlaces para arredondamento simples, que é mais adequada para este caso de uso específico do que o math.format.

Controle de Precisão Decimal

Para garantir a precisão desejada nos cálculos, a função roundToDecimalPlaces foi implementada. Diferente do método toFixed() que retorna uma string e pode ter comportamentos inesperados com números de ponto flutuante, Math.round() combinado com fatores de potência de 10 oferece um controle numérico mais confiável para arredondamento:


/**
* Arredonda um valor para um número específico de casas decimais.
* @param {number} value - O valor numérico a ser arredondado.
* @param {number} decimals - O número de casas decimais a manter.
* @returns {number|string} O valor arredondado ou string vazia se a entrada for inválida.
*/
roundToDecimalPlaces(value, decimals) {
 if (value === null || isNaN(value)) return "";
 const factor = Math.pow(10, decimals);
 return Math.round(value * factor) / factor;
},
   

Tags: Vue.js Element UI javascript math.js Cálculo

Publicado em 6-25 23:02