Es mostren els missatges amb l'etiqueta de comentaris programació. Mostrar tots els missatges
Es mostren els missatges amb l'etiqueta de comentaris programació. Mostrar tots els missatges

4 de juliol del 2020

ERROR: node-sass errors.missingBinary() [SOLUCIONAT]

Us ha passat mai que en anar a executar la vostre aplicació Ionic/Angular us salti un error del tipus ?
  • Node Sass could not find a binding for your current environment

I que un npm install clàssic tampoc no ho solucioni ?

No patiu, que no és tan greu. L'error apareix típicament quan hem canviat la versió del node. Llavors, simplement cal recompilar el paquet node-sass a mà:
npm rebuild node-sass
I tal com podeu veure en la imatge següent, després ja podrem executar l'aplicació normalment:


Ja em comentareu si us ha servit !

29 de març del 2018

Ionic: aplicació per saber l'hora de sortida/posta del sol (VII)

Part VII: Personalització d'icones i tipus de lletra

Aquest article ens mostrarà com canviar el tipus de lletra que utilitza una aplicació i com afegir icones personalitzades.

Fins ara hem fet una aplicació per saber l'hora de sortida i posta del sol:
Ara anem a personalitzar-la una mica, amb icones i tipus de lletra diferents.

Comencem pel tipus de lletra. En tenim moltes de disponibles a Google fonts. N'escollim una que s'ajusti a les necessitats (Poppins en el nostre cas) i descomprimim els fitxer *.ttf a assets/fonts. Tot seguit declarem la nova font i la posem per defecte, editant el fitxer theme/variables.scss per afegir:
 ...
 // Fonts
 // --------------------------------------------------

 //@import "roboto";
 //@import "noto-sans";

 @font-face {
  font-family: "Poppins";
  //font-style: normal;
  //font-weight: 400;
  src:  url($font-path+'/Poppins-Regular.ttf');
 }
 @font-face {
  font-family: "Poppins";
  src:  url($font-path+'/Poppins-Bold.ttf');
  font-weight: bold;
 }
 @font-face {
  font-family: "Poppins";
  src: url($font-path+'/Poppins-Italic.ttf');
  font-style: italic;
 }
 @font-face {
  font-family: "Poppins";
  src: url($font-path+'/Poppins-BoldItalic.ttf');
  font-weight: bold;
  font-style: italic;
 }

 $font-family-base: "Poppins";

 $font-family-md-base: "Poppins";
 $font-family-ios-base: "Poppins";
 $font-family-wp-base: "Poppins";

Pel que fa a les icones, si n'hem d'utilitzar moltes seria convenient declarar un nou tipus de lletra, però si són poques, és molt molt senzill afegir-les directament. Per això, escollim icones lliures, per exemple a Flaticon, procurant que siguin d'un sol color i en format SVG. Llavors les afegim a app/app.scss:
 ...
 // --- Custom icons -------------------------------------------------------------------
 //
 // https://stackoverflow.com/a/44575053/1581368
 // To generate a font : https://yannbraga.com/2017/06/28/how-to-use-custom-icons-on-ionic-3/
 ion-icon {
    &[class*="custom-"] {
        // Instead of using the font-based icons we're applying SVG masks
        mask-size: contain;
        mask-position: 50% 50%;
        mask-repeat: no-repeat;
        background: currentColor;
        width: 1em;
        height: 1em;
    }
    // custom icons
    &[class*="custom-sunrise"] {
        mask-image: url(../assets/icon/sunrise.svg);
    }
    &[class*="custom-sunset"] {
        mask-image: url(../assets/icon/sunset.svg);
    }
 }

I llavors ja els podem utilitzar normalment a home.html:
 ...
    <ion-item-divider color="light">
      {{ "APP.timetable" | translate }}
    </ion-item-divider>
    <ion-item>
      <ion-icon name="custom-sunrise" color="orange" item-left></ion-icon>
      <ion-note item-right>{{ sunrise }}</ion-note>
    </ion-item>
    <ion-item>
      <ion-icon name="custom-sunset" color="blue" item-left></ion-icon>
      <ion-note item-right>{{ sunset }}</ion-note>
    </ion-item>
 ...

El resultat es pot veure a continuació:

Observem en el lateral com s'ha carregat correctament el fitxer de fonts (les versions normal i negreta, que són les que s'utilitzen).

I d'aquesta manera tan senzilla podem donar-li un aspecte més polit a una aplicació. Per acabar-ho d'arrodonir, completem la pàgines de contacte, de crèdits i afegim informació al repositori de Github (en el fitxer README.md).

Com sempre, teniu tot el codi a la vostre disposició en el Github: v1.0.

I amb aquests retocs finals podem donar l'aplicació per completada. Espero que hagi resultat útil. Si teniu alguna idea per millorar-la, traduccions a altres idiomes o qualsevol comentari, no dubteu a comentar/contactar.

24 de març del 2018

Ionic: aplicació per saber l'hora de sortida/posta del sol (VI)

Part VI: Traducció a diversos idiomes

Aquest article ens mostrarà com permetre diversos llenguatges en la nostre aplicació.

Fins ara hem fet una aplicació per saber l'hora de sortida i posta del sol:
Per introduir la possibilitat d'utilitzar diversos idiomes, utilitzarem el paquet ngx-translate. En primer lloc l'instal·lem:
 npm install @ngx-translate/core --save
 npm install @ngx-translate/http-loader --save

Seguint les instruccions d'ús, afegim a app.module.ts els mòduls i serveis necessaris:
 ...
 import { SplashScreen } from '@ionic-native/splash-screen';
 
 // ngx-translate
 import {TranslateModule, TranslateLoader} from '@ngx-translate/core';
 import {TranslateHttpLoader} from '@ngx-translate/http-loader';

 import { HttpClientModule, HttpClient } from '@angular/common/http';
 ...
 import { LangService } from '../providers/lang-service';

 // Translations
 export function createTranslateLoader(http: HttpClient) {
   return new TranslateHttpLoader(http, './assets/i18n/', '.json');
 }
 
 @NgModule({
   ...
   imports: [
     ...
     CalendarModule,
     TranslateModule.forRoot({
       loader: {
           provide: TranslateLoader,
           useFactory: (createTranslateLoader),
           deps: [HttpClient]
       }
     }),
     IonicModule.forRoot(MyApp)
   ],
   ...
   providers: [
     ...
     Network,
     LangService
   ]
 })
 ...

Observem com hem utilitzem un servei LangService per gestionar la detecció de l'idioma i el canvi de llenguatge. Per això creem un fitxer providers/lang-service.ts amb el següent contingut:
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
 import { TranslateService } from '@ngx-translate/core';

 @Injectable()
 export class LangService {
  onLang : BehaviorSubject = new BehaviorSubject("");

  lang_sets : Array = [];

  constructor(public translate: TranslateService) {
    console.log('### LangService');
  }

  init(lang_default: string, lang_sets: Array, lang_fallback?: string) {
    // Verify that some sets are present
    if(lang_sets.length==0) {
      console.error("[LangService] error: no lang_sets defined");
      return;
    } else {
      this.lang_sets = lang_sets;
    }

    // Verify default is present in lang_sets
    if(this.lang_sets.indexOf(lang_default) == -1) {
      console.log("[LangService] Warning: trying to use default lang '"+
                   lang_default+"' which is not present in lang_sets");
      console.log("[LangService] Warning: setting default to first" 
                   "language in set");
      lang_default = this.lang_sets[0];
    }

    // this language will be used as a fallback when a translation 
    // isn't found in the current language
    if(!lang_fallback) lang_fallback = lang_default;

    this.translate.setDefaultLang(lang_fallback);

    let langRegex = new RegExp(this.lang_sets.join("|"), 'gi');

    // get browser language by default
    let userLang = navigator.language.split('-')[0];
    userLang = langRegex.test(userLang) ? userLang : lang_default;

    this.setLanguage(userLang);
    // ----------------------------------------------------------------    
  }

  getCurrentLang() {return this.translate.currentLang};

  setLanguage(lang: string) {
    // Verify default is present in lang_sets
    if(this.lang_sets.indexOf(lang) == -1) {
      console.log("[LangService] Warning: trying to use lang '"+lang+
                  "' which is not present in lang_sets");
      return;
    }

    // the lang to use, if the lang isn't available, it will use 
    // the current loader to get them
    this.translate.use(lang);        
    console.log("[LangService] Using language: "+
                 this.translate.currentLang);

    // Inform to all subscriptors
    this.onLang.next(lang);
  }
 }

Observem com el servei:
  • [8] Declara un BehaviorSubject onLang que permetrà notificar un canvi d'idioma a la resta de l'aplicació.
  • [16-48] La funció init permet indicar quin idioma s'utilitza per defecte en l'aplicació (lang_default), el conjunt de llenguatges possibles (lang_sets), i quin és el llenguatge que s'utilitzarà si una traducció no existeix en un idioma (normalment, serà el llenguatge que utilitza com a primari el programador). La funció fa comprovacions d'errors i llavors estableix com a llenguatge per defecte a usar, el que tingui definit l'usuari en el seu dispositiu/navegador.
  • [50] La funció getCurrentLang simplement retorna el llenguatge actualment actiu en l'aplicació.
  • [52-68] La funció setLanguage estableix el llenguatge a utilitzar en l'aplicació, si existeix en el conjunt de disponibles. En cas contrari, agafa el que hi hagi per defecte en l'aplicació.

Aquest servei s'inicialitza a app.component.ts:
 ...
 import { LangService } from '../providers/lang-service';
 ...
  constructor( ..., langService : LangService ) 
  {
     platform.ready().then(() => {
       ...
       langService.init("en", ["en", "ca"]);
     });
   }
 ...

Podem veure com l'aplicació estarà en anglès i català. Per això, cal posar els fitxers d'idioma a assets/i18n/ca.json i assets/i18n/es.json. El nom es correspon al codi d'idioma i el contingut és un JSON senzills amb les traduccions. Per exemple, pel català:
 {
 "APP.title": "Sortida/posta de Sol",
 "APP.place": "Lloc",
 "APP.lat": "Latitud",
        ...
 }

Ara el que ens queda és utilitzar les traduccions en els missatges del programa, i permetre el canvi d'idioma. Modificarem la pàgina home.html:

 <ion-header>
  <ion-navbar>
    <ion-title>{{ "APP.title" | translate }}</ion-title>
  </ion-navbar>
 </ion-header>

 <ion-content padding>
  <ion-list>
    <ion-item-divider color="light">
      {{ "APP.language" | translate }}
    </ion-item-divider>
    <ion-item>
      <ion-label>{{ "APP.lang-current" | translate }}</ion-label>
      <ion-select [(ngModel)]="lang" (ionChange)="doChangeLang()" item-right>
        <ion-option value="ca">Català</ion-option>
        <ion-option value="en">English</ion-option>
      </ion-select>              
    </ion-item>
    ...

Observem que:
  •  [3, 10, 13] S'han canviat els missatges de text pel seu identificador en el fitxer d'idioma i demanant la seva traducció.
  • [14-17] S'ha afegit una opció de canvi d'idioma que crida la funció doChangeLang().

La funció doChangeLang() s'implementa a home.ts, que s'ha modificat de la següent manera:
 ...
 import {TranslateService} from '@ngx-translate/core';
 import { LangService } from '../../providers/lang-service';
 ...
   constructor(  ...,
                 public translate: TranslateService,
                 public langService : LangService,  
               )
   ...
   ionViewDidLoad() {
     ...
     this.dateMsg = "APP.today";

     this.langService.onLang.subscribe(lang=> {
       this.lang = this.translate.currentLang;
       console.log("[HomePage] Current lang: "+this.lang);
       moment.locale(this.lang);  
     });
   }
 
   selectDate() {
     const options: CalendarModalOptions = {
       title: this.translate.instant("APP.date-choose"),
       canBackwardsSelected: true,
       closeLabel: this.translate.instant('APP.Cancel'),
       doneLabel: this.translate.instant('APP.Done'),
     ...
   }
   ...
   doChangeLang() {
     this.langService.setLanguage(this.lang);
   }
 }

On podem veure com:
  • Assignem codis de traducció (p.e. a dateMsg) per mostrar-los traduïts en un html.
  • Utilitzem this.translate.instant() per traduir directament.
  • Es crida al servei langService per canviar l'idioma globalment.
  • Ens subscribim a langService.onLang per canviar el format del dia/hora segons el llenguatge actual.
També cal modificar location-select.html per traduir els missatges, tal com ho hem fet amb home.html.

Però la part més delicada és modificar l'idioma de GoogleMaps. El llenguatge es fixa en crear el mapa, i fins on jo sé, l'API no permet canviar-lo després. Per això, ha calgut modificar el codi de google-maps-service.ts de la següent manera:

 ...
 import { LangService } from '../providers/lang-service';
 ...
  constructor(  ...,
                 public langService : LangService,  
               ) 
  {
     this.langService.onLang.subscribe(lang=> {
       if(this.googleMapScriptElement) {
         console.log("[GoogleMapsService] Current lang: "+lang);
 
         this.updateScriptSrc();
       }
     });
  }
  ... 
  updateScriptSrc() {
    let lang = this.langService.getCurrentLang();

    console.log("[GoogleMapsService].updateScriptSrc with lang: "+lang);
    
    if(this.googleMapScriptElement) {
      let head = document.getElementsByTagName('head')[0];
      let scripts = Array.prototype.slice.call(head.getElementsByTagName('script'));
      let styles = Array.prototype.slice.call(head.getElementsByTagName('style'));

      scripts.forEach(s=> {
        if(s.src.indexOf("://maps.google") != -1) head.removeChild(s);
        else if(!s.src || s.src.indexOf("cordova.js") != -1 || 
                          s.src.indexOf("ion-dev.js") != -1) {}
        else console.log("Skip removing <script src='"+s.src+"'");
      });
      styles.forEach(s=> {
        if(s.textContent.indexOf(".gm-style") != -1) head.removeChild(s);
        else console.log("Skip removing <style>", s);
      });
      document.body.removeChild(this.googleMapScriptElement);
    }

    let script = document.createElement("script");
    script.id = "googleMaps";

    if(this.apiKey){
      script.src = 'http://maps.google.com/maps/api/js?key=' + this.apiKey + 
                   '&callback=mapInit&libraries=places&language='+lang;
    } else {
      script.src = 'http://maps.google.com/maps/api/js?callback=mapInit&'
                            'amp;libraries=places&language='+lang;       
    }    

    this.googleMapScriptElement = document.body.appendChild(script);
  }
  ...

Bàsicament, importem el LangService per subscriure'ns al canvi d'idioma. Quan es produeix, s'actualitza GoogleMaps, eliminant els scripts i estils que afegeix i recreant el mapa amb el nou llenguatge.

Amb tot plegat, podem veure el resultat en la imatge:



Podem observar en la consola els missatges del LangService quan es detecta l'idioma i quan es canvia. També el nou control per canvi d'idioma en la pantalla.

Si ens fixem en la consola, podem veure que la recreació del mapa de GoogleMaps no és del tot perfecte. Tot i eliminar els scripts i estils per evitar duplicats, probablement encara queda alguna cosa que és detectada en carregar el nou mapa. A la llarga, això aniria consumint memòria, però tampoc no canviarem d'idioma constantment, no?

Com sempre, teniu tot el codi a la vostre disposició en el Github: v0.5 (amb traducció a diversos idiomes).

Ara ja tenim una aplicació funcional i en diversos idiomes, però podem fer-hi encara alguns canvis estètics, amb icones personalitzades i tipus de lletra diferents; i finalment, no oblidem completar el projecte donant les dades de contacte i afegint-hi una pàgina per donar crèdit als projectes i persones que l'han fet possible.

Seguiu l'evolució de l'aplicació:

7 de març del 2018

Ionic: aplicació per saber l'hora de sortida/posta del sol (V)

Part V: Versió amb Google Maps

Aquest article ens mostrarà com utilitzar Google Maps per escollir el lloc en l'aplicació per saber l'hora de sortida i posta de sol.

Fins ara hem vist:

Ara utilitzarem Google Maps per poder escollir el lloc. Tot i que hi ha molta informació sobre com utilitzar Google Maps, la informació sol estar incompleta o no acabar-se d'adaptar a les nostres necessitats concretes.

En general, una de les meves primeres fonts d'informació són els articles de Josh Morony. Són acurats i força actualitzats. Tota una referència. I just podem trobar-hi el que busquem: "com fer una pàgina de selecció de lloc amb Google Maps i Ionic". Seguirem el tutorial i fem els canvis necessaris per afegir la funcionalitat a la nostra aplicació.

Afegim el mòdul de xarxa, que permetrà verificar quan tenim connexió de dades, i les definicions de tipus per Google Maps:
 $ ionic cordova plugin add cordova-plugin-network-information
 $ npm install --save @ionic-native/network
 $ npm install @types/google-maps --save

Seguint el tutorial, afegim la pàgina nova (LocationSelect) i els nous proveïdors a app.module.ts:
 ...
 import { CalendarModule } from "ion2-calendar";
 import { LocationSelect } from '../pages/location-select/location-select';
 import { Connectivity } from '../providers/connectivity-service';
 import { GoogleMapsService } from '../providers/google-maps-service';
 import { Network } from '@ionic-native/network';
 ...
  declarations: [
    ...
    HomePage,
    LocationSelect,
    TabsPage
  ],
  ...
  entryComponents: [
    ...
    HomePage,
    LocationSelect,
    TabsPage
  ],
  providers: [
    ...
    Geolocation,
    Connectivity,
    GoogleMapsService,
    Network
  ]
 ...

Ara afegim les pàgines dels proveïdors de servei, segons el tutorial. En primer lloc, providers/connectivity-service.ts:
 import { Injectable } from '@angular/core';
 import { Network } from '@ionic-native/network';
 import { Platform } from 'ionic-angular';

 @Injectable()
 export class Connectivity {
  onDevice: boolean;

  constructor(public platform: Platform, public network: Network) {
    this.onDevice = this.platform.is('cordova');
  }

  isOnline(): boolean {
    if(this.onDevice && this.network.type){
      return this.network.type != 'none';
    } else {
      return navigator.onLine; 
    }
  }

  isOffline(): boolean {
    if(this.onDevice && this.network.type){
      return this.network.type == 'none';
    } else {
      return !navigator.onLine;   
    }
  }

  watchOnline(): any { return this.network.onConnect(); }
  watchOffline(): any { return this.network.onDisconnect(); }
 }

I tot seguit, providers/google-maps-service.ts:
 import { Injectable } from '@angular/core';
 import { Connectivity } from './connectivity-service';
 import { Geolocation } from '@ionic-native/geolocation';

 @Injectable()
 export class GoogleMapsService {
  mapElement: any;
  pleaseConnect: any;
  map: any;
  mapInitialised: boolean = false;
  mapLoaded: any;
  mapLoadedObserver: any;
  currentMarker: any;
  apiKey: string; //  = "YOUR_API_KEY";
  lat: number = null;
  lon: number = null;

  constructor( public connectivityService: Connectivity, 
               public geolocation: Geolocation ) 
  {
  }

  init( mapElement: any, pleaseConnect: any, 
        lat: number, lon: number 
       ): Promise 
  {

    this.mapElement = mapElement;
    this.pleaseConnect = pleaseConnect;
    this.lat = lat;
    this.lon = lon;

    return this.loadGoogleMaps();
  }

  loadGoogleMaps(): Promise {
    return new Promise((resolve) => {
      if(typeof google=="undefined" || typeof google.maps=="undefined") {
        console.log("Google maps JavaScript needs to be loaded.");
        this.disableMap();

        if(this.connectivityService.isOnline()){
          window['mapInit'] = () => {
            this.initMap().then(() => {
              resolve(true);
            });

            this.enableMap();
          }

          let script = document.createElement("script");
          script.id = "googleMaps";

          if(this.apiKey){
            script.src = 'http://maps.google.com/maps/api/js?key=' + 
                       this.apiKey + '&callback=mapInit&libraries=places';
          } else {
            script.src = 'http://maps.google.com/maps/api/js' + 
                                     '?callback=mapInit&libraries=places';       
          }

          document.body.appendChild(script);  
        } 
      } else {
        if(this.connectivityService.isOnline()){
          this.initMap();
          this.enableMap();
        } else {
          this.disableMap();
        }

 resolve(true);
      }

      this.addConnectivityListeners();
    });
  }

  initMap(): Promise {
    this.mapInitialised = true;

    return new Promise((resolve) => {
      if(this.lat && this.lon) {
        this.initMapPos(this.lat, this.lon);
        resolve(true);

      } else {
        this.geolocation.getCurrentPosition().then((pos) => {
          this.initMapPos(pos.coords.latitude, pos.coords.longitude);
          resolve(true);

        }).catch((error) => {
          console.log('Error getting location'+JSON.stringify(error));
          return Promise.reject("Unable to get location");
        });
      }
    });
  }

  initMapPos(lat: number, lon: number) {
    let latLng = new google.maps.LatLng(lat, lon);
    let mapOptions = {
      center: latLng,
      zoom: 15,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    }

    this.map = new google.maps.Map(this.mapElement, mapOptions);
  }

  disableMap(): void {
    if(this.pleaseConnect){
      this.pleaseConnect.style.display = "block";
    }
  }

  enableMap(): void {
    if(this.pleaseConnect){
      this.pleaseConnect.style.display = "none";
    }
  }

  addConnectivityListeners(): void {
    this.connectivityService.watchOnline().subscribe(() => {
      setTimeout(() => {
        if(typeof google=="undefined" || typeof google.maps=="undefined") {
          this.loadGoogleMaps();
        } else {
          if(!this.mapInitialised) {
            this.initMap();
          }
          this.enableMap();
        }
      }, 2000);
    });

    this.connectivityService.watchOffline().subscribe(() => {
      this.disableMap();
    });
  }
 }

En aquest fitxer hem fet alguns canvis:
  • [6] Hem canviat el nom de la classe a GoogleMapsService.
  • [14] Hem eliminat l'apiKey. Se n'hauria de generar una per poder-la utilitzar en l'aplicació però, per fer proves, podem treballar sense.
  • [15-16] Declarem les variables per poder inicialitzar el mapa a partir d'una posició concreta, enlloc de la posició del nostre dispositiu.
  • [24-31] Afegim paràmetres per la posició desitjada a la funció init i els traslladem als corresponents camps de la classe. 
  • [59] Hem afegit el paràmetre que faltava a la petició: '&libraries=places'.
  • [79-109] En la funció initMap, utilitzem la posició obtinguda durant la inicialització, o si està buida, usem el servei de geolocalització. També hem separat el codi d'inicialització de Google Maps en una nova funció initMapPos per no repetir codi.

Pel que fa a la pàgina LocationSelect, el fitxer d'estils location-select/location-select.scss no s'ha modificat, en el fitxer location-select/location-select.html només s'han traduït els textos al català, i el fitxer controlador location-select/location-select.ts s'ha modificat per poder passar la localització actual al servei GoogleMapsService:
 ...
 import { NavParams } from 'ionic-angular';
 import { GoogleMapsService } from '../../providers/google-maps-service';
 ...
 export class LocationSelect {
    ...
    location: any; 
    lat: number = 42.582104;
    lon: number = 1.0008419;
  
    constructor( public navCtrl: NavController, 
   public params: NavParams,
   public maps: GoogleMapsService, 
   public platform: Platform, 
   public geolocation: Geolocation, 
   public viewCtrl: ViewController) 
    {
      this.searchDisabled = true;
      this.saveDisabled = true;

      this.lat = params.get('lat');
      this.lon = params.get('lon');
    }

    ionViewDidLoad(): void {
      this.maps.init( this.mapElement.nativeElement, 
                      this.pleaseConnect.nativeElement, 
                      this.lat, this.lon).then(() => {
        this.autocompleteService = 
               new google.maps.places.AutocompleteService();
 this.placesService = 
               new google.maps.places.PlacesService(this.maps.map);
 this.searchDisabled = false;
      }); 
    }
    ...

On:
  • [2] S'ha afegit la classe NavParams per poder passar paràmetres a la pàgina.
  • [8-9] S'han afegit membres a la classe per a guardar la posició inicial.
  • [12] S'utilitza el controlador NavParams en la classe, per a rebre els paràmetres.
  • [21-22] Es llegeixen i es guarden els paràmetres relacionats amb la posició.
  • [28] Els paràmetres de la posició s'utilitzen en la crida init del servei GoogleMapsService.

I finalment només ens queda modificar la pàgina HomePage per afegir la funcionalitat. En el controlador home.ts:
 ...
 import { LocationSelect } from '../location-select/location-select';
 ...
 export class HomePage {
  ...
  launchLocationPage() {
    let modal = this.modalCtrl.create( LocationSelect,
                                       {lat: this.lat, lon: this.lon});
    modal.onDidDismiss((location) => {
      if(location) {
        console.log("Nou lloc: ", location);
        
        this.lat = location.lat;
        this.lon = location.lng;
        this.posMsg = location.name;
        this.posError = false;
        this.getSunriseSunsetFromApi();
      }
    });

    modal.present(); 
  }
 }

Hem afegit:
  • [2] La importació de la nova pàgina per a seleccionar el lloc.
  • [6-22] La funció launchLocationPage per mostrar la finestra de selecció de lloc. Observem com passem els paràmetres de la posició actual a la nova pàgina [8], i com s'actualitzen les dades i missatges en cas d'haver escollit un lloc, en el retorn de la pàgina [13-17].

En la part de visualització home.html, simplement activem la crida a la funció anterior:
  ...
  <ion-item color="light" *ngIf="posError" (click)="launchLocationPage()">
    <h2 text-wrap>No s'ha pogut obtenir la posició actual.</h2>
    <p text-wrap>Verifica el GPS i/o els permisos.</p>
    <ion-icon name="warning" color="danger" item-left></ion-icon>
  </ion-item>
  <ion-item (click)="launchLocationPage()">
    <ion-label>Latitud</ion-label>
    <ion-note item-right>{{ lat | number: "1.1-7" }}</ion-note>
  </ion-item>
  <ion-item (click)="launchLocationPage()">
    <ion-label>Longitud</ion-label>
    <ion-note item-right>{{ lon | number: "1.1-7" }}</ion-note>
  </ion-item>
  ...

I ja ho tenim tot a punt ! Provem de seguida la nova funcionalitat:

Podem veure com el mapa s'obre en una nova finestra superposada, si té prou espai, i com permet escollir un lloc, auto-completant les opcions. En escollir-ne una, el mapa es posició en ella i, en guardar-la, s'utilitza per calcular les hores de sortida/posta del sol en la nostra aplicació:

Podem veure en la consola l'objecte retornat pel servei de Google Maps. Les seves propietats s'utilitzen per actualitzar les dades en la pàgina principal.
Com sempre, teniu tot el codi a la vostre disposició en el Github: v0.4 (amb Google Maps).

Ara ja tenim una aplicació funcional, però encara la podem completar en alguns aspectes. Per exemple, podríem afegir-hi traducció a diversos idiomes; també es poden fer alguns canvis estètics, amb icones personalitzades i tipus de lletra diferents; i finalment, completar el projecte donant les dades de contacte i afegint una pàgina per donar crèdit als projectes i persones que l'han fet possible.

Seguiu l'evolució de l'aplicació:

2 de març del 2018

Ionic: aplicació per saber l'hora de sortida/posta del sol (IV)

Part IV: Versió amb calendari

Aquest article ens mostrarà com afegir un calendari per escollir el dia en l'aplicació per saber l'hora de sortida i posta de sol.

Fins ara hem vist:

Ara afegirem un calendari senzill per poder escollir el dia. Tenim diferents opcions de calendari per Ionic i Angular. Si no necessitem res molt especial, el millor sol ser anar per una opció que estigui actualitzada, ben documentada i, si pot ser, amb una demostració que permeti veure bé la seva funcionalitat.

Afegim el mòdul de calendari ion2-calendar, que sembla complir tot l'anterior, al projecte. Escollim aquest perquè permet obrir una finestra per escollir la data:
 $ npm install ion2-calendar moment --save

Seguint la documentació, afegim el mòdul a app.module.ts:
 ...
 import { CalendarModule } from "ion2-calendar";
 ...
  imports: [
    BrowserModule,
    HttpClientModule,
    CalendarModule,
    IonicModule.forRoot(MyApp)
  ],
 ...
Ara implementem la funcionalitat desitjada en la pàgina home.ts:
import { Component } from '@angular/core';
import { ModalController } from 'ionic-angular';

import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Geolocation } from '@ionic-native/geolocation';
import { CalendarModal, 
         CalendarModalOptions, 
         CalendarResult 
       } from "ion2-calendar";
import * as moment from 'moment';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  API: string = "https://api.sunrise-sunset.org/json";
  
  // Estany de Sant Maurici
  lat: number = 42.582104;
  lon: number = 1.0008419;

  sunrise : string;
  sunset : string;

  date : Date = new Date();
  dateMsg: string = "Avui";
  posMsg : string;
  posError : boolean = false;

  constructor(  public http: HttpClient,
                private geolocation: Geolocation,
                public modalCtrl: ModalController
  )
  {
    moment.locale('ca-ES');
  }

  ionViewDidLoad() {
    this.geolocation.getCurrentPosition().then((answer) => {
      this.lat = answer.coords.latitude;
      this.lon = answer.coords.longitude;
      this.posMsg = "Posició actual";
      this.getSunriseSunsetFromApi();

    }).catch((error) => {
       console.log('Error getting location', error);
       this.posMsg = "Per defecte";
       this.posError = true;
       this.getSunriseSunsetFromApi();
     });
  }

  selectDate() {
    const options: CalendarModalOptions = {
      title: 'Escull la data',
      canBackwardsSelected: true,
      closeLabel: "Cancel·lar",
      doneLabel: "Fet",
      weekdays: moment.weekdaysShort(),
      weekStart: 1,
    };
    let myCalendar =  this.modalCtrl.create(CalendarModal, {
      options: options
    });

    myCalendar.present();

    myCalendar.onDidDismiss((date: CalendarResult, type: string) => {
      if(date) {
        console.log(date);
        this.date = date.dateObj;
        this.dateMsg = this.date.toLocaleDateString("ca-ES");
        this.getSunriseSunsetFromApi();
      }
    });
  }

  getSunriseSunsetFromApi() {
    let data = {  lat: this.lat.toString(), 
                  lng: this.lon.toString(),
                  formatted: "0",
                  date: this.date.toISOString().slice(0,10)
                };
    console.log("Calling API with date= "+data.date);

    const params = new HttpParams({fromObject: data});
    const headers = new HttpHeaders().set("Accept", "application/json");
    let options = {headers: headers, params: params, withCredentials: false};

    this.http.get(this.API, options).subscribe(answer => {
      if (answer['status']=="OK") {
        let date_options = {hour: "2-digit", minute: "2-digit"};

        this.sunrise = new Date(answer['results'].sunrise)
                                .toLocaleTimeString("ca-ES", date_options);
        this.sunset = new Date(answer['results'].sunset)
                                .toLocaleTimeString("ca-ES", date_options);
      }
    },
    err => console.log(err)
    );    
  }
}

Observem les modificacions que hi hem fet:
  • [6-10] Importem les classes necessàries pel calendari.
  • [26-27] Declarem noves variables per la data escollida.
  • [33] Afegim el controlador Modal de Ionic per poder obrir el calendari en una finestra.
  • [36] Indiquem el llenguatge a utilitzar per a mostrar dates en el calendari.
  • [54-77] Declarem una nova funció selectDate, que ens permet obrir el calendari per escollir la data i assignar-la a les variables que hem declarat. Aquesta funció es cridarà a partir de la interfície d'usuari, com veurem en el codi de home.html. Els paràmetres de configuració del calendari els hem obtingut de la documentació del mòdul. A l'exemple de codi que es presenta en la documentació simplement hi hem afegit l'assignació a les variables i la crida a l'API.
  • [83] S'ha afegit un nou paràmetre a l'hora de cridar l'API per tal d'utilitzar la data seleccionada enlloc de l'actual.

Les modificacions fetes a home.tml són poques:
 ...
    <ion-item-divider color="light">Data</ion-item-divider>
    <ion-item (click)="selectDate()">
      <ion-icon name="search" item-left color="primary"></ion-icon>
      <ion-note item-right>{{ dateMsg }}</ion-note>
    </ion-item>
 ...
Hi hem afegit una icona i hem canviat el missatge fixe pel contingut de la variable dateMsg, però la part més interessant és la captura de l'event click en el <ion-item>. Utilitzem les opcions per capturar les interaccions de l'usuari que ens proporciona Angular. En aquest cas, el que es fa és cridar la funció selectDate del nostre controlador (la que obre el calendari i crida l'API amb la nova data seleccionada).

Podem provar el codi en el navegador, com sempre amb:
 $ ionic serve -c -s
En fer clic en la fila de la data, s'obre una finestra amb el calendari:

 Podem veure en la consola l'objecte que rep el calendari. Tot seguit, en escollir una nova data i tancar la finestra, es crida l'API i la pantalla s'actualitza amb la nova informació:


Observem com s'ha actualitzat la data i les hores de sortida/posta del sol, i també podem comprovar en la consola com s'ha cridat l'API amb la data que s'havia escollit.

Com sempre, teniu tot el codi a la vostre disposició en el Github: v0.3 (amb calendari).

En la propera modificació, podríem permetre canviar el lloc, enlloc d'utilitzar la posició actual.

Seguiu l'evolució de l'aplicació:

27 de febrer del 2018

Ionic: aplicació per saber l'hora de sortida/posta del sol (III)

Part III: Versió amb Geolocalització

Aquest article ens mostrarà com afegir geolocalització a l'aplicació per saber l'hora de sortida i posta de sol justament del lloc on ens trobem.

Fins ara hem vist:

Ara afegirem geolocalització per obtenir la posició actual del mòbil. La precisió variarà depenent de si està el GPS actiu, o si el posicionament és per les antenes de ràdio, wifi, etc... Però pel que necessitem nosaltres, serà més que suficient.

Afegim el mòdul de Geolocalització del Ionic al projecte:
$ ionic cordova plugin add cordova-plugin-geolocation \\
        --variable GEOLOCATION_USAGE_DESCRIPTION="Per posicionar-te"
$ npm install --save @ionic-native/geolocation
Ja veieu que es tracta d'un mòdul que requereix Cordova. Molts d'aquest mòduls només estan disponibles en un dispositiu físic (mòbil o tablet) i no en el navegador d'un PC. És important comprovar sempre la compatibilitat, per no tenir sorpreses.

En el cas que ens ocupa, Browser apareix llistat com a compatible, pel que sí podem utilitzar-lo en el navegador d'un PC (això sí, ens demanarà permís abans de permetre la localització).

Comprovem la documentació del propi mòdul per veure com utilitzar-lo:


Aquest codi l'afegirem a la pàgina home.ts. Però no ens hem d'oblidar d'afegir també el proveïdor Geolocation en el fitxer app.module.ts. Ens cas contrari, generaria un error de l'estil:


Ja que hi estem posats, organitzarem el codi separant la crida a l'API en una funció separada. Ens anirà bé, ja que la cridarem en un parell de situacions diferents.

També aprofitarem per generar un missatge indicant si la posició en la que es calcula és l'actual o la que hi ha per defecte (en cas de no poder usar la geolocalització). En aquest cas, donarem també un avís en la pantalla, en forma d'error o advertiment.

Bé, el codi quedaria com segueix:
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Geolocation } from '@ionic-native/geolocation';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  API: string = "https://api.sunrise-sunset.org/json";
  
  // Estany de Sant Maurici
  lat: number = 42.582104;
  lon: number = 1.0008419;

  sunrise : string;
  sunset : string;

  posMsg : string;
  posError : boolean = false;

  constructor(  public navCtrl: NavController,
                public http: HttpClient,
                private geolocation: Geolocation
  )
  {  }

  ionViewDidLoad() {
    this.geolocation.getCurrentPosition().then((answer) => {
      //console.log(answer);
      this.lat = answer.coords.latitude;
      this.lon = answer.coords.longitude;
      this.posMsg = "Posició actual";
      this.getSunriseSunsetFromApi();

    }).catch((error) => {
       console.log('Error getting location', error);
       this.posMsg = "Per defecte";
       this.posError = true;
       this.getSunriseSunsetFromApi();
     });
  }

  getSunriseSunsetFromApi() {
    console.log("Calling API...");

    let data = {  lat: this.lat.toString(), 
                  lng: this.lon.toString(),
                  formatted: "0"
                };

    const params = new HttpParams({fromObject: data});
    const headers = new HttpHeaders().set("Accept", "application/json");
    let options = {headers: headers, params: params, withCredentials: false};

    this.http.get(this.API, options).subscribe(answer => {
      console.log(answer);

      if (answer['status']=="OK") {
        let date_options = {hour: "2-digit", minute: "2-digit"};

        this.sunrise = new Date(answer['results'].sunrise)
                            .toLocaleTimeString("ca-ES", date_options);
        this.sunset = new Date(answer['results'].sunset)
                            .toLocaleTimeString("ca-ES", date_options);

        console.log(this.sunrise);
        console.log(this.sunset);
      }
    },
    err => console.log(err)
    );    
  }
}

Observem els canvis:
  • [5] Importem el nou mòdul de geolocalització.
  • [21-22] Declarem les noves variables pel missatge del lloc i el possible error.
  • [26] Afegim el nou proveïdor de geolocalització.
  • [30-44] Ara, en carregar la pàgina provem d'obtenir la posició actual tal com en indica la documentació del mòdul. Si tot és correcte, obtenim la posició; en cas contrari, utilitzem la que tenim per defecte. En tot cas, actualitzem un missatge per l'usuari i una senyal d'error.
  • [46-75] La crida a l'API que calcula la sortida/posta de sol l'hem separat en una funció pròpia. A part d'això, no hi hem fet canvis.

També cal modificar el fitxer home.html per mostrar el nou missatge i el possible error:
 ...
<ion-content padding>
  <ion-list>
    <ion-item-divider color="light">Lloc: {{ posMsg }}</ion-item-divider>
    <ion-item color="light" *ngIf="posError">
      <h2>No s'ha pogut obtenir la posició actual</h2>
      <p>Verifica els permisos</p>
      <ion-icon name="warning" color="danger" item-left></ion-icon>
    </ion-item>
    ...

Només hi hem afegit un ion-item per mostrar un error en cas que no es pugui utilitzar la geolocalització. Fixem-nos en l'ús de l'operador *ngIf d'Angular. Amb ell, només es mostrarà l'element (i tot el seu contingut) si es compleix la condició indicada (en aquest cas, si la variable posError s'avalua com a certa [línia 41, quan salta un error en provar de geolocalitzar]).

El resultat, quan salta l'error, es pot veure a continuació:

Si mirem els missatges en la consola, podem veure que l'error que es produeix és un 403 (intent d'accedir a un recurs sense permisos adients).

En canvi, si li donem els permisos adequats, el resultat que obtenim és:

Noteu com el codi corresponent a l'error ja no es mostra ! En canvi, en la consola podem veure l'objecte que ens retorna el mòdul de geolocalització.

I d'aquesta manera tant senzilla hem aconseguit geolocalitzar l'aplicació. Com sempre, teniu tot el codi a la vostre disposició en el Github: v0.2 (amb geolocalització).

En la propera modificació, podríem afegir un calendari per escollir la data que volem, enlloc d'utilitzar l'actual.

Seguiu l'evolució de l'aplicació:

24 de febrer del 2018

Ionic: aplicació per saber l'hora de sortida/posta del sol (II)

Part II: Versió "clavada a ferro"

Aquest article ens mostrarà com fer una aplicació amb Ionic per saber l'hora de sortida i posta de sol en un lloc determinat.

En la primera part vàrem preparar el projecte inicial.

Ara farem una primera versió senzilla, on fixarem el lloc i el dia. Més endavant anirem modificant el codi per fer-lo més útil en general (i de passada anirem veien diferents tècniques).

En lloc de fer el càlcul de l'hora de sortida/posta de sol directament, l'aplicació es connectarà a internet per a obtenir-la. Amb una cerca ràpida a Google podem trobar una API que sembla feta a mida a https://sunrise-sunset.org/api.

En una primera versió, podem fer una petició simple amb:
 https://api.sunrise-sunset.org/json?
             lat=36.7201600&lng=-4.4203400&formatted=0

I obtindrem una resposta en JSON similar a:


Fixeu-vos que només necessitem la latitud i la longitud i fer una petició a l'API anterior per obtenir les dades que necessitem en la nostre aplicació.

En una aplicació Ionic, per poder fer peticions web, primer hem d'importar el mòdul HttpClientModule al fitxer app.module.ts. Quedaria com segueix:
 ...
 import { SplashScreen } from '@ionic-native/splash-screen';

 import { HttpClientModule } from '@angular/common/http';

 @NgModule({
  declarations: [
   ...
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  ...

Amb això ja podem implementar la funcionalitat desitjada en la pàgina home.ts:
 import { Component } from '@angular/core';
 import { NavController } from 'ionic-angular';

 import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';

 @Component({
  selector: 'page-home',
  templateUrl: 'home.html'
 })
 export class HomePage {
  API: string = "https://api.sunrise-sunset.org/json";
  
  // Estany de Sant Maurici
  lat: number = 42.582104;
  lon: number = 1.0008419;

  sunrise : string;
  sunset : string;

  constructor(  public navCtrl: NavController,
                public http: HttpClient )
  { }

  ionViewDidLoad() {
    console.log("Calling API...");

    let data = {  lat: this.lat.toString(), 
                  lng: this.lon.toString(),
                  formatted: "0"
                };

    const params = new HttpParams({fromObject: data});
    const headers = new HttpHeaders().set("Accept", "application/json");
    let options = { headers: headers, 
                    params: params, 
                    withCredentials: false
                  };

    this.http.get(this.API, options).subscribe(answer => {
      console.log(answer);

      if (answer['status']=="OK") {
        let date_options = {hour: "2-digit", minute: "2-digit"};

        this.sunrise = new Date(answer['results'].sunrise)
                               .toLocaleTimeString("ca-ES", date_options);
        this.sunset = new Date(answer['results'].sunset)
                               .toLocaleTimeString("ca-ES", date_options);

        console.log(this.sunrise);
        console.log(this.sunset);
      }
    },
    err => console.log(err)
    );
  }
 }

Observem l'estructura:
  • [4] Hem importat les classes que necessitarem.
  • [11-18] Declarem les variables internes a la classe, amb el seu tipus (gràcies, Typescript !) i opcionalment amb un valor per defecte. Aquí fixarem el valor de la latitud i la longitud del lloc que ens interessi a partir de les dades GPS (que podem obtenir a través de google maps, per exemple).
  • [21] Afegim el proveïdor per a les connexions a internet en el constructor.
  • [24] ionViewDidLoad és una funció que es crida una sola vegada, i és el lloc ideal per posar-hi codi d'inicialització. En aquest cas, com que les nostres dades no varien gaire sovint, les podem inicialitzar aquí. Val la pena consultar els cicles de vida d'una pàgina a Ionic a NavController.
  • [27-30] Preparem les dades en el format requerit per l'API. Necessitem convertir els números en strings.
  • [32-37] Generem els paràmetre i capçaleres de la nostre petició a l'API, i finalment ho posem tot plegat en un objecte options.
  • [39-55] Fem la petició i quan obtenim la resposta, extraiem les dades que volem mostrar. En cas d'error, traiem un missatge per la consola.
Val la pena comentar que la petició web retornarà les dades al cap d'un temps indeterminat. Per això, el valor es retorna a través d'un Observable, al qual ens subscrivim per rebre el seu valor quan estigui disponible.

Fixem-nos com el fitxer home.ts actua com a controlador de les dades de la classe. Per la seva banda, la visualització d'aquestes es fa en el fitxer home.html:
<ion-header>
  <ion-navbar>
    <ion-title>Sortida/posta de Sol</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-list>
    <ion-item-divider color="light">Lloc</ion-item-divider>
    <ion-item>
      <ion-label>Latitud</ion-label>
      <ion-note item-right>{{ lat }}</ion-note>
    </ion-item>
    <ion-item>
        <ion-label>Longitud</ion-label>
        <ion-note item-right>{{ lon }}</ion-note>
      </ion-item>
  
    <ion-item-divider color="light">Data</ion-item-divider>
    <ion-item>
      <ion-note item-right>Avui</ion-note>
    </ion-item>

    <ion-item-divider color="light">Horari</ion-item-divider>
    <ion-item>
      <ion-icon name="sunny" color="orange" item-left></ion-icon>
      <ion-note item-right>{{ sunrise }}</ion-note>
    </ion-item>
    <ion-item>
      <ion-icon name="moon" color="blue" item-left></ion-icon>
      <ion-note item-right>{{ sunset }}</ion-note>
    </ion-item>

  </ion-list>
</ion-content>

L'estructura utilitza tags tipus html que, de fet, són components pre-definits de Ionic. Amb això podem construir una interfície d'usuari agradable sense molt d'esforç: amb llistes, icones, etiquetes, etc...

El més destacable és com podem utilitzar en el fitxer html les variables que tenim definides en el nostre controlador, gràcies als operadors d'enllaç de dades d'Angular. En la nostre aplicació només utilitzem l'operador d'interpolació, {{ var }}, que serà substituït en la pàgina de sortida pel valor que tingui la variable en la classe en cada moment.

Bé, ha arribat el moment de veure el resultat:
 $ ionic serve -c -s

El resultat ens apareixerà en una finestra nova al navegador:


Observem com gràcies a les "Eines de desenvolupador" podem veure els missatges que surten per la consola. En el cas d'un objecte com el que obtenim de la petició a l'API [línia 40], el podem expandir i veure'n tots els detalls.

Però anem a veure com es genera l'aplicació mòbil. De fet, si tenim els SDK d'Android i el XCode ja instal·lats i configurats, és tan senzill com:
 $ ionic cordova build android
 $ ionic cordova build ios

El resultat es pot veure a continuació:


A l'esquerra hi ha una captura de pantalla directe des d'un Samsung S6, i a la dreta el resultat en l'emulador d'un iPhone 7.

Tal com es pot observar, Ionic ens permet obtenir una aplicació que corre en els dos principals sistemes operatius mòbils (i en la web, tot sigui dit de passada) a partir d'un mateix codi.

I parlant de codi, si us animeu a provar l'aplicació, la teniu disponible al GitHub: v0.1 (clavada a ferro). Proveu-la, i ja em comentareu.

Més endavant anirem modificant l'aplicació per permetre variar el lloc i la data, la qual cosa ens permetrà veure noves tècniques de programació, ús del GPS en els dispositius mòbils, etc...
Seguiu l'evolució de l'aplicació:

28 de febrer del 2015

MySQL: Actualitzar una taula a partir d'una selecció sobre la mateixa taula

Treballant amb bases de dades tot sovint ens trobem que cal fer una actualització massiva de dades en una taula. Si la cosa és senzilla, es pot fer amb:
UPDATE LaMevaTaula
SET ElMeuCamp=NouValor
WHERE condició
Evidentment, podem modificar diversos camps, i la condició pot ser més o menys senzilla.

Però, de vegades, la condició pot ser realment complexe i difícil de formular si inclou la mateixa taula i relacions complexes amb d'altes taules. En aquest cas, convé separar la selecció de l'actualització. Això ho podem fer amb dues comandes MySQL, però serà més eficient si ho fem en una de sola:
UPDATE LaMevaTaula
SET ElMeuCamp=NouValor
WHERE id IN 
(
    SELECT T.id FROM (
 SELECT TS.id FROM LaMevaTaula AS TS 
        JOIN unió_complexe_amb_altres_taules
    ) AS T
)
Fixeu-vos com generem els identificadors a actualitzar amb la selecció complexe, i després s'utilitzen aquests per filtrar els registres en l'actualització en massa.

28 d’octubre del 2013

Ubuntu Saucy 13.10 i els servidors web virtuals (VirtualHost)

Amb l'actualització a Ubuntu Saucy 13.10 m'he trobat que no em funcionaven els diferents projectes web que tenia en el servidor local.

El problema té fàcil solució. Resulta que Ubuntu ha actualitzat la versió del servidor Apache, tal com es pot veure amb la comanda:
$ apache2 -v
Server version: Apache/2.4.6 (Ubuntu)
Server built:   Aug  9 2013 14:28:56

En resum, simplement hem de fer que el fitxer que conté les declaracions del servidors virtuals acabi en .conf i reiniciar Apache:
$ cd /etc/apache2/sites-available
$ sudo mv vhosts vhosts.conf
$ sudo service apache2 restart

I ja ho tenim.

15 de juny del 2013

HTML: Fixar amplades de columna en una taula

En la programació web hi ha gent molt radical en contra de l'ús de taules i en favor d'altres opcions, com ara els estils CSS. Els estils CSS estan molt bé. Els podeu veure aplicats en aquest mateix bloc ;-)

Però les taules també tenen la seva utilitat. Si encara en feu servir, us podeu trobar que de vegades fixar la mida de les columnes pot ser problemàtic, sobretot si es barreja amb estils CSS...

Un truc per fer-ho senzill consisteix en definir una primera fila buida que ens serveixi només per establir les amplades de les columnes de la taula:
<table style="width: 100%;">
    <tr>
        <td width="60%"></td>
        <td width="40%"></td>
    </tr>
    <tr>
        <td>Primera columna</td>
        <td>Segona columna</td>
    </tr>
    <tr >
        <td>Contingut</td>
        <td>Més contingut</td>
    </tr>
</table>

El resultat (amb un xic de CSS) el podeu veure a continuació:

Primera columna Segona columna
Contingut Més contingut

No és una mala opció, veritat?

7 de juny del 2013

Subversion: afegir fitxers ignorats globalment

El Subversion és un sistema de control de versions molt utilitzat en programació, tot i que recentment el git li va traient protagonisme.

Sigui com sigui, fa uns dies em vaig trobar que m'estava ignorant unes llibreries, cosa que en general ja està bé, però que vaig trobar a faltar força en provar de compilar en un altre ordinador :-(

Per tal que no torni a passar, i sempre que vulgueu saber quins fitxers s'estan ignorant pel Subversion, us pot ser d'utilitat la comanda:
$ svn status --no-ignore | grep "I  "

L'executem en l'arrel del directori que volem verificar i ens llista els fitxers que s'estan ignorant en aquell directori. Si n'hi ha algun que hauria d'estar sota el control de versions, el podem afegir manualment amb:
$ svn add nom_del_fitxer
A veure si no se m'oblida ;-)

1 de maig del 2013

RabbitVCS a Ubuntu Raring 13.04

RabbitVCS és un gestor gràfic de control de versions. S'utilitza especialment en programació, per mantenir un control fàcil dels canvis que es fan en el codi per part d'un grup de desenvolupadors.

El problema és que les dues darreres versions d'Ubuntu (des de 12.10 Quantal) trenquen la seva integració amb el navegador de fitxers Nautilus: es perden els menús contextuals (i les icones).

Per recuperar el seu funcionament normal a Ubuntu 13.04 Raring, que ha sortit just la setmana passada, cal fer el següent:
  1. Descarregar el fitxer RabbitVCS.py actualitzat a la darrera versió.
  2. Copiar-lo al seu lloc, substituint el que hi hagi actualment instal·lat al sistema.
  3. Corregir la referència a una llibreria python, creant un enllaç simbòlic.
  4. Tancar el navegador de fitxers Nautilus.
Tot això ho podem fer des del terminal amb:
$ sudo cp RabbitVCS.py /usr/share/nautilus-python/extensions/
$ sudo ln -s /usr/lib/x86_64-linux-gnu/libpython2.7.so.1.0 \
             /usr/lib/libpython2.7.so.1.0
$ nautilus -q

Ja només falta obrir de nou el Nautilus des del Dash per comprovar que tornem a tenir les opcions del RabbitVCS.


.

I ja no tindrem excusa per no tornar a la feina!

5 de desembre del 2012

PHP Warning: json_encode(): Invalid UTF-8 sequence in argument [SOLUCIONAT]

Si mai us trobeu que en accedir des de PHP a una base de dades MySQL i codificar el resultat en JSON per retornar-lo us surt un error tipus "PHP Warning:  json_encode(): Invalid UTF-8 sequence in argument", està clar que teniu un problema. Possiblement algunes de les dades que esteu retornant estan buides, són invàlides o contenen caràcters estranys. Per sort, la solució és senzilla.

En primer lloc, comproveu que la base de dades utilitza codificació UTF-8 (p.e. amb phpMyAdmin):


Llavors, en accedir a la base de dades des de PHP afegiu l'opció "SET NAMES utf8" a l'atribut MYSQL_ATTR_INIT_COMMAND. Per exemple, utilitzant PDO podria ser similar a:
$DBH = new PDO("mysql:host=$server;dbname=$dbname",
            $user, $pass, 
            array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
I ja veureu com l'error desapareix dels vostres logs, i les dades s'obtenen correctament.

3 d’agost del 2012

Diferents projectes web en el servidor local

Hi ha diferents motius pels quals podem voler tenir diferents projectes web en el nostre ordinador, que actuarà com a servidor en la xarxa local: per desenvolupar projectes per diferents clients, per poder provar diferents gestors de continguts (Drupal, Joomla, etc...), o simplement per tenir un conjunt de servidors de continguts (fotos, música, etc...) sense que s'interfereixin.

Sigui com sigui, anem a veure com ho podem configurar de forma senzilla en el nostre Ubuntu Precise. En primer lloc, instal·lem el servidor web Apache i el PHP:
$ sudo apt-get install apache2 libapache2-mod-php5
En el meu cas, tenia una instal·lació prèvia que m'ha donat problemes amb el PHP (no executava els codis). M'ha calgut reinstal·lar amb:
$ sudo apt-get purge libapache2-mod-php5
$ sudo apt-get install libapache2-mod-php5
$ sudo a2enmod php5
$ sudo /etc/init.d/apache2 restart
Ara, caldrà definir els servidors virtuals que volem utilitzar:
$ gksudo gedit /etc/apache2/sites-available/vhosts
Amb un contingut semblant a (canvieu els noms dels servidors i el que volgueu):
# Use name-based virtual hosting.
#
NameVirtualHost *:80

# "fotos.localhost" VirtualHost:
#
<Directory "/var/www/fotos">
 Options Indexes FollowSymLinks MultiViews
 AllowOverride All
 Order allow,deny
 Allow from all
</Directory>

<VirtualHost *:80>
        ServerAdmin webmaster@fotos.localhost
        DocumentRoot "/var/www/fotos"
        ServerName fotos.localhost
 ErrorLog ${APACHE_LOG_DIR}/fotos_error.log
 CustomLog ${APACHE_LOG_DIR}/fotos_access.log combined
</VirtualHost>

# "web.localhost" VirtualHost:
#
<Directory "/var/www/web">
 Options Indexes FollowSymLinks MultiViews
 AllowOverride All
 Order allow,deny
 Allow from all
</Directory>

<VirtualHost *:80>
        ServerAdmin webmaster@web.localhost
        DocumentRoot "/var/www/web"
        ServerName web.localhost
 ErrorLog ${APACHE_LOG_DIR}/web_error.log
 CustomLog ${APACHE_LOG_DIR}/web_access.log combined
</VirtualHost>
 
I l'activem amb:
$ sudo a2ensite vhosts
$ sudo /etc/init.d/apache2 restart
En principi, amb això ja estaria. Només ens queda afegir els noms dels servidors virtuals al fitxer de noms coneguts:
$ gksudo gedit /etc/hosts
Només caldrà afegir una línia:
...
127.0.0.1 fotos.localhost web.localhost
...
I per provar que tot ha anat bé, afegim un fitxer 'index.php' a cadascún dels nostres projectes. El contingut pot ser un simple 'Hola món' en PHP:
<?php echo 'web.localhost diu: Hola a tothom!';?>
I comprovem que efectivament hi tenim accés i ens respon, anant a l'adreça http://web.localhost:

I ja ho tenim. Dues estructures de directoris separades per poder fer el que necessitem, sense que s'interfereixin. Objectiu assolit.