diff --git a/debug.log b/debug.log index 6581d38c5f3df8be38e58e0f744286247dbf16ce..26c7cc0afcc00ef8990ea7f41f7343814c7b2a27 100644 --- a/debug.log +++ b/debug.log @@ -1 +1,5 @@ [1019/142259.425:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) +[0702/155529.530:ERROR:directory_reader_win.cc(43)] FindFirstFile: Le chemin d’accès spécifié est introuvable. (0x3) +[0702/173110.201:ERROR:directory_reader_win.cc(43)] FindFirstFile: Le chemin d’accès spécifié est introuvable. (0x3) +[0705/092415.179:ERROR:directory_reader_win.cc(43)] FindFirstFile: Le chemin d’accès spécifié est introuvable. (0x3) +[0705/104154.675:ERROR:directory_reader_win.cc(43)] FindFirstFile: Le chemin d’accès spécifié est introuvable. (0x3) diff --git a/package-lock.json b/package-lock.json index 69adfc8fc5b44b3a9cd4af7575aaa85734d967e3..21e197f2103d3a9d9767e6ac7828592a3db98236 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2883,6 +2883,14 @@ } } }, + "@ngx-lite/json-ld": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@ngx-lite/json-ld/-/json-ld-0.6.2.tgz", + "integrity": "sha512-oFWyYqBPOKQZBqP2ev3Xkn5UjjiWAC1dKYsCduvpDJTA3phc573NhPDpBqxMDfoeIuu72QOJogPehFWJV5991w==", + "requires": { + "tslib": "^1.9.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", diff --git a/package.json b/package.json index 0034b5253b92f89a8e0ef965e1a308167642c1b5..876e7cb46b95510efd19027853e1a21e145f8d6b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@angular/router": "~9.1.11", "@nebular/auth": "^6.2.1", "@nebular/theme": "^5.1.0", + "@ngx-lite/json-ld": "^0.6.2", "file-saver": "^2.0.2", "ng2-cookies": "^1.0.12", "ngx-filesaver": "^9.0.0", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 8ffa3622296cc788340a013035d88209308fac67..a78e3ca466fe9581e68b197859059b49ad96098a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,9 +1,8 @@ import { NgModule } from '@angular/core'; -import { Routes, RouterModule, Route } from '@angular/router'; +import { RouterModule, Route } from '@angular/router'; import { UserComponent } from './user/user.component'; import { RepositoryComponent } from './repository/repository.component'; import { AccessapiComponent } from './accessapi/accessapi.component'; -import { DatasetsComponent } from './datasets/datasets.component'; import { SettingfdpComponent } from './settingfdp/settingfdp.component'; import { SettingsmartapiComponent } from './settingsmartapi/settingsmartapi.component'; import { RepositoryinfoComponent } from './repositoryinfo/repositoryinfo.component'; @@ -12,7 +11,6 @@ import { SearchComponent } from './search/search.component'; import { AuthenticationComponent } from './authentication/authentication.component'; import { SigninComponent } from './authentication/signin/signin.component'; import { SignupComponent } from './authentication/signup/signup.component'; -import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { AuthGuardService } from './authentication/services/auth.guard'; import { StatsComponent } from './stats/stats.component'; @@ -31,10 +29,9 @@ const routes: ICustomRoute[] = [ { path: 'repository', component: RepositoryComponent }, { path: 'repositoryinfo', component: RepositoryinfoComponent }, { path: 'accessapi', component: AccessapiComponent }, - { path: 'datasets', component: DatasetsComponent }, { path: 'stats', component: StatsComponent }, - { path: 'settingfdp', component: SettingfdpComponent }, - { path: 'settingsmartharvester', component: SettingsmartapiComponent }, + /* { path: 'settingfdp', component: SettingfdpComponent }, + { path: 'settingsmartharvester', component: SettingsmartapiComponent },*/ { path: 'publishapi', component: PublishApiComponent }, { path: 'advancedsearch', component: SearchComponent }, ] diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e63425c29f634e934b17a68d2ccac5aad13d7355..71dfe3724981a242b55241ff68023a6449474ab6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -28,18 +28,20 @@ import { AppConfiguration } from './AppConfiguration'; import { PublishApiComponent } from './publishapi/publishapi.component'; import { SearchComponent } from './search/search.component' import { ParseXmlService } from './services/parse-xml.service'; - import { AuthenticationModule } from './authentication/authentication.module'; import { SearchModule} from './search/search.module'; import { StatsComponent } from './stats/stats.component'; +import { MappingComponent } from './mapping/mapping.component'; import { NebularModule } from './nebular.module'; import { AppRoutingModule } from './app-routing.module'; -import { NbLayoutModule, NbThemeModule, NbTooltipModule, NbSpinnerModule, NbSelectModule, NbTabsetModule, NbListModule, NbAccordionModule } from '@nebular/theme'; +import { NbLayoutModule, NbThemeModule, NbTooltipModule, NbSpinnerModule, NbSelectModule, NbTabsetModule, NbAutocompleteModule, NbListModule, NbAccordionModule } from '@nebular/theme'; import { DashboardComponent } from './dashboard/dashboard.component'; + + @NgModule({ declarations: [ AppComponent, @@ -54,6 +56,8 @@ import { DashboardComponent } from './dashboard/dashboard.component'; SearchComponent, StatsComponent, DashboardComponent, + MappingComponent, + ], imports: [ BrowserModule, @@ -79,6 +83,7 @@ import { DashboardComponent } from './dashboard/dashboard.component'; NbSelectModule, NbTabsetModule, NbTooltipModule, + NbAutocompleteModule, NbListModule, NbAccordionModule ], diff --git a/src/app/authentication/services/auth.service.ts b/src/app/authentication/services/auth.service.ts index 13de15a0c805f24eb3e202bba808faecd75c0961..a4ee7ca60067b8f0f8e0333b80286ff41dc68ecb 100644 --- a/src/app/authentication/services/auth.service.ts +++ b/src/app/authentication/services/auth.service.ts @@ -2,23 +2,30 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { TokenStorageService } from './token-storage.service'; -import { map } from "rxjs/operators"; import { AppConfiguration } from 'src/app/AppConfiguration'; -import { env } from 'process'; import { environment } from 'src/environments/environment'; + + const AUTH_API = environment.smartharvesterUrl + '/harvester/auth'; +const FDP_URL = environment.fdpUrl; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; + @Injectable({ providedIn: 'root' }) export class AuthService { - constructor(private http: HttpClient, private tokenService: TokenStorageService, private appConfig:AppConfiguration) { } + + + constructor(private http: HttpClient, private tokenService: TokenStorageService, private appConfig: AppConfiguration) { } + + + login(credentials): Observable<any> { let data: any @@ -41,16 +48,16 @@ export class AuthService { update(user): Observable<any> { return this.http.post(AUTH_API + '/user', { - catalog_id: user.cat_id + catalog_id: user.cat_id }, httpOptions); } - getF2DSAuthToken():Observable<any> { - return this.http.post(this.appConfig.fdpurl+'/tokens', { - email: this.appConfig.fdpemail, - password: this.appConfig.fdppassword - },httpOptions); + getF2DSAuthToken(credentials): Observable<any> { + return this.http.post(FDP_URL + '/tokens', { + email: credentials.email, + password: credentials.password + }, httpOptions); } public isLoggedIn() { @@ -58,6 +65,12 @@ export class AuthService { } public logout() { - this.tokenService.removeToken(); + const config = { + headers: new HttpHeaders({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) + }; + this.tokenService.signOut(); + return this.http.post(`${AUTH_API}/logout`, null, config); } } diff --git a/src/app/authentication/services/token-storage.service.ts b/src/app/authentication/services/token-storage.service.ts index 4acab50b5bb634f83d04e30a93475f6ecebde903..5efd4714424d1a8284fab6fc24ca20d83abb2373 100644 --- a/src/app/authentication/services/token-storage.service.ts +++ b/src/app/authentication/services/token-storage.service.ts @@ -17,6 +17,7 @@ export class TokenStorageService { public saveToken(token: string,f2dsToken:string): void { window.sessionStorage.removeItem(TOKEN_KEY); + window.sessionStorage.removeItem(F2DS_TOKEN_KEY); window.sessionStorage.setItem(TOKEN_KEY, token); window.sessionStorage.setItem(F2DS_TOKEN_KEY, f2dsToken); } @@ -38,6 +39,7 @@ export class TokenStorageService { } public removeToken(){ window.localStorage.removeItem(TOKEN_KEY); + window.sessionStorage.removeItem(F2DS_TOKEN_KEY); } diff --git a/src/app/authentication/signin/signin.component.ts b/src/app/authentication/signin/signin.component.ts index ba61f6ab1d33fe6ad3e2b553331f81ce8f8884e8..63f28e0f3d67f9113cf5d2ad07ab6e31dad7466c 100644 --- a/src/app/authentication/signin/signin.component.ts +++ b/src/app/authentication/signin/signin.component.ts @@ -21,7 +21,7 @@ export class SigninComponent implements OnInit { isLoginFailed = false; errorMessage = ''; showPassword = false; - user: SmartHarvesterUser = new SmartHarvesterUser(); + user: SmartHarvesterUser = new SmartHarvesterUser({}); constructor(private authService: AuthService, private router: Router, private tokenStorage: TokenStorageService) { } @@ -31,27 +31,29 @@ export class SigninComponent implements OnInit { // Uncomment line to avoid re-authentication if token is still valid //this.isLoggedIn = true; } - this.authService.getF2DSAuthToken().subscribe((response:F2DSResponse) => { - if (response) this.f2dsToken = response.token; - console.log("F2ds token is : ", this.f2dsToken) - }) + } //get formControls() { return this.formGroup.controls; } onSubmit(): void { - + this.authService.login(this.user).subscribe( data => { - this.tokenStorage.saveToken(data.accessToken, this.f2dsToken); + this.authService.getF2DSAuthToken(this.user).subscribe((response:F2DSResponse) => { + if (response) this.f2dsToken = response.token; + console.log("F2ds token is : ", this.f2dsToken); + this.tokenStorage.saveToken(data.accessToken, this.f2dsToken); this.tokenStorage.saveUser(data); this.isLoginFailed = false; this.isLoggedIn = true; console.log("User data : ", data) this.router.navigateByUrl('/dashboard'); + }) + //this.reloadPage(); }, err => { diff --git a/src/app/authentication/signup/signup.component.ts b/src/app/authentication/signup/signup.component.ts index b9e374291e8875ecb6f12be49bba73d15f1f3525..8a757692fe84b9f1c0de3813244802f9175a806e 100644 --- a/src/app/authentication/signup/signup.component.ts +++ b/src/app/authentication/signup/signup.component.ts @@ -16,7 +16,7 @@ export class SignupComponent implements OnInit { isLoginFailed = false; errorMessage = ''; showPassword = false; - user: SmartHarvesterUser = new SmartHarvesterUser(); + user: SmartHarvesterUser = new SmartHarvesterUser({}); constructor(private authService: AuthService, private router: Router) { } diff --git a/src/app/dashboard/dashboard.component.html b/src/app/dashboard/dashboard.component.html index 439aa3d7bc10717c951504531f02772aa98d0594..57e29c2f935d4af3b161a13ef27eabf29168c229 100644 --- a/src/app/dashboard/dashboard.component.html +++ b/src/app/dashboard/dashboard.component.html @@ -4,9 +4,10 @@ <a routerLink="/dashboard" routerLinkActive="active"> <img width="80" alt="Angular Logo" src="assets/images/logo.png" /> </a> - + <h3 style="width: 100%;text-align: center;"> <strong></strong></h3> <!--User badge--> + <nb-user style="white-space: pre;" name="{{userData.firstName}}" title="{{userData.lastName}}" @@ -14,7 +15,7 @@ nbContextMenuTag="my-context-menu" badgePosition="right"> </nb-user> - <!--<button routerLink="/login" nbContextMenuPlacement="right" outline nbButton>Login</button>--> + <button (click)="logout()" nbContextMenuPlacement="right" outline nbButton>Logout</button> </nb-layout-header> diff --git a/src/app/dashboard/dashboard.component.scss b/src/app/dashboard/dashboard.component.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..10fd21b25f6b83f3e270adf71cd32da8c888fef6 100644 --- a/src/app/dashboard/dashboard.component.scss +++ b/src/app/dashboard/dashboard.component.scss @@ -0,0 +1,3 @@ +button { + margin-left: 10px; +} \ No newline at end of file diff --git a/src/app/dashboard/dashboard.component.ts b/src/app/dashboard/dashboard.component.ts index ebcd85424f4474da94d9c34927e67d11c4acc4b3..2dab938b9bad67c03d3d0c6a8106b3af998f5bfe 100644 --- a/src/app/dashboard/dashboard.component.ts +++ b/src/app/dashboard/dashboard.component.ts @@ -52,7 +52,7 @@ export class DashboardComponent implements OnInit { link: '/dashboard/simplesearch', pathMatch: 'full' }, - { + /*{ title: 'Settings', icon: 'options-2-outline', children: [ @@ -67,7 +67,7 @@ export class DashboardComponent implements OnInit { pathMatch: 'full' } ], - }, + },*/ ]; constructor(private readonly sidebarService: NbSidebarService, @@ -91,4 +91,11 @@ export class DashboardComponent implements OnInit { this.sidebarService.toggle(true); return false; } + + logout() { + this.authService.logout().subscribe( + value => this.route.navigateByUrl('/auth/signin') + ); + + } } diff --git a/src/app/datasets/datasets.component.ts b/src/app/datasets/datasets.component.ts index ce752ed7cfaf352417fd7fa279e47fc10776f97c..e57dc1e1bfb62bb6be21acb242d8e51f56373325 100644 --- a/src/app/datasets/datasets.component.ts +++ b/src/app/datasets/datasets.component.ts @@ -1,9 +1,10 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, Output } from '@angular/core'; import { AppConfiguration } from '../AppConfiguration'; import { DatasetCrudService } from './services/dataset-crud.service'; import { OpenApi } from '../publishapi/class/openapi'; import { ParameterType } from '../publishapi/class/openapi-enum'; import { OpenApiTag } from '../publishapi/class/openapi-dto'; +import { environment } from 'src/environments/environment.prod'; interface RequestInfo { value?: string; @@ -26,9 +27,10 @@ export class DatasetsComponent implements OnInit { values = new Map<string, Map<string, Map<string, RequestInfo>>>(); previews = new Map<string, any>(); spinners = new Map<string, boolean>(); + FDP_URL = environment.fdpUrl; - itemsdatasets: any; - itemsdataset: any; + + Object = Object; //urlrepo = "dataverse.ird.fr"; @@ -44,7 +46,8 @@ export class DatasetsComponent implements OnInit { } ngOnInit() { - this.itemsdataset = []; + this.dataSetService.itemsDataset = []; + } ngOnChanges() { @@ -119,6 +122,9 @@ export class DatasetsComponent implements OnInit { break; case OpenApiTag.dataset: this.processDataSet(data, pathName, preview); + if (response.status.toString().startsWith('2')) + this.dataSetService.saveDatasets(data); + console.log(this.dataSetService.itemsDataset) break; } }).finally(() => this.spinners.set(pathName, false)); @@ -215,7 +221,7 @@ export class DatasetsComponent implements OnInit { return null; } - listdatasets() { + /*listdatasets() { var myHeaders = new Headers(); myHeaders.append("Content-Type", "Application/json"); var myInit = { method: 'GET', headers: myHeaders }; @@ -296,14 +302,13 @@ export class DatasetsComponent implements OnInit { data = '\@prefix dcat: <http://www.w3.org/ns/dcat#>.\n\ @prefix dct: <http://purl.org/dc/terms/>.\n\ @prefix language: <http://id.loc.gov/vocabulary/iso639-1/>.\n\ - @prefix s: <'+ this.appConfig.fdpurl + '/>.\n\ - @prefix c: <'+ this.appConfig.fdpurl + '/catalog/>.\n\ - \n\ + @prefix s: <'+ this.FDP_URL + '/>.\n\ + @prefix c: <'+ this.FDP_URL+ '/catalog/>.\n\ s:new\n\ a dcat:Dataset, dcat:Resource;\n\ - dct:description '+ description + ';\n\ + dct:description ' + description + ';\n\ dct:hasVersion "1.0";\n\ - dct:isPartOf c:a21f9b06-b7e7-43c0-869d-d81f09053383;\n\ + dct:isPartOf <https://f2ds.eosc-pillar.eu/catalog/afe472f9-2e70-409c-ad26-f174799e7834>;\n\ dct:language language:en;\n\ dct:license <http://rdflicense.appspot.com/rdflicense/cc-by-nc-nd3.0>;\n\ dct:title '+ name + ';\n\ @@ -333,8 +338,8 @@ export class DatasetsComponent implements OnInit { data = '@prefix dcat: <http://www.w3.org/ns/dcat#>.\n\ @prefix dct: <http://purl.org/dc/terms/>.\n\ @prefix language: <http://id.loc.gov/vocabulary/iso639-1/>.\n\ - @prefix ffd: <https://ffds.eosc-pillar.eu/>.\n\ - @prefix d: <https://ffds.eosc-pillar.eu/dataset/>.\n\ + @prefix ffd: <https://f2ds.eosc-pillar.eu/>.\n\ + @prefix d: <https://f2ds.eosc-pillar.eu/dataset/>.\n\ @prefix GVW: <https://dataverse.ird.fr/dataset.xhtml?persistentId=doi:10.23708/GVWCA0#>.\n\ \n\ ffd:new\n\ @@ -354,7 +359,7 @@ export class DatasetsComponent implements OnInit { return this.dataSetService.createDistribution(data); - } + }*/ diff --git a/src/app/datasets/services/dataset-crud.service.ts b/src/app/datasets/services/dataset-crud.service.ts index 3abb6be9e7efcc4028a6335e9c904c24cb4e5fd4..f894912d4c2094d9cf6a5ffcdb29b149d4551f30 100644 --- a/src/app/datasets/services/dataset-crud.service.ts +++ b/src/app/datasets/services/dataset-crud.service.ts @@ -1,47 +1,47 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; import { AppConfiguration } from 'src/app/AppConfiguration'; -import { AuthService } from 'src/app/authentication/services/auth.service'; +import { TokenStorageService } from 'src/app/authentication/services/token-storage.service'; +import { environment } from 'src/environments/environment.prod'; import { ParseXmlService } from '../../services/parse-xml.service'; - +const FDP_URL = environment.fdpUrl; @Injectable({ providedIn: 'root' }) export class DatasetCrudService { - fds2Token: string - public results: string[] = []; - constructor(private http: HttpClient,private appConfig: AppConfiguration, private authService: AuthService, private parserService: ParseXmlService) { } + fds2Token: string = this.sessionStorage.getFDPToken(); + public results: string[] = []; + itemsDataset: Object[] = [] ; + constructor(private http: HttpClient,private appConfig: AppConfiguration, private parserService: ParseXmlService, private sessionStorage: TokenStorageService) { } createDataSet(data:string){ - - this.authService.getF2DSAuthToken().subscribe(data=>{ - this.fds2Token = data.token - }) - if (this.fds2Token) { - const httpOptions = { - headers: new HttpHeaders({ - 'Accept': 'text/turtle', - 'Content-Type': 'text/turtle', - 'Authorization': 'Bearer '+ this.fds2Token - }) - }; - - let resultat = this.http.post(this.appConfig.fdpurl+"/catalog", data, httpOptions ).subscribe( (r)=>{console.log('reponse: ', r)}) ; - if (resultat){ - console.log("resultat: " + JSON.stringify(resultat)); - return JSON.stringify(resultat); + if (this.fds2Token) { + const httpOptions = { + headers: new HttpHeaders({ + 'Accept': 'text/turtle', + 'Content-Type': 'text/turtle', + 'Authorization': 'Bearer '+ this.fds2Token + }) + }; + + this.http.post(FDP_URL +"/dataset", data, httpOptions ).subscribe( r => { + console.log("resultat: " + JSON.stringify(r)); + }, + error => console.error("The repository has not been published: " + error) , + () => console.log("The datasets has benn published") + + ) ; + } - return "The repository has not been published" - } + + } createDistribution(data:string){ - this.authService.getF2DSAuthToken().subscribe(data=>{ - this.fds2Token = data.token - }) if (this.fds2Token) { const httpOptions = { headers: new HttpHeaders({ @@ -51,15 +51,18 @@ export class DatasetCrudService { }) }; - let resultat = this.http.post(this.appConfig.fdpurl+"/distribution", data, httpOptions ).subscribe( (r)=>{console.log('reponse: ', r)}) ; - if (resultat){ - console.log("resultat: " + JSON.stringify(resultat)); + let resultat = this.http.post(FDP_URL +"/distribution", data, httpOptions ).subscribe((r)=>{ + console.log('reponse: ', r); return JSON.stringify(resultat); - } + }) ; } } - + getDatasetsFromApi(url: string, params: string): Observable<any[]> { + const httpOptions = { + headers: new HttpHeaders({'Content-Type': 'Application/json'})}; + return this.http.get<any[]>(`${url}/${params}`, httpOptions); + } findDataSetByTitle(title:string){ @@ -71,13 +74,19 @@ export class DatasetCrudService { this.results = []; data.results.bindings.forEach(element => { this.results.push(element);}); if (this.results[0]) { - console.log(this.results[0]['uri'].value.substring(36,72)); + let exist = true; } }}); return exist } + getLocally<T = any>(path: string): Observable<T> { + return this.http.get<T>(`${path}`); + } + saveDatasets(obj: Object) { + this.itemsDataset.push(obj); + } } diff --git a/src/app/mapping/class/dataset.ts b/src/app/mapping/class/dataset.ts new file mode 100644 index 0000000000000000000000000000000000000000..50e6b8fbe1fc470e6ca8f40ff1b5a359f804da5e --- /dev/null +++ b/src/app/mapping/class/dataset.ts @@ -0,0 +1,11 @@ +export class Dataset { + public name: string; + public identifier: string; + public usageNote: string; +} +export interface TreeNode<T> { + data: T; + children?: TreeNode<T>[]; + expanded?: boolean; +} + diff --git a/src/app/mapping/class/group.ts b/src/app/mapping/class/group.ts new file mode 100644 index 0000000000000000000000000000000000000000..c280f781ae92fa1c33fa86c4f80ac277a4bca9d0 --- /dev/null +++ b/src/app/mapping/class/group.ts @@ -0,0 +1,9 @@ +export class Group { + public name: string; + public children: string[]; + + constructor(name: string, children: string[]) { + this.name = name; + this.children = children; + } +} \ No newline at end of file diff --git a/src/app/mapping/mapping.component.html b/src/app/mapping/mapping.component.html new file mode 100644 index 0000000000000000000000000000000000000000..cd2e7483bd93cd9a74171a0b92fba5880fdf5510 --- /dev/null +++ b/src/app/mapping/mapping.component.html @@ -0,0 +1,93 @@ +<div class="card-row"> + <div class="card-col"> + <nb-card size="giant" [nbSpinner]="loading" nbSpinnerStatus="primary" nbSpinnerSize="large" + nbSpinnerMessage="Loading..."> + <nb-card-header>Dataset metadata</nb-card-header> + <nb-card-body> + <div></div> + <ng-container *ngFor="let dataset of datasetModel; let index = index;trackBy:trackByIndex;"> + <nb-form-field> + <div class="row"> + + <div class="col-4"> + + <nb-form-field> + <input nbInput fullWidth type="text" value="{{dataset.identifier}}" disabled /> + <button nbSuffix nbTooltip="{{dataset.name}}: {{dataset.usageNote}}" + nbTooltipStatus="info" nbButton status="basic" ghost> + <nb-icon [icon]=" 'question-mark-circle-outline' " pack="eva"> + </nb-icon> + </button> + </nb-form-field> + + </div> + <div class="col-8"> + + <nb-form-field> + <input #autoInput #{{dataset.identifier}} fullWidth id="{{dataset.identifier}}" + nbInput (input)="onChange()" placeholder="Enter value" [nbAutocomplete]="auto" + [(ngModel)]="selectedPaths[index]" (focus)="reset()" /> + + <nb-autocomplete #auto> + + <nb-option *ngFor="let option of filteredOptions | async" [value]="option"> + {{ option }} + </nb-option> + + </nb-autocomplete> + </nb-form-field> + </div> + </div> + </nb-form-field> + </ng-container> + </nb-card-body> + </nb-card> + </div> +</div> +<div class="row"> + <div> + <button nbButton (click)="mapDataset()">Check mapping</button> + </div> +</div> +<div class="card-row"> + <div class="card-col"> + <nb-card> + <nb-card-header>Map</nb-card-header> + <nb-card-body> + <nb-list> + + <nb-list-item *ngFor="let data of mappedMetadatas[index] | keyvalue; trackBy:trackByIndex; "> + <div class="row"> + <div class="col-5"><label for="{{data.key}}">{{data.key}}</label></div> + <div class="col-5"> <input nbInput autofocus [ngModel]="data.value" + (ngModelChange)="mappedMetadatas[index].set(data.key, $event)" /> </div> + <div class="col-2"><button nbButton ghost> + <nb-icon icon="trash-2-outline" status="danger" (click)="deleteProperty(data.key)"> + </nb-icon> + </button></div> + </div> + </nb-list-item> + + </nb-list> + <div class="row"> + <button class="button-center" nbButton status="danger" (click)="mappedMetadatas.splice(index, 1); prev()">Delete datset<nb-icon icon="trash-2-outline"> + </nb-icon></button> + </div> + </nb-card-body> + <nb-card-footer> + <div class="row"> + <div *ngIf=" !first "> + <button nbButton (click)=" prev()" [disabled]="index == 0">prev</button> + </div> + + <div *ngIf="!first "> + <button nbButton (click)=" next()" [disabled]="index == mappedMetadatas.length -1">next</button> + </div> + <div *ngIf="!first "> + <button nbButton (click)=" publishDataset()">Publish</button> + </div> + </div> + </nb-card-footer> + </nb-card> + </div> +</div> \ No newline at end of file diff --git a/src/app/mapping/mapping.component.scss b/src/app/mapping/mapping.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..9a0e2643936659799adea87b351a84d6e9554564 --- /dev/null +++ b/src/app/mapping/mapping.component.scss @@ -0,0 +1,56 @@ + + .card-row { + display: flex; + flex: 1 0 calc(100%); + margin: 0 -0.5rem; + } + .row { + display: flex; + flex: 1 0 calc(100%); + margin: 0 -0.5rem; + } + + .card-col { + width: 100%; + flex: calc(50% ); + margin: 0 0.5rem; + } + + .col-5 { + width: 45%; + vertical-align: middle; + margin-right: 10px; + align-items: stretch; + } + .col-2 { + width: 10%; + vertical-align: middle; + margin-right: 10px; + align-items: stretch; + } + .col-4 { + width: 30%; + + margin-right: 10px; + align-items: stretch; + } + + .col-8 { + width: 70%; + + margin: 10 0.5rem; + align-items: stretch; + } + input { + margin: 10px 10px; + + } + + input-basic-disabled-text-color { + color: black; + } + + .button-center{ + vertical-align: middle; + margin: auto + } \ No newline at end of file diff --git a/src/app/mapping/mapping.component.spec.ts b/src/app/mapping/mapping.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd48d2db40d7c3d3a32776fb02f3f6b2805274b9 --- /dev/null +++ b/src/app/mapping/mapping.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MappingComponent } from './mapping.component'; + +describe('MappingComponent', () => { + let component: MappingComponent; + let fixture: ComponentFixture<MappingComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MappingComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MappingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/mapping/mapping.component.ts b/src/app/mapping/mapping.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d3c947af9724afad2de9175ca987156324de79d --- /dev/null +++ b/src/app/mapping/mapping.component.ts @@ -0,0 +1,225 @@ + +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { environment } from 'src/environments/environment.prod'; +import { AppConfiguration } from '../AppConfiguration'; +import { DatasetCrudService } from '../datasets/services/dataset-crud.service'; +import { Dataset } from './class/Dataset'; + +@Component({ + selector: 'app-mapping', + templateUrl: './mapping.component.html', + styleUrls: ['./mapping.component.scss'] +}) +export class MappingComponent implements OnInit { + + + itemsdataset: Object[] = [] + datasetModel: Dataset[]; + filteredOptions: Observable<string[]>; + keys: string[] = []; + keysMap: Map<number, string[]> = new Map(); + selectedPaths: string[]; + mappedMetadatas: Map<string, string>[] = []; + obsMappedMetadatas: Observable<Map<string, string>[]>; + DatasetToPublish: Map<string, string>[]; + index: number = 0 + first: boolean = true; + loading: boolean = false; + FDP_URL = environment.fdpUrl; + @ViewChild('autoInput') input; + @Input() catalogId: any; + + constructor(private appConfig: AppConfiguration, private dataSetService: DatasetCrudService) { } + + + ngOnInit() { + this.dataSetService.getLocally('./assets/dataset.json').subscribe( + dataset => { + this.datasetModel = dataset; + this.selectedPaths = new Array(dataset.length); + }, + error => { + console.error(error); + }, + ); + + this.obsMappedMetadatas = of(this.mappedMetadatas) + this.populatecatalogue(); + } + + + + + populatecatalogue() { + this.itemsdataset = this.dataSetService.itemsDataset; + this.keys = []; + for (let i = 0; i< this.itemsdataset.length; i++){ + if (i === 0) { + this.getKeysFromMetadata(this.itemsdataset[i], ''); + this.keysMap.set(this.itemsdataset.length, this.keys); + this.filteredOptions = of(this.keysMap.get(1)); + } + } + } + + createDataset(item: Object): Map<string, string> { + let mappedMetadata: Map<string, string> = new Map() + for (let i = 0; i < this.selectedPaths.length; i++) { + if (this.selectedPaths[i]) { + let tab = this.selectedPaths[i].split(' : '); + mappedMetadata.set(this.datasetModel[i].identifier, this.getValue(tab, item)); + } + } + + return mappedMetadata; + + } + + + +publishDataset() { + let data: string = ''; + let properties: string = ''; + + this.mappedMetadatas.forEach((value: Map<string, string>, key: number) => { + properties = ""; + value.forEach( (value: string, key: string) => { + + properties += key+ ' "' + value + '";\n'; + }) + + + data = '\@prefix dcat: <http://www.w3.org/ns/dcat#>.\n\ + @prefix dct: <http://purl.org/dc/terms/>.\n\ + @prefix language: <http://id.loc.gov/vocabulary/iso639-1/>.\n\ + @prefix s: <'+ this.FDP_URL + '/>.\n\ + @prefix c: <'+ this.FDP_URL + '/catalog/>.\n\ + \n\ +s:new\n\ +a dcat:Dataset, dcat:Resource;\n\ +dct:isPartOf c:'+ this.catalogId + ';\n' + properties + '.'; + + console.log('data: ' + data); + //this.dataSetService.createDataSet(data); + }) + +} + + + private getValue(tab: string[], item: Object): string { + let obj: Object + for (let i = 0; i < tab.length; i++) { + if (tab.length == 1) { + return item[tab[0]]; + } else { + if (i == 0) { + obj = item[tab[i]] + } else if (i < tab.length - 1 && obj[tab[i]]) { + obj = obj[tab[i]]; + } else { + if (obj[tab[i]]) { + return obj[tab[i]]; + } else { + return 'undefined'; + } + + } + } + } + } + mapDataset() { + this.mappedMetadatas = []; + this.itemsdataset.forEach((dataset: Object) => { + this.mappedMetadatas.push(this.createDataset(dataset)) + }) + console.log(this.mappedMetadatas) + this.first = false; + } + next() { + if (this.index < this.itemsdataset.length) { + this.index += 1; + this.createDataset(this.itemsdataset[this.index]) + console.log('index = ' + this.index); + } + } + prev() { + if(this.index > 0){ + this.index -= 1; + } + + this.createDataset(this.itemsdataset[this.index]) + console.log('index = ' + this.index); + } + + // to delete a property of dcat dataset mapped + deleteProperty(key: string) { + this.mappedMetadatas[this.index].delete(key); + } + + /* function to get recursively all the keys and values from the JSON object */ + getKeysFromMetadata(obj: Object, keyParent: string) { + + Object.keys(obj).forEach(key => { + if (typeof obj[key] === 'object') { + if (Array.isArray(obj[key])) { + obj[key].forEach(e => { + if (typeof e === 'object') { + this.getKeysFromMetadata(e, keyParent + ' : ' + key + ' : ' + obj[key].indexOf(e)); + } else { + this.keys.push(keyParent + ' : ' + key + ' : ' + obj[key].indexOf(e)) + } + }); + } else { + if (keyParent) { + this.getKeysFromMetadata(obj[key], keyParent + ' : ' + key); + } else { + this.getKeysFromMetadata(obj[key], key); + } + } + } else { + if (keyParent) { + this.keys.push(keyParent + ' : ' + key) + } else { + this.keys.push(key) + } + } + }); + + } + + trackByIndex(index: number, obj: any): any { + return index; + } + + /* autocompletion functions */ + + private filter(value: string): string[] { + let filterValue = value.toLowerCase(); + return this.keys.filter(optionValue => optionValue.toLowerCase().includes(filterValue)); + } + + getFilteredOptions(value: string): Observable<string[]> { + return of(value).pipe( + map(filterString => this.filter(filterString)), + ); + } + onChange() { + this.filteredOptions = this.getFilteredOptions(this.input.nativeElement.value); + } + onSelectionChange($event) { + this.filteredOptions = this.getFilteredOptions($event); + } + reset() { + this.filteredOptions = of(this.keys); + } + +} + + + + + + + diff --git a/src/app/publishapi/publishapi.component.html b/src/app/publishapi/publishapi.component.html index 5aa225c9ab5fa4b4a340a8e669edc225a7907f17..1640975f8700e8506157ae348fe3d1385609ab3d 100644 --- a/src/app/publishapi/publishapi.component.html +++ b/src/app/publishapi/publishapi.component.html @@ -4,7 +4,7 @@ <nb-card> <nb-card-body> - <nb-stepper orientation="horizontal" disableStepNavigation> + <nb-stepper orientation="horizontal" disableStepNavigation > <nb-step [label]="labelOne"> <ng-template #labelOne>First step</ng-template> <h4>Describe API</h4> @@ -292,18 +292,19 @@ <h4>Use parameters & launch request</h4> <app-datasets [openApi]="openApi"></app-datasets> <button class="prev-button" nbButton nbStepperPrevious>prev</button> - <button class="next-button" nbButton nbStepperNext>next</button> + <button class="next-button" nbButton nbStepperNext (click)="initLabelThree = true">next</button> </nb-step> - <nb-step [label]="labelThree"> + <nb-step [label]="labelThree" > <ng-template #labelThree>Third step</ng-template> <h4>Map DCAT Schema </h4> <p class="lorem"> Map your metadata schema with DCAT schema - </p> - <button class="prev-button" nbButton nbStepperPrevious>prev</button> - <button class="next-button" nbButton nbStepperNext>next</button> + </p> + <app-mapping *ngIf="initLabelThree" [catalogId]="openApi.info['x-catalog-id']"></app-mapping> + <button class="prev-button" nbButton nbStepperPrevious (click)="initLabelThree = false">prev</button> + <button class="next-button" nbButton disabled nbStepperNext>next</button> </nb-step> - <nb-step [label]="labelFour"> + <!--<nb-step [label]="labelFour"> <ng-template #labelFour>Fourth step</ng-template> <h4>Populate FDP</h4> <p class="lorem"> @@ -311,7 +312,7 @@ </p> <button class="prev-button" nbButton nbStepperPrevious>prev</button> <button class="next-button" nbButton disabled nbStepperNext>next</button> - </nb-step> + </nb-step>--> </nb-stepper> </nb-card-body> -</nb-card> \ No newline at end of file +</nb-card> diff --git a/src/app/publishapi/publishapi.component.ts b/src/app/publishapi/publishapi.component.ts index 085fbae77ac6a09792bfa94df3ef09b5e7ccc0f7..3a3cf88533c5e2cda0d81e39f7b788e212248cfd 100644 --- a/src/app/publishapi/publishapi.component.ts +++ b/src/app/publishapi/publishapi.component.ts @@ -31,6 +31,7 @@ export class PublishApiComponent implements OnInit { httpStatusCodes: string[]; catalogList: { title: string, catId: string, server: string }[] = []; canNext = false; + initLabelThree: boolean = false; ngOnInit(): void { this.openApi = new OpenApi(null, null); diff --git a/src/app/repository/repository.component.ts b/src/app/repository/repository.component.ts index e9c83cc6eaf9154be7b3b2a4c139caa528c8a45d..425df48009699147f8174b18a00ba10f8bc357e7 100644 --- a/src/app/repository/repository.component.ts +++ b/src/app/repository/repository.component.ts @@ -22,6 +22,7 @@ export class RepositoryComponent implements OnInit { public importFile: File; public resultat: any; public persistentUrl: string; + FDP_URL = environment.fdpUrl; Form = new FormGroup({ repotype: new FormControl(), @@ -43,7 +44,7 @@ export class RepositoryComponent implements OnInit { ) { } ngOnInit() { - this.authService.getF2DSAuthToken(); + this.publishService.getPersistentUrl().subscribe({ next: (response) => this.persistentUrl = response.persistentUrl }); @@ -115,7 +116,7 @@ repository:\n\ @prefix dct: <http://purl.org/dc/terms/>.\n\ @prefix foaf: <http://xmlns.com/foaf/0.1/> .\n\ @prefix lang: <http://id.loc.gov/vocabulary/iso639-1/>. \n\ -@prefix fdp: <'+ this.appConfig.fdpurl + '/>.\n\nfdp:new \n\ +@prefix fdp: <'+ this.FDP_URL + '/>.\n\nfdp:new \n\ a dcat:Catalog, dcat:Resource;\n\ dct:description "'+ this.Form.value.repodescription + '";\n\ dct:hasVersion "'+ this.Form.value.repoversion + '";\n\ diff --git a/src/app/repository/services/publish-repository.service.ts b/src/app/repository/services/publish-repository.service.ts index 84dca3fab971fa5be37da7df9f015c656654aca4..934455262d0bb5997b816e053f08d5db3fba28a1 100644 --- a/src/app/repository/services/publish-repository.service.ts +++ b/src/app/repository/services/publish-repository.service.ts @@ -10,11 +10,12 @@ interface PersistentUrlResponse { persistentUrl: string; } - +const FDP_URL = environment.fdpUrl; @Injectable({ providedIn: 'root' }) export class PublishRepositoryService { + fds2Token: string = this.tokenService.getFDPToken(); constructor( private http: HttpClient, @@ -29,17 +30,15 @@ export class PublishRepositoryService { publishRepository(data: string) { - this.authService.getF2DSAuthToken().subscribe(tokenResponse => { - const fds2Token = tokenResponse.token; - if (fds2Token) { + if (this.fds2Token) { const myHeaders = new Headers(); myHeaders.append('Accept', 'text/turtle'); myHeaders.append('Content-Type', 'text/turtle'); - myHeaders.append('Authorization', 'Bearer ' + fds2Token); + myHeaders.append('Authorization', 'Bearer ' + this.fds2Token); const myInit = { method: 'POST', body: data, headers: myHeaders }; - const myRequest = new Request(this.appConfig.fdpurl + "/catalog", myInit); + const myRequest = new Request(FDP_URL + "/catalog", myInit); fetch(myRequest, myInit) .then(response => { @@ -53,7 +52,7 @@ export class PublishRepositoryService { return "The repository has not been published" } - }); + } getPersistentUrl(): Observable<PersistentUrlResponse> { @@ -61,7 +60,7 @@ export class PublishRepositoryService { } async addUserCatalog(catId: string): Promise<any> { - const tokenResponse = this.tokenService.getToken; + const tokenResponse = this.tokenService.getToken(); const user = this.tokenService.getUser(); return this.http.post(this.smartApiUrl + '/user/' + user.email + '/catalogs', catId, diff --git a/src/app/services/catalog.service.ts b/src/app/services/catalog.service.ts index e90209f3980a00dcd6c4c852c0252452ce06198d..f839c4026eccadd187f5d60b0833e056f518e547 100644 --- a/src/app/services/catalog.service.ts +++ b/src/app/services/catalog.service.ts @@ -17,6 +17,8 @@ export interface FdpApiResponseItem { context: string } +const FDP_URL = environment.fdpUrl; + @Injectable({ providedIn: 'root' }) @@ -49,6 +51,6 @@ export class CatalogService { } getCatalogContentById(catId: string) { - return this.http.get<FdpApiResponseItem[]>(this.appConfig.fdpurl + '/catalog/' + catId, this.httpOptionsFDP); + return this.http.get<FdpApiResponseItem[]>(FDP_URL + '/catalog/' + catId, this.httpOptionsFDP); } } diff --git a/src/app/user/model/user.ts b/src/app/user/model/user.ts index ad90ab7478c93d7b71f3932e4229c7b429db32f3..fbd9c6fd1b95733efdc8dc9f81792589561b3275 100644 --- a/src/app/user/model/user.ts +++ b/src/app/user/model/user.ts @@ -6,5 +6,13 @@ export class SmartHarvesterUser { public passwordConfirm?: string; public id?: "5fbfb86b688c8577c7b8aeff" public token?: "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0QGNpbmVzLmZyIiwiaWF0IjoxNjA2ODQzNjkwLCJleHAiOjE2MDY5MzAwOTB9.UcmKMRmIEyNLW_kCEEI83uMuDG3Lgf5BKeAHvOhhjiFsV-8keKXxy5VLyHR4LvX_7vZL9WN_H_49-sLxGFTJyQ" - public tokenType?: "Bearer" + public tokenType?: "Bearer"; + + constructor(params: any) { + Object.assign(this, params); + } + + isAnonyme(): boolean { + return this.email === undefined; + } } diff --git a/src/assets/dataset.json b/src/assets/dataset.json new file mode 100644 index 0000000000000000000000000000000000000000..abaaa625f171594bdc61cf1cd8d456d03c69da9a --- /dev/null +++ b/src/assets/dataset.json @@ -0,0 +1,177 @@ +[ + { + "name": "description", + "identifier": "dct:description", + "usageNote": "Mandatory property. This property contains a free-text account of the Dataset. This property can be repeated for parallel language versions of the description." + }, + { + "name": "title", + "identifier": "dct:title", + "usageNote": "Mandatory property. This property contains a name given to the Dataset. This property can be repeated for parallel language versions of the name." + }, + { + "name": "contact point", + "identifier": "dcat:contactPoint", + "usageNote": "Recommended property.This property contains contact information that can be used for sending comments about the Dataset." + }, + { + "name": "dataset distribution", + "identifier": "dcat:distribution", + "usageNote": "Recommended property. This property links the Dataset to an available Distribution." + }, + { + "name": "keyword/ tag", + "identifier": "dcat:keyword", + "usageNote": "Recommended property. This property contains a keyword or tag describing the Dataset." + }, + { + "name": "publisher", + "identifier": "dct:publisher", + "usageNote": "Recommended property. This property refers to an entity (organisation) responsible for making the Dataset available." + }, + { + "name": "spatial/geographical coverage", + "identifier": "dct:spatial", + "usageNote": "Recommended property. This property refers to a geographic region that is covered by the Dataset. " + }, + { + "name": "theme/category", + "identifier": "dcat:theme", + "usageNote": "Recommended property. This property refers to a category of the Dataset. A Dataset may be associated with multiple themes. Subproperty of dct:subject." + }, + { + "name": "temporal coverage", + "identifier": "dct:temporal", + "usageNote": "Recommended property. This property refers to a temporal period that the Dataset covers." + }, + { + "name": "conforms to", + "identifier": "dct:conformsTo", + "usageNote": "Optional property. This property refers to an implementing rule or other specification." + }, + { + "name": "frequency", + "identifier": "dct:accrualPeriodicity", + "usageNote": "Optional property. This property refers to the frequency at which Dataset is updated." + }, + { + "name": "identifier", + "identifier": "dct:identifier", + "usageNote": "Optional property. This property contains the main identifier for the Dataset, e.g. the URI or other unique identifier in the context of the Catalogue." + }, + { + "name": "landing page", + "identifier": "dcat:landingPage", + "usageNote": "Optional property. This property refers to a web page that provides access to the Dataset, its Distributions and/or additional information." + }, + { + "name": "language", + "identifier": "dct:language", + "usageNote": "Optional property. This property refers to a language of the Dataset. This property can be repeated if there are multiple languages in the Dataset." + }, + { + "name": "other identifier", + "identifier": "adms:identifier", + "usageNote": "Optional property. This property contains the date of formal issuance (e.g., publication) of the Dataset." + }, + { + "name": "release date datele", + "identifier": "dct:issued", + "usageNote": "Optional property. This property contains the date of formal issuance (e.g., publication) of the Dataset." + }, + { + "name": "update/modification date", + "identifier": "dct:modified", + "usageNote": "Optional property. This property contains the most recent date on which the Dataset was changed or modified." + }, + { + "name": "provenance", + "identifier": "dct:provenance", + "usageNote": "Optional property. This property contains a statement about the lineage of a Dataset." + }, + { + "name": "sample", + "identifier": "adms:sample", + "usageNote": "Optional property. This property refers to a sample distribution of the dataset." + }, + { + "name": "source", + "identifier": "dct:source", + "usageNote": "Optional property. This property refers to a related Dataset from which the described Dataset is derived." + }, + { + "name": "type", + "identifier": "dct:type", + "usageNote": "Optional property. This property refers to the type of the Dataset. A controlled vocabulary for the values has not been established." + }, + { + "name": "related resource", + "identifier": "dct:relation", + "usageNote": "Optional property. This property refers to a related resource." + }, + { + "name": "version", + "identifier": "owl:versionInfo", + "usageNote": "Optional property. This property contains a version number or other version designation of the Dataset." + }, + { + "name": "version notes", + "identifier": "adms:versionNotes", + "usageNote": "Optional property. This property contains a description of the differences between this version and a previous version of the Dataset." + }, + { + "name": "is Version Of", + "identifier": "dct:isVersionOf", + "usageNote": "Optional property. This property refers to a related Dataset of which the described Dataset is a version, edition, or adaptation." + }, + { + "name": "has Version", + "identifier": "dct:hasVersion", + "usageNote": "Optional property.This property refers to a related Dataset that is a version, edition, or adaptation of the described Dataset." + }, + { + "name": "documentation", + "identifier": "foaf:page", + "usageNote": "Optional property. This property refers to a page or document about this Dataset." + }, + { + "name": "access rights", + "identifier": "dct:accessRights", + "usageNote": "Optional property. This property refers to information that indicates whether the Dataset is open data, has access restrictions or is not public. A controlled vocabulary with three members (:public, :restricted, :non-public) will be created and maintained by the Publications Office of the EU." + }, + { + "name": "creator", + "identifier": "dct:creator", + "usageNote": "Optional property. This property refers to an entity primarily responsible for making the resource." + }, + { + "name": "qualified attribution", + "identifier": "prov:qualifiedAttribution", + "usageNote": "Optional property. This property refers to the link to an Agent having some form of responsibility for the resource. " + }, + { + "name": "was generated by", + "identifier": "prov:wasGeneratedBy", + "usageNote": "Optional property. This property refers to an activity that generated, or provides the business context for, the creation of the dataset." + }, + { + "name": "temporal resolution", + "identifier": "dcat:temporalResolution", + "usageNote": "This property refers to the minimum time period resolvable in the dataset." + }, + { + "name": "spatial resolution", + "identifier": "dcat:spatialResolutionInMeters", + "usageNote": "Optional property. This property refers to the minimum spatial separation resolvable in a dataset, measured in meters. " + }, + { + "name": "qualified Relation", + "identifier": "dcat:qualifiedRelation", + "usageNote": "Optional property. This property provides a link to a description of a relationship with another resource." + }, + { + "name": "is reference by", + "identifier": "dct:isReferencedBy", + "usageNote": "Optional property. This property refers to a related resource, such as a publication, that references, cites, or otherwise points to the dataset." + } +] \ No newline at end of file