Sémantique des données ouvertes et croisement avec des données sociales
Comme exposé lors d’un précédent billet, le portail Open Data data.gouv.fr met à disposition plus de 350 000 jeux de données sous la Licence Ouverte de la mission Etalab. Les données ainsi libérées sont de natures très diverses et rendent donc possible un grand nombre de scénarios en faisant preuve d’imagination.
On retrouve ainsi des données liées à la Culture et plus particulièrement les lieux de culture en France comme les centres d’arts, les centres dramatiques, les centres chorégraphiques, les opéras, les orchestres permanents ou encore les scènes nationales.
A partir de ces ensembles de données proposés au travers de différents fichiers mais disposant de propriétés communes (nom, adresse, code postal, ville, latitude et longitude). Nous allons y revenir. On pourrait imaginer sur cette base une application proposant, au contraire, une vue unifiée de toutes ces données comme, par exemple, une application de cartographie. Pour enrichir une telle application, pourquoi ne pas croiser ces données avec d’autres sources de données comme des données issues des réseaux sociaux.
Flickr, un réseau social de partage de photos offrant une API qui permet, entre autres, de chercher des photos selon leurs informations de géolocalisation, semble un candidat idéal pour cela.
Au cours de ce billet, nous nous intéressons au cheminement qui nous a amené à exposer l’application exemple Cultura sous sa forme courante. Nous débutons par la réflexion menée sur la sémantique commune des jeux de données originaux, puis nous poursuivons sur le choix de croiser ces données ouvertes publiques avec des données sociales afin d’enrichir l’application et d’apporter une réelle valeur ajoutée pour l’utilisateur et enfin, nous concluons sur les perspectives d’évolutions d’un paradigme croisant les données ouvertes, des données sociales voire même d’autres types de données.
Cette application a été développée en Silverlight 5 et est disponible en libre consultation à l’adresse https://culturafrance.cloudapp.net/. Comme à l’accoutumée, le code source est fourni à la fin de ce billet.
Réflexions sur la sémantique des données ouvertes
Les (ensembles de) données que l’application exemple Cultura consomme proviennent de fichiers XLS publiés sous la Licence Ouverte et issus du portail data.gouv.fr. Ces mêmes données ont ensuite été publiées l’instance de test OGDI France de façon à la consommer via un service de données RESTful.
Une rapide analyse de ces fichiers met en exergue des propriétés communes très intéressantes pour le scénario mis en avant (ou plutôt, disons que c’est le fait que tous ces ensembles de données aient des propriétés communes qui a permis l’émergence du scénario mis en avant ici).
Ainsi, comme cela a été souligné précédemment, tous ces ensembles de données (scènes nationales, opéras, centres dramatiques, centres chorégraphiques, opéras et orchestres permanents) disposent pour les entités qui les composent des mêmes propriétés (nom, adresse, code postal, ville, latitude et longitude). Partant de ce constat, nous avons décidé de factoriser chacun de ces fichiers aux propriétés qui nous intéressaient avant de les télécharger sur l’instance OGDI France.
C’est en grande partie de la qualité des données que dépend la qualité des applications qui vont les consommer. Imaginons un instant que chaque département ou collectivité de France s’entende sur une façon de standardiser les structures de données ayant un domaine fonctionnel commun : les subventions apportées aux associations, les finances publiques, etc. Le « simple » fait que les structures de données soient les mêmes en faciliterait l’exploitation et les analyses, mettant côte à côte les ensembles de données de chaque collectivité ; cela conduirait à fournir des résultats très pertinents sans que les analystes aient besoin de réaliser un travail d’intégration et de traitement propre à chacun des flux de données avant de pouvoir mener une analyse. Ils pourraient ainsi se concentrer sur le cœur de leur travail, produire les résultats attendus beaucoup plus rapidement et à moindre coût.
Les développeurs d’applications pourraient bénéficier des mêmes effets. C’est ce volet que nous développons ici par la suite. La création d’applications et de services couvrant tout le territoire en intégrant des sources de données locales serait dans une toute autre dynamique qu’elle ne l’est actuellement. Les coûts de développement seraient ainsi bien moindres du fait de leur factorisation.
Le fait de standardiser la sémantique de jeux de données ayant le même domaine fonctionnel permettrait donc une réelle capitalisation à terme aussi bien pour les consommateurs des données qui pourraient ainsi se concentrer sur le cœur de leur métier et donc par conséquent sur la valeur ajoutée qu’ils apportent que pour les producteurs de données qui en retour disposeraient d’outils d’analyses, d’évaluation et de positionnement sur le marché bien plus pertinents que ce dont ils peuvent bénéficier aujourd’hui.
Conséquences directes sur le développement et le code
Revenons à l’application Cultura. Cette sémantique commune a permis de réaliser un modèle de données simplifié au maximum ou chaque type d’élément implémente une interface commune. Ainsi, on peut créer une source de données très simple sur laquelle la vue de notre application peut venir se lier.
Comme vous pouvez le constater, le code de la classe DataSource sur laquelle la vue vient se lier est donc très compact malgré le fait qu’il représente le point d’exposition de 6 jeux de données :
public class DataSource
{
public ObservableCollection<PushpinModel> CentresChoregraphiques {get; set;}
public ObservableCollection<PushpinModel> CentresDart { get; set; }
public ObservableCollection<PushpinModel> CentresDramatiques { get; set; }
public ObservableCollection<PushpinModel> Operas { get; set; }
public ObservableCollection<PushpinModel> OrchestresPermanents { get; set; }
public ObservableCollection<PushpinModel> ScenesNationales { get; set; }
public DataSource()
{
CentresChoregraphiques = new ObservableCollection<PushpinModel>();
CentresDart = new ObservableCollection<PushpinModel>();
CentresDramatiques = new ObservableCollection<PushpinModel>();
Operas = new ObservableCollection<PushpinModel>();
OrchestresPermanents = new ObservableCollection<PushpinModel>();
ScenesNationales = new ObservableCollection<PushpinModel>();
var CentresChroregraphiquesCollection =
new DataServiceCollection<centreschoregraphiquesItem>();
var CentresDartCollection =
new DataServiceCollection<centresdartItem>();
var CentresDramatiquesCollection =
new DataServiceCollection<centresdramatiquesItem>();
var OperasCollection =
new DataServiceCollection<operasItem>();
var OrchestresPermanentsCollection =
new DataServiceCollection<orchestrespermanentsItem>();
var ScenesNationalesCollection =
new DataServiceCollection<scenesnationalesItem>();
frOpenDataDataService proxy = new frOpenDataDataService(
new Uri("https://ogdifrancedataservice.cloudapp.net/v1/frOpenData"));
DataServiceQuery<centreschoregraphiquesItem> centresChroregraphiquesQuery =
proxy.centreschoregraphiques;
DataServiceQuery<centresdartItem> centresdartQuery =
proxy.centresdart;
DataServiceQuery<centresdramatiquesItem> centresdramatiquesQuery =
proxy.centresdramatiques;
DataServiceQuery<operasItem> operasQuery =
proxy.operas;
DataServiceQuery<orchestrespermanentsItem> orchestresPermanentsQuery =
proxy.orchestrespermanents;
DataServiceQuery<scenesnationalesItem> scenesNationalesQuery =
proxy.scenesnationales;
CentresChroregraphiquesCollection.LoadCompleted += (s, e) =>
{
PopulateItems(CentresChroregraphiquesCollection as IEnumerable<IInterestPoint>,
CentresChoregraphiques);
};
CentresDartCollection.LoadCompleted += (s, e) =>
{
PopulateItems(CentresDartCollection as IEnumerable<IInterestPoint>,
CentresDart);
};
CentresDramatiquesCollection.LoadCompleted += (s, e) =>
{
PopulateItems(CentresDramatiquesCollection as IEnumerable<IInterestPoint>,
CentresDramatiques);
};
OperasCollection.LoadCompleted += (s, e) =>
{
PopulateItems(OperasCollection as IEnumerable<IInterestPoint>,
Operas);
};
OrchestresPermanentsCollection.LoadCompleted += (s, e) =>
{
PopulateItems(OrchestresPermanentsCollection as IEnumerable<IInterestPoint>,
OrchestresPermanents);
};
ScenesNationalesCollection.LoadCompleted += (s, e) =>
{
PopulateItems(ScenesNationalesCollection as IEnumerable<IInterestPoint>,
ScenesNationales);
};
CentresChroregraphiquesCollection.LoadAsync(centresChroregraphiquesQuery);
CentresDartCollection.LoadAsync(centresdartQuery);
CentresDramatiquesCollection.LoadAsync(centresdramatiquesQuery);
OperasCollection.LoadAsync(operasQuery);
OrchestresPermanentsCollection.LoadAsync(orchestresPermanentsQuery);
ScenesNationalesCollection.LoadAsync(scenesNationalesQuery);
}
private void PopulateItems(IEnumerable<IInterestPoint> collection,
ObservableCollection<PushpinModel> pushpins)
{
int numero = 1;
foreach (IInterestPoint item in collection)
{
PushpinModel model = new PushpinModel
{
Adresse = item.adresse,
Location = new Location(item.latitude.Value, item.longitude.Value),
Nom = item.nom,
Numero = numero++,
Ville = item.ville
};
pushpins.Add(model);
}
}
}
Ainsi, au niveau de notre vue en XAML il suffit de définir dans les ressources de la page notre modèle de données (ainsi que différents templates visuels mais c’est hors-propos ici) et de lier les différents niveaux de cartes aux propriétés correspondantes du modèle de données :
<UserControl.Resources>
<SolidColorBrush x:Key="AccentColorBrush" Color="#119EDA" />
<models:DataSource x:Key="DataSource" />
<Style x:Key="PopUpPushpinStyle" TargetType="map:Pushpin">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ContentPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="map:MapItemsControl">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<map:Pushpin Location="{Binding Location}"
PositionOrigin="BottomLeft"
Height="40" Width="20"
MouseLeftButtonDown="Pushpin_MouseLeftButtonDown">
<map:Pushpin.Content>
<StackPanel>
<Canvas Height="40" Width="20" >
<Polygon Points="0,0 20,0 20,20 0,20"
Fill="{StaticResource AccentColorBrush}" />
<Polygon Points="0,10 20,20 0,40"
Fill="{StaticResource AccentColorBrush}" />
<TextBlock Text="{Binding Numero}" FontWeight="Bold"
TextAlignment="Center" Width="20"/>
</Canvas>
</StackPanel>
</map:Pushpin.Content>
<map:Pushpin.Template>
<ControlTemplate>
<ContentPresenter/>
</ControlTemplate>
</map:Pushpin.Template>
</map:Pushpin>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid x:Name="LayoutRoot"
DataContext="{StaticResource DataSource}"
Style='{StaticResource LayoutRootGridStyle}'>
…
<map:Map Name="Map" CredentialsProvider="XXXXXXXXXX"
Center="46.7695007324219, 2,43255996704102" ZoomLevel="6">
<map:MapItemsControl Tag="CentresDart" Opacity="0"
Visibility="Collapsed"
ItemsSource="{Binding CentresDart}" />
<map:MapItemsControl Tag="Comedies" Opacity="0"
Visibility="Collapsed"
ItemsSource="{Binding CentresDramatiques}" />
<map:MapItemsControl Tag="Danse" Opacity="0"
Visibility="Collapsed"
ItemsSource="{Binding CentresChoregraphiques}" />
<map:MapItemsControl Tag="Operas" Opacity="0"
Visibility="Collapsed"
ItemsSource="{Binding Operas}" />
<map:MapItemsControl Tag="Orchestres" Opacity="0"
Visibility="Collapsed"
ItemsSource="{Binding OrchestresPermanents}" />
<map:MapItemsControl Tag="ScenesNationales" Opacity="0"
Visibility="Collapsed"
ItemsSource="{Binding ScenesNationales}" />
<map:MapLayer Name="MapLayer" Tag="MapLayer"></map:MapLayer>
<map:MapLayer Name="PhotoLayer" Tag="PhotoLayer"></map:MapLayer>
</map:Map>
…
</Grid>
Bien sûr l’identifiant CredentialsProvider pour se connecter à l’API Bing Cartes n’est pas fourni, mais vous pouvez en obtenir un gratuitement pour vos applications en créant un compte développeur sur https://www.bingmapsportal.com.
Croisement de données ouvertes avec des données sociales
Exposer des données publiques permet d’amorcer des scénarios originaux offrant des services innovants aux citoyens. Cependant pour offrir un service riche, les données publiques mises à disposition ne sont pas toujours suffisantes en tant que telles et il peut s’avérer nécessaire de les croiser avec des sources de données exposées par d’autres acteurs.
Dans le cas présent, et à titre d’illustration, nous avons fait le choix d’afficher sur la carte non seulement les lieux de culture géo-localisés, mais également les photos géo localisées venant de l’API Flickr. Cet ajout apporte une expérience média plus immersive pour l’utilisateur et lui permet aussi de mieux s’imprégner de l’architecture, de la culture et de l’ambiance de la ville qu’il compte visiter.
De plus, l’API Flickr est vivante. En effet, les utilisateurs de ce service de partage de photos ajoutent continuellement des photos et les applications exploitant cette API disposeront ainsi de facto de données de plus en plus nombreuses et de plus en plus riches.
La documentation de l’API de Flickr est disponible en ligne et propose pour chaque méthode, une interface interactive à destination du développer afin qu’il puisse tester ses requêtes.
Intégration de l’API Flickr dans Cultura
L’API Flickr étant une API Web de type REST (Representational State Transfer), le développement d’une interface cliente vers cette API s’est imposé afin de pouvoir exécuter deux opérations :
- Obtenir la liste des photos autour d’un point de coordonnées latitude/longitude dans un rayon donné. C’est l’objet de la méthode GetPhotoListByGeolocalisationAsync;
- Obtenir la latitude et la longitude exacte d’une photo. C’est le rôle affecté à la méthode PopulatePhotoLatitudeAndLongitude.
Sans plus attendre, voici donc le code de cette interface (qui, comme vous l’aurez très certainement remarqué avec la présence des mots clés async et await, fonctionne en mode asynchrone) :
public class FlickrHelper
{
public const string FlickrApiKey = "XXXXXX";
public const string FlickrApiTemplateUrl =
"https://api.flickr.com/services/rest/?method={0}&api_key={1}&{2}&format={3}";
public async Task<List<FlickrPhoto>> GetPhotoListByGeolocalisationAsync(double latitude,
double longitude, double radius, int photoCount)
{
var uri = string.Format(FlickrApiTemplateUrl,
"flickr.photos.search",
FlickrApiKey,
"lat="
+ latitude.ToString(CultureInfo.InvariantCulture)
+ "&lon="
+ longitude.ToString(CultureInfo.InvariantCulture)
+ "&radius="
+ radius.ToString(CultureInfo.InvariantCulture),
"rest");
var request = HttpWebRequest.Create(uri);
List<FlickrPhoto> liste = new List<FlickrPhoto>();
var client = new WebClient();
string data = await client.DownloadStringTaskAsync(new Uri(uri, UriKind.Absolute));
XElement root = XElement.Parse(data);
var photos = root.Descendants("photo").Take(photoCount).ToList();
if (photos.Count > 0)
{
photos.ForEach(p =>
{
FlickrPhoto photo = new FlickrPhoto
{
FarmId = p.Attribute("farm").Value,
Id = p.Attribute("id").Value,
Secret = p.Attribute("secret").Value,
ServerId = p.Attribute("server").Value
};
liste.Add(photo);
});
}
if (liste.Count > 0)
{
return liste;
}
return null;
}
public async Task<FlickrPhoto> PopulatePhotoLatitudeAndLongitude(FlickrPhoto photo)
{
var uri = string.Format(FlickrApiTemplateUrl, "flickr.photos.getInfo",
FlickrApiKey, "photo_id=" + photo.Id, "rest");
var client = new WebClient();
string data = await client.DownloadStringTaskAsync(new Uri(uri, UriKind.Absolute));
XElement root = XElement.Parse(data);
var location = root.Descendants("location").FirstOrDefault();
if (location != null)
{
double latitude = double.Parse(location.Attribute("latitude").Value,
CultureInfo.InvariantCulture);
double longitude = double.Parse(location.Attribute("longitude").Value,
CultureInfo.InvariantCulture);
photo.Latitude = latitude;
photo.Longitude = longitude;
}
return photo;
}
}
Comme vous pouvez le constater, tout cela est très simple d’utilisation, il suffit juste :
- D’exécuter des requêtes http vers l’API Flickr en spécifiant à chaque fois en paramètre de l’URL le nom de l’opération que l’on souhaite exécuter ;
- Et ajouter les paramètres de l’opération à la suite du nom de la méthode, la clé de l’application utilisant l’API (la création d’un compte développeur est gratuite) ainsi qu’un dernier paramètre indiquant le format attendu de la réponse (XML dans le cas présent).
Une fois la réponse obtenue, il suffit d’utiliser LINQ to XML pour créer, à partir du flux XML réceptionné, des objets C# représentant les photos. C’est le rôle dévolu à la classe FlickrPhoto dont voici le code :
public class FlickrPhoto : INotifyPropertyChanged
{
public string Id { get; set; }
public string Secret { get; set; }
public string ServerId { get; set; }
public string FarmId { get; set; }
private double latitude;
private double longitude;
public double Latitude
{
get
{
return latitude;
}
set
{
if (latitude != value)
{
latitude = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Latitude"));
}
}
}
public double Longitude
{
get
{
return longitude;
}
set
{
if (longitude != value)
{
longitude = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Longitude"));
}
}
}
public string Url
{
get
{
return String.Format("https://farm{0}.staticflickr.com/{1}/{2}_{3}.jpg",
FarmId, ServerId, Id, Secret);
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Et enfin, pour afficher les photos venant de Flickr sur la carte à chaque fois qu’un lieu de culture est sélectionné, il suffit :
- De s’abonner à l’évènement MouseLeftButtonDown de chaque punaise générée ; c’est la méthode Pushpin_MouseLeftButtonDown;
- Et d’appeler une méthode qui va instancier notre interface cliente vers l’API Flickr et invoquer les méthodes encapsulant les appels d’API que nous avons spécifié précédemment ; il s’agit de la méthode DisplayPhotoPushpins).
Voici donc le code résultant :
private void Pushpin_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
PushpinModel model = (sender as Pushpin).DataContext as PushpinModel;
…
DisplayPhotoPushpins(model.Location);
}
private async void DisplayPhotoPushpins(Location location)
{
FlickrHelper helper = new FlickrHelper();
var lst = await helper.GetPhotoListByGeolocalisationAsync(
location.Latitude, location.Longitude, 5, 100);
if (lst != null)
{
lst.ForEach(async p =>
{
var photo = await helper.PopulatePhotoLatitudeAndLongitude(p);
System.Windows.Media.Imaging.BitmapImage img =
new System.Windows.Media.Imaging.BitmapImage(new Uri(photo.Url));
Image image = new Image();
image.Source = img;
image.Height = 50;
image.Width = 50;
image.Opacity = 0;
image.Loaded += (s, e) =>
{
Image current = s as Image;
DoubleAnimation anim = new DoubleAnimation();
anim.To = 1;
anim.Duration = new Duration(TimeSpan.FromSeconds(1));
Storyboard board = new Storyboard();
board.Children.Add(anim);
Storyboard.SetTarget(board, current);
Storyboard.SetTargetProperty(anim, new PropertyPath("Opacity"));
board.Begin();
};
image.MouseEnter += (s, e) =>
{
Image current = s as Image;
DoubleAnimation heightAnim = new DoubleAnimation();
heightAnim.To = 300;
heightAnim.Duration = new Duration(TimeSpan.FromSeconds(1.5));
heightAnim.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut };
DoubleAnimation widthAnim = new DoubleAnimation();
widthAnim.To = 300;
widthAnim.Duration = new Duration(TimeSpan.FromSeconds(1.5));
widthAnim.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut };
Storyboard board = new Storyboard();
board.Children.Add(heightAnim);
board.Children.Add(widthAnim);
Storyboard.SetTarget(board, current);
Storyboard.SetTargetProperty(heightAnim, new PropertyPath("Height"));
Storyboard.SetTargetProperty(widthAnim, new PropertyPath("Width"));
board.Begin();
};
image.MouseLeave += (s, e) =>
{
Image current = s as Image;
DoubleAnimation heightAnim = new DoubleAnimation();
heightAnim.To = 50;
heightAnim.Duration = new Duration(TimeSpan.FromSeconds(1));
heightAnim.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut };
DoubleAnimation widthAnim = new DoubleAnimation();
widthAnim.To = 50;
widthAnim.Duration = new Duration(TimeSpan.FromSeconds(1));
widthAnim.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut };
Storyboard board = new Storyboard();
board.Children.Add(heightAnim);
board.Children.Add(widthAnim);
Storyboard.SetTarget(board, current);
Storyboard.SetTargetProperty(heightAnim, new PropertyPath("Height"));
Storyboard.SetTargetProperty(widthAnim, new PropertyPath("Width"));
board.Begin();
};
PhotoLayer.AddChild(image,
new Location(photo.Latitude, photo.Longitude),
PositionOrigin.Center);
});
}
}
En guise de conclusion
Voilà, au cours de ce billet, nous avons vu le processus de développement d’une application croisant des données publiques avec des données sociales ; ce qui représente une ébauche de scénario intéressant et donnant des idées pour l’avenir.
On pourrait imaginer des services et applications croisant :
- A la fois des données publiques avec des données sociales et des données de partenaires locaux ;
- Ou alors les données publiques avec des données de plusieurs réseaux sociaux chacun ayant un facteur différenciant.
A ce propos, Twitter constitue aujourd’hui un service très vivant. On pourrait ainsi se servir des derniers tweets géo-localisés afin d’offrir une application mêlant des données publiques avec des données sociales en temps réel…