Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/lang/i18n.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,16 @@
"ARASAAChair": "Hair color",
"ARASAACidentifier": "Context",
"ARASAACidentifierPosition": "Context symbol position",
"GLOBALSYMBOLSexpandSynonyms": "Expand with synonyms and concepts",
"GLOBALSYMBOLSsymbolsets": "Symbol sets",
"OPENSYMBOLSrepositories": "Repositories",
"swapSymbols": "Swap symbols",
"gridToSwapSymbols": "Grid to swap symbols",
"searchSymbols": "Search symbols",
"moreResults": "more results",
"searching": "Searching",
"noResultsFound": "No results found",
"refreshSymbols": "Refresh symbols",
"past": "past",
"future": "future",
"classroom": "classroom",
Expand Down
4 changes: 3 additions & 1 deletion src/js/service/pictograms/arasaacService.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,10 @@ arasaacService.getSupportedGrammarLangs = function (translate) {

function getUrl(apiId, options) {
let paramSuffix = '';
// Only use ARASAAC-specific options, ignore options from other providers
const validArasaacOptions = ['plural', 'color', 'action', 'skin', 'hair', 'identifier', 'identifierPosition'];
options.forEach((option) => {
if (option.value !== undefined) {
if (option.value !== undefined && validArasaacOptions.includes(option.name)) {
paramSuffix += `&${option.name}=${encodeURIComponent(option.value)}`;
}
});
Expand Down
147 changes: 127 additions & 20 deletions src/js/service/pictograms/globalSymbolsService.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import $ from '../../externals/jquery.js';
import { util } from '../../util/util';
import { i18nService } from '../i18nService';
import { constants } from '../../util/constants.js';

let API_SUGGEST_URL = 'https://globalsymbols.com/api/v1/concepts/suggest';
let API_LABELS_URL = 'https://globalsymbols.com/api/v1/labels/search';
let API_SYMBOLSET_API = 'https://globalsymbols.com/api/v1/symbolsets';
let AUTHOR_DEFAULT = 'Global Symbols';
let AUTHOR_URL_DEFAULT = 'https://globalsymbols.com/';
Expand All @@ -23,13 +25,28 @@ let _lastRawResultList = null; // flattened list of pictos
let _hasNextChunk = false;
let _symbolsetInfo = null;
let _lastLang = null;
let _lastOptions = null;
let _lastOptionsKey = null;

let searchProviderInfo = {
name: globalSymbolsService.SEARCH_PROVIDER_NAME,
url: 'https://globalsymbols.com/',
service: globalSymbolsService,
searchLangs: [ // see endpoint https://globalsymbols.com/api/docs#!/languages/getV1LanguagesActive - langs.map(lang => lang.iso639_1).filter(lang => !!lang).map(lang => `'${lang}'`).toString()
'en','af','sq','am','ar','an','hy','as','az','ba','eu','bn','bs','br','bg','my','ca','zh','hr','cs','da','dv','nl','et','fo','fj','fi','fr','gl','lg','ka','de','gu','ht','ha','he','hi','hu','is','ig','id','iu','ga','it','ja','kn','ks','kk','km','rw','ky','ko','ku','lo','lv','ln','lt','mk','mg','ms','ml','mt','mi','mr','el','ne','nb','ny','or','pa','ps','fa','pl','pt','ro','rn','ru','sm','sr','sh','sn','sd','si','sk','sl','so','st','es','sw','sv','ty','ta','tt','te','th','bo','ti','to','tn','tr','tk','ug','uk','ur','uz','vi','cy','xh','yo','zu'
],
options: [
{
name: 'expandSynonyms',
type: constants.OPTION_TYPES.BOOLEAN,
value: true // Enable by default since it's core to GlobalSymbols' value
},
{
name: 'symbolsets',
type: constants.OPTION_TYPES.MULTI_SELECT,
value: [],
optionsFunction: getSymbolsetOptions
}
]
};

Expand All @@ -51,7 +68,10 @@ globalSymbolsService.getSearchProviderInfo = function () {
globalSymbolsService.query = function (search, options, searchLang) {
_lastChunkNr = 1;
_hasNextChunk = false;
return queryInternal(search, searchLang);
_lastOptions = options || JSON.parse(JSON.stringify(searchProviderInfo.options || []));
_lastOptionsKey = buildOptionsKey(_lastOptions);
_lastLang = searchLang || null;
return queryInternal(search, _lastOptions, searchLang);
};

/**
Expand All @@ -60,7 +80,7 @@ globalSymbolsService.query = function (search, options, searchLang) {
*/
globalSymbolsService.nextChunk = function () {
_lastChunkNr++;
return queryInternal(_lastSearchTerm, _lastLang, _lastChunkNr, _lastChunkSize);
return queryInternal(_lastSearchTerm, _lastOptions, _lastLang, _lastChunkNr, _lastChunkSize);
};

/**
Expand All @@ -76,43 +96,115 @@ async function getSymbolSetInfos() {
return _symbolsetInfo;
}

async function getSymbolsetOptions() {
let infos = await getSymbolSetInfos() || [];
let options = infos.map(info => ({ value: info.id, label: info.name })).sort((a,b) => (''+a.label).localeCompare(''+b.label));
// Put ARASAAC at the top if present
let idx = options.findIndex(o => /arasaac/i.test(o.label));
if (idx > 0) {
let ar = options.splice(idx,1)[0];
options.unshift(ar);
}
return options;
}

globalSymbolsService.getSymbolsetOptions = getSymbolsetOptions;

async function getSymbolSetInfo(symbolSetId) {
let infos = await getSymbolSetInfos() || [];
return infos.find(info => info.id === symbolSetId) || { license: '' };
}

async function queryInternal(search, lang, chunkNr, chunkSize) {
lang = lang || i18nService.getAppLang();
async function queryInternal(search, options, lang, chunkNr, chunkSize) {
lang = lang || i18nService.getContentLangBase();
chunkSize = chunkSize || _lastChunkSize;
chunkNr = chunkNr || 1;
let queriedElements = [];

// Build a simple key representing options relevant to data retrieval
const currentOptionsKey = buildOptionsKey(options);

return new Promise(async (resolve, reject) => {
if (!search) {
return resolve([]);
}
if (_lastSearchTerm !== search) {
let concepts = await util.fetchJson(`${API_SUGGEST_URL}?query=${encodeURIComponent(search)}&language=${lang}&language_iso_format=639-1`);
if (!concepts) {
reject('no internet');
if (_lastSearchTerm !== search || _lastLang !== lang || _lastOptionsKey !== currentOptionsKey) {
// Determine selected symbolset IDs from options
let selectedSymbolsetIds = [];
if (options && Array.isArray(options)) {
let symOpt = options.find(o => o && o.name === 'symbolsets');
if (symOpt && Array.isArray(symOpt.value)) {
selectedSymbolsetIds = symOpt.value.map(v => typeof v === 'string' ? parseInt(v) : v).filter(v => !isNaN(v));
}
}

// Determine if we should expand synonyms
let expandSynonyms = false;
if (options && Array.isArray(options)) {
let opt = options.find(o => o && o.name === 'expandSynonyms');
expandSynonyms = !!(opt && opt.value);
}
// Flatten pictos from all concepts into a single list
let flattened = [];
if (Array.isArray(concepts)) {
for (let c of concepts) {
if (c && Array.isArray(c.pictos)) {
for (let p of c.pictos) {
if (p && p[globalSymbolsService.PROP_IMAGE_URL]) {
flattened.push({
url: p[globalSymbolsService.PROP_IMAGE_URL],
symbolsetId: p[globalSymbolsService.PROP_SYMBOLSET_ID]

// Always start with labels/search as the base method
let allPictos = new Map(); // dedupe by picto id

// First: Get results from labels/search (always done)
try {
const labels = await util.fetchJson(`${API_LABELS_URL}?query=${encodeURIComponent(search)}&language=${lang}&language_iso_format=639-1`);
if (Array.isArray(labels) && labels.length) {
for (let label of labels) {
if (label && label.picto && label.picto[globalSymbolsService.PROP_IMAGE_URL] && label.picto.id != null) {
if (!allPictos.has(label.picto.id)) {
allPictos.set(label.picto.id, {
id: label.picto.id,
url: label.picto[globalSymbolsService.PROP_IMAGE_URL],
symbolsetId: label.picto[globalSymbolsService.PROP_SYMBOLSET_ID]
});
}
}
}
}
} catch (e) {
// ignore labels failure
}
_lastRawResultList = flattened;
processResultList(flattened);

// Second: If "expand with synonyms" is enabled, also search concepts for broader results
if (expandSynonyms) {
try {
const concepts = await util.fetchJson(`${API_SUGGEST_URL}?query=${encodeURIComponent(search)}&language=${lang}&language_iso_format=639-1`);
if (Array.isArray(concepts)) {
for (let c of concepts) {
if (c && Array.isArray(c.pictos)) {
for (let p of c.pictos) {
if (p && p[globalSymbolsService.PROP_IMAGE_URL] && p.id != null) {
if (!allPictos.has(p.id)) {
allPictos.set(p.id, {
id: p.id,
url: p[globalSymbolsService.PROP_IMAGE_URL],
symbolsetId: p[globalSymbolsService.PROP_SYMBOLSET_ID]
});
}
}
}
}
}
}
} catch (e) {
// ignore concepts failure
}
}

let flattened = Array.from(allPictos.values());

// Apply symbolset filtering if selected
if (selectedSymbolsetIds.length > 0) {
_lastRawResultList = flattened.filter(e => selectedSymbolsetIds.includes(e.symbolsetId));
} else {
_lastRawResultList = flattened;
}
_lastLang = lang;
_lastOptionsKey = currentOptionsKey;
processResultList(_lastRawResultList);
} else {
processResultList(_lastRawResultList || []);
}
Expand Down Expand Up @@ -141,6 +233,7 @@ async function queryInternal(search, lang, chunkNr, chunkSize) {
element.author = author;
element.authorURL = authorURL;
element.searchProviderName = globalSymbolsService.SEARCH_PROVIDER_NAME;
element.searchProviderOptions = JSON.parse(JSON.stringify(options || []));
queriedElements.push(element);
}
}
Expand All @@ -150,5 +243,19 @@ async function queryInternal(search, lang, chunkNr, chunkSize) {
});
}

function buildOptionsKey(options) {
try {
if (!Array.isArray(options)) return '';
let expand = options.find(o => o && o.name === 'expandSynonyms');
let sets = options.find(o => o && o.name === 'symbolsets');
return JSON.stringify({
expand: !!(expand && expand.value),
sets: Array.isArray(sets && sets.value) ? sets.value.slice().sort() : []
});
} catch (e) {
return '';
}
}

export { globalSymbolsService };

65 changes: 55 additions & 10 deletions src/js/service/pictograms/openSymbolsService.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import $ from '../../externals/jquery.js';
import { imageUtil } from '../../util/imageUtil';
import { constants } from '../../util/constants.js';

let QUERY_URL = 'https://www.opensymbols.org/api/v1/symbols/search?q=';
let openSymbolsService = {};
Expand All @@ -17,7 +18,27 @@ let _hasNextChunk = false;
let searchProviderInfo = {
name: openSymbolsService.SEARCH_PROVIDER_NAME,
url: 'https://www.opensymbols.org/',
service: openSymbolsService
service: openSymbolsService,
options: [
{
name: 'repositories',
type: constants.OPTION_TYPES.MULTI_SELECT,
value: [], // Empty array means all repositories selected
options: [
{ value: 'arasaac', label: 'ARASAAC' },
{ value: 'noun-project', label: 'Noun Project' },
{ value: 'twemoji', label: 'Twemoji' },
{ value: 'sclera', label: 'Sclera' },
{ value: 'mulberry', label: 'Mulberry' },
{ value: 'tawasol', label: 'Tawasol' },
{ value: 'icomoon', label: 'IcoMoon' },
{ value: 'icon_archive', label: 'Icon Archive' },
{ value: 'language_craft', label: 'Language Craft' },
{ value: 'coughdrop_symbols', label: 'CoughDrop Symbols' },
{ value: 'word_art', label: 'Word Art' }
]
}
]
};

openSymbolsService.getSearchProviderInfo = function () {
Expand All @@ -40,10 +61,10 @@ openSymbolsService.getSearchProviderInfo = function () {
* element.author ... name of the author of the image
* additional all properties that are received from opensymbols.org API are available: https://www.opensymbols.org/api/v1/symbols/search?q=test
*/
openSymbolsService.query = function (search) {
openSymbolsService.query = function (search, options) {
_lastChunkNr = 1;
_hasNextChunk = false;
return queryInternal(search);
return queryInternal(search, options);
};

/**
Expand All @@ -64,7 +85,17 @@ openSymbolsService.hasNextChunk = function () {
return _hasNextChunk;
};

function queryInternal(search, chunkNr, chunkSize) {
function queryInternal(search, optionsOrChunkNr, chunkSize) {
// Handle backward compatibility - if second param is a number, it's chunkNr
let options, chunkNr;
if (typeof optionsOrChunkNr === 'number') {
chunkNr = optionsOrChunkNr;
options = null;
} else {
options = optionsOrChunkNr;
chunkNr = 1;
}

chunkSize = chunkSize || _lastChunkSize;
chunkNr = chunkNr || 1;
let queriedElements = [];
Expand All @@ -75,29 +106,43 @@ function queryInternal(search, chunkNr, chunkSize) {
if (_lastSearchTerm !== search) {
$.get(QUERY_URL + search, null, function (resultList) {
_lastRawResultList = resultList;
processResultList(resultList);
processResultList(resultList, options);
}).fail(() => {
reject('no internet');
});
} else {
processResultList(_lastRawResultList);
processResultList(_lastRawResultList, options);
}

function processResultList(resultList) {
function processResultList(resultList, options) {
if (!resultList || !resultList.length || resultList.length === 0) {
resultList = [];
}

// Filter by repositories if specified
let filteredList = resultList;
if (options) {
let repositoriesOption = options.find(opt => opt.name === 'repositories');
if (repositoriesOption && repositoriesOption.value && repositoriesOption.value.length > 0) {
// Filter to only include selected repositories
filteredList = resultList.filter(item =>
item.repo_key && repositoriesOption.value.includes(item.repo_key)
);
}
}

let startIndex = chunkNr * chunkSize - chunkSize;
let endIndex = startIndex + chunkSize - 1;
_hasNextChunk = resultList.length > endIndex + 1;
_hasNextChunk = filteredList.length > endIndex + 1;
for (let i = startIndex; i <= endIndex; i++) {
if (resultList[i]) {
if (filteredList[i]) {
let element = {};
let apiElement = JSON.parse(JSON.stringify(resultList[i]));
let apiElement = JSON.parse(JSON.stringify(filteredList[i]));
element.url = apiElement[openSymbolsService.PROP_IMAGE_URL];
element.author = apiElement[openSymbolsService.PROP_AUTHOR];
element.authorURL = apiElement[openSymbolsService.PROP_AUTHOR_URL];
element.searchProviderName = openSymbolsService.SEARCH_PROVIDER_NAME;
element.searchProviderOptions = options;
/*let promise = imageUtil.urlToBase64(element.url);
element.promise = promise;
promise.then((base64) => {
Expand Down
3 changes: 2 additions & 1 deletion src/js/util/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,8 @@ constants.OPTION_TYPES = {
BOOLEAN: 'BOOLEAN',
COLOR: 'COLOR',
SELECT: 'SELECT',
SELECT_COLORS: 'SELECT_COLORS'
SELECT_COLORS: 'SELECT_COLORS',
MULTI_SELECT: 'MULTI_SELECT'
};

constants.ARASAAC_AUTHOR = 'ARASAAC - CC (BY-NC-SA)';
Expand Down
Loading