Skip to content

Commit

Permalink
Merge branch 'mm204-filter-config' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
Nick Stokoe committed Mar 29, 2024
2 parents 5a35916 + 0a57dff commit 0d71539
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 121 deletions.
39 changes: 37 additions & 2 deletions src/map-app/app/map-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { Map } from "./map";
import { MapPresenter } from "./presenter/map";
import { MarkerManager } from "./marker-manager";
import { Config } from "./model/config";
import { DataServices } from "./model/data-services";
import { DataServices, isVocabPropDef } from "./model/data-services";
import { EventBus } from "../eventbus";
import "./map"; // Seems to be needed to prod the leaflet CSS into loading.
import { SidebarPresenter } from "./presenter/sidebar";
import { PhraseBook } from "../localisations";
import { toString as _toString } from '../utils';
import { Action, AppState, PropEquality, StateManager, TextSearch } from "./state-manager";
import { StateChange } from "../undo-stack";
import { Dictionary } from "../common-types";

export class MapUI {
public map?: Map;
Expand All @@ -28,7 +29,12 @@ export class MapUI {
this.labels = this.dataServices.getFunctionalLabels();

const allInitiatives = new Set(dataServices.getAggregatedData().loadedInitiatives);
const initialState = new AppState(allInitiatives, allInitiatives);
const initialFilters = this.mkInitialFilters();

const initialState = AppState.startState(allInitiatives, undefined, initialFilters);

// Set the intial state when constructing the StateManager. This will be the state
// to which a reset returns to.
this.stateManager = new StateManager(initialState, change => this.onStateChange(change));

// This is here to resolve a circular dependency loop - MapUI needs the SidebarView
Expand All @@ -45,6 +51,35 @@ export class MapUI {
EventBus.Directory.initiativeClicked.sub(initiative => this.onInitiativeClickedInSidebar(initiative));
}

// This inspects the config and constructs an appropriate set of
// filters to construct the initial AppState with.
static mkInitialFilters(config: Config): Dictionary<PropEquality> {
const filters: Dictionary<PropEquality> = {};
const filteredFields = config.getFilteredPropDefs();
for(const name in filteredFields) {
const propDef = filteredFields[name];
const filter = propDef.filter;
if (filter != undefined) {
// If we get here, this property should have a default filter
// value set.

// We can only filter Vocab properties (single or multi), so check that.
if (isVocabPropDef(propDef)) {
filters[name] = new PropEquality(
name, filter, propDef.type === 'multi'
);
}
}
}
return filters;
}

// This inspects the MapUI's config and constructs an appropriate
// set of filters to construct the initial AppState with.
private mkInitialFilters() {
return MapUI.mkInitialFilters(this.config);
}

createMap() {
if (this.mapPresenter) return;

Expand Down
115 changes: 88 additions & 27 deletions src/map-app/app/model/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type {
DataServices,
PropDef,
PropDefs,
FieldDefs,
ConfigPropDefs,
} from './data-services';

import type {
Expand Down Expand Up @@ -110,14 +110,16 @@ export interface ReadableConfig {
attr_namespace(): string;
doesDirectoryHaveColours(): boolean;
elem_id(): string;
fields(): FieldDefs;
fields(): ConfigPropDefs; // @deprecated
getPropDefs(): ConfigPropDefs;
getCustomPopup(): InitiativeRenderFunction | undefined;
getDataSources(): AnyDataSource[];
getDefaultLatLng(): Point2d;
getDefaultOpenSidebar(): boolean;
getDialogueSize(): DialogueSize;
getDisableClusteringAtZoom(): number;
getFilterableFields(): string[];
getFilteredPropDefs(): Record<string, PropDef>;
getInitialBounds(): Box2d | undefined;
getLanguage(): string;
getLanguages(): string[];
Expand Down Expand Up @@ -209,7 +211,7 @@ export class ConfigData {
disableClusteringAtZoom: number = 0;
doesDirectoryHaveColours: boolean = false;
elem_id: string = 'map-app';
fields: Dictionary<PropDef | PropDef['type']> = {};
fields?: Dictionary<PropDef | PropDef['type']>; // @deprecated - use propDefs going forwards
filterableFields: string[] = [];
gitcommit: string = '0';
htmlTitle: string = '';
Expand All @@ -226,6 +228,7 @@ export class ConfigData {
minZoom: number = 2;
noLodCache: boolean = true;
mykoMapVersion: string = '0';
propDefs: Dictionary<PropDef | PropDef['type']> = {};
searchedFields: string[] = ['name'];
servicesPath: string = 'services/';
showAboutPanel: boolean = true;
Expand Down Expand Up @@ -469,14 +472,14 @@ export class Config implements ReadableConfig, WritableConfig {

private readonly data: ConfigData;
private readonly configSchemas: ConfigSchemas;
private _fields: PropDefs;

private stringsToPropDefs(fields: ConfigData['fields']): PropDefs {
const propDefEntries = Object.entries(fields ?? {}).map(
([id, field]) => {
if (typeof field === 'string')
return [id, { type: field, from: id }];
return [id, field];
private _propDefs: PropDefs;

private stringsToPropDefs(propDefs: ConfigData['propDefs']): PropDefs {
const propDefEntries = Object.entries(propDefs ?? {}).map(
([id, def]) => {
if (typeof def === 'string')
return [id, { type: def, from: id }];
return [id, def];
}
);
return Object.fromEntries(propDefEntries);
Expand Down Expand Up @@ -571,7 +574,8 @@ export class Config implements ReadableConfig, WritableConfig {
},
fields: {
id: 'fields',
descr: 'Defines extended definitions of new or existing initiative fields',
descr: 'If present, defines extended definitions of extended or existing initiative fields '+
'(deprecated - use propDefs going forward)',
getter: 'fields',
type: types.propDefs,
},
Expand Down Expand Up @@ -687,6 +691,12 @@ export class Config implements ReadableConfig, WritableConfig {
getter: 'getVersionTag',
type: types.string,
},
propDefs: {
id: 'propDefs',
descr: 'Defines extended definitions of extended or existing initiative properties',
getter: 'getPropDefs',
type: types.propDefs,
},
searchedFields: {
id: 'searchedFields',
descr: "A list of fields that are looked at when using the search function. Valid "+
Expand Down Expand Up @@ -771,7 +781,7 @@ export class Config implements ReadableConfig, WritableConfig {
},
vocabularies: {
id: 'vocabularies',
descr: 'Specifies the vocabularies to obtain via SPARQL query for use in `fields`',
descr: 'Specifies the vocabularies to obtain via SPARQL query for use in `propDefs`',
defaultDescr: 'No vocabs are queried if nothing is provided',
getter: 'vocabularies',
type: types.vocabSources,
Expand All @@ -784,9 +794,10 @@ export class Config implements ReadableConfig, WritableConfig {
if (this.data.languages.length === 0)
throw new Error("languages is configured empty, this should not happen");
this.data.languages = this.data.languages.map(validateLang);
this.validateFilterableFields(this.data.filterableFields);

// Expand abbreviated field defs
this._fields = this.stringsToPropDefs(this.data.fields);
this._propDefs = this.stringsToPropDefs(this.data.fields ?? this.data.propDefs);

// Special know-how validations...

Expand Down Expand Up @@ -818,10 +829,10 @@ export class Config implements ReadableConfig, WritableConfig {
}
}

// Make sure the fields all reference a known vocab
// Make sure the propDefs all reference a known vocab
/* FIXME this no longer works - can we check it later?
for(const fieldId in this._fields ?? {}) {
let field = this._fields[fieldId];
for(const fieldId in this._propDefs ?? {}) {
let field = this._propDefs[fieldId];
if (field === undefined)
continue;
if (field.type === 'multi')
Expand Down Expand Up @@ -873,12 +884,12 @@ is essentially an empty object, but that would result in a totally empty map.
For the sake of illustration, here is an example of what you might put
in this parameter for a map with pins which have a \`size\`,
\`description\` and \`address\` field, in addition of the hard-wired
bare minimum fields of \`uri\`, \`name\`, \`lat\` and \`lng\`. The
\`description\` and \`address\` properties, in addition of the hard-wired
bare minimum properties of \`uri\`, \`name\`, \`lat\` and \`lng\`. The
\`size\` field can be one of several pre-defined values - a taxonomy,
also known as a vocabulary. Because of the \`filterableFields\`
attribute, there will be a single drop-down on the search panel for this
narrowing the displayed pins by values of this field.
also known as a vocabulary. Because of the presence of a \`filter\`
attribute of \`size\`, there will be a single drop-down on the search
panel for this narrowing the displayed pins by values of this field.
\`\`\`
import { ConfigData } from "mykomap/app/model/config-schema";
Expand All @@ -887,14 +898,14 @@ import { InitiativeObj } from "mykomap/src/map-app/app/model/initiative";
const config: ConfigData = {
htmlTitle: "Outlets",
fields: {
propDefs: { // the old name for this is 'fields', but deprecated
address: 'value',
size: {
type: 'vocab',
uri: 'sz:',
filter: undefined,
},
},
filterableFields: ["size"],
vocabularies: [
{
type: 'json',
Expand Down Expand Up @@ -927,7 +938,7 @@ This config would need to be accompanied with a \`example.json\` file
defining the vocabs, and a data file \`example.csv\`. Both of these
can be supplied in map project source code in the \`www/\` directory,
or an URL to elsewhere can be supplied. The \`transform\` attribute defines
the mapping from CSV fields to map pin fields.
the mapping from CSV fields to map pin properties.
The vocabs file might look like this, which defines one vocabulary: size,
represented in the config by the abbreviated base URI \`sz:\`. The language
Expand Down Expand Up @@ -1036,8 +1047,13 @@ ${def.descr}
return this.data.elem_id;
}

// @deprecated
fields() {
return this._fields;
return this._propDefs;
}

getPropDefs() {
return this._propDefs;
}

getDataSources(): AnyDataSource[] {
Expand All @@ -1057,9 +1073,38 @@ ${def.descr}
getDisableClusteringAtZoom(): number {
return this.data.disableClusteringAtZoom;
}
getFilterableFields(): string[] {
// @deprecated: use getFilteredFields going forward
getFilterableFields(): string[] {
return this.data.filterableFields;
}
// Gets a dictionary of filtered properties, with the same order as their definition.
//
// Returns a shortlist dictionary of properties which have a filter attribute present
// (even if that is `undefined` or `null`, which still implies there should be a filter,
// just not one set to anything in particular, or one which includes only empty values)
getFilteredPropDefs(): Record<string, PropDef> {
const propDefs = this.getPropDefs();
const filterableFields = this.data.filterableFields;
const filteredFields: Record<string, PropDef> = {};
if (filterableFields.length > 0) {
// Back-compatibility override case: use these properties as filters
for(const name of filterableFields) {
const propDef = propDefs[name];
if (propDef)
filteredFields[name] = propDef;
}
}
else {
// Standard case: look for properties with a filter
for(const name in propDefs) {
const propDef = propDefs[name];
if (propDef != null && 'filter' in propDef) { // note loose null match
filteredFields[name] = propDef;
}
}
}
return filteredFields;
}
getInitialBounds(): Box2d | undefined {
return this.data.initialBounds;
}
Expand Down Expand Up @@ -1145,9 +1190,25 @@ ${def.descr}
setDisableClusteringAtZoom(val: number): void {
this.data.disableClusteringAtZoom = val;
}
// @deprecated: set the `filter` property in field property definitions instead
setFilterableFields(val: string[]): void {
this.validateFilterableFields(val);
this.data.filterableFields = val;
}
validateFilterableFields(val: string[]): void {
// Check that all the filterable fields are property names -
// Something is wrong if not.
const propDefs = this.data.fields ?? this.data.propDefs;
const badFields = val
.filter(name => !propDefs[name]);

if (badFields.length > 0) {
throw new Error(
`setFilterableFields() used with invalid property names: `+
badFields.join(", ")
);
}
}
setHtmlTitle(val: string): void {
this.data.htmlTitle = val;
}
Expand Down
4 changes: 2 additions & 2 deletions src/map-app/app/model/data-aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class DataAggregator extends AggregatedData implements DataConsumer<Initi
this.paramBuilder = this.mkBuilder(vocabs);
this.propIndex = new PropertyIndexer(
this.registeredValues,
config.getFilterableFields(),
Object.keys(config.getFilteredPropDefs()),
new PropDefIndex(this.propDefs,
(uri) => vocabs.getVocab(uri, config.getLanguage()),
labels),
Expand Down Expand Up @@ -217,7 +217,7 @@ export class DataAggregator extends AggregatedData implements DataConsumer<Initi
return buildAny;
}

// This expects complete initiaitives, with multi-valued fields
// This expects complete initiaitives, with multi-valued properties
// expressed as either single values, or an array of multiple
// values. (Single values are historically allowed to make it easier
// to marshall the multiple-record expression of multiple values
Expand Down
Loading

0 comments on commit 0d71539

Please sign in to comment.