Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/entities/language/LanguageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export interface LanguageData extends ObjectBase {

latitude?: number;
longitude?: number;
coordsSource?: LanguageSource;
depth?: number; // Computed depth in the language family tree, with 0 being a root language

// References to other objects, filled in after loading the TSV
Expand Down
3 changes: 2 additions & 1 deletion src/features/data/compute/computeRecursiveLanguageData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LanguageData } from '@entities/language/LanguageTypes';
import { LanguageData, LanguageSource } from '@entities/language/LanguageTypes';
import { getVitalityMetascore } from '@entities/language/vitality/LanguageVitalityComputation';

import averageCoordinates from '@shared/lib/averageCoordinates';
Expand Down Expand Up @@ -73,6 +73,7 @@ function computeCoordinates(lang: LanguageData): void {
const { latitude, longitude } = averageCoordinates(children);
lang.latitude = latitude;
lang.longitude = longitude;
if (lang.latitude != null && lang.longitude != null) lang.coordsSource = LanguageSource.Combined;
}

export default computeRecursiveLanguageData;
4 changes: 4 additions & 0 deletions src/features/data/load/extra_entities/GlottologData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
LanguageData,
LanguagesBySource,
LanguageScope,
LanguageSource,
} from '@entities/language/LanguageTypes';
import { setLanguageNames } from '@entities/language/setLanguageNames';

Expand Down Expand Up @@ -130,6 +131,9 @@ export function addGlottologLanguages(
lang.Glottolog.name = name;
lang.latitude = latitude;
lang.longitude = longitude;
if (lang.latitude != null && lang.longitude != null)
lang.coordsSource = LanguageSource.Glottolog;

setLanguageNames(lang);
if (lang.scope == null) {
lang.scope = scope;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';

import Selector from '@features/params/ui/Selector';
import { SelectorDisplay } from '@features/params/ui/SelectorDisplayContext';
import usePageParams from '@features/params/usePageParams';

import { LanguageScope } from '@entities/language/LanguageTypes';

import { getLanguageScopeDescription, getLanguageScopeLabel } from '@strings/LanguageScopeStrings';

const LanguageScopeSelector: React.FC = () => {
type Props = { display?: SelectorDisplay };

const LanguageScopeSelector: React.FC<Props> = ({ display }) => {
const { languageScopes, updatePageParams } = usePageParams();

const selectorDescription =
Expand All @@ -27,6 +30,7 @@ const LanguageScopeSelector: React.FC = () => {
selected={languageScopes}
getOptionLabel={getLanguageScopeLabel}
getOptionDescription={getLanguageScopeDescription}
display={display}
/>
);
};
Expand Down
20 changes: 12 additions & 8 deletions src/widgets/details/sections/LanguageConnections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { useDataContext } from '@features/data/context/useDataContext';
import HoverableButton from '@features/layers/hovercard/HoverableButton';
import HoverableObjectName from '@features/layers/hovercard/HoverableObjectName';
import usePageParams from '@features/params/usePageParams';
import { getScopeFilter } from '@features/transforms/filtering/filter';
import Field from '@features/transforms/fields/Field';
import useFilters from '@features/transforms/filtering/useFilters';
import { getSortFunction } from '@features/transforms/sorting/sort';
import { TreeNodeData } from '@features/treelist/TreeListNode';
import TreeListRoot from '@features/treelist/TreeListRoot';
Expand All @@ -23,7 +24,7 @@ const LanguageConnections: React.FC<{ lang: LanguageData }> = ({ lang }) => {
const { languageSource } = usePageParams();
const { getCLDRLanguage } = useDataContext();
const sortFunction = getSortFunction();
const filterByScope = getScopeFilter();
const filterByScope = useFilters()[Field.TerritoryScope];
const { childLanguages, ISO, Glottolog, variants, equivalentVariant } = lang;
const relatedLanguages = (lang.CLDR.languageMatch ?? [])
.map((match) => ({
Expand Down Expand Up @@ -72,27 +73,30 @@ const LanguageConnections: React.FC<{ lang: LanguageData }> = ({ lang }) => {
</CommaSeparated>
</DetailsField>
)}
<DetailsField title="Descendant Languages">
<DetailsField title="Constituents">
<TreeOrList
treeNodes={
childLanguages.length > 0
? getLanguageTreeNodes([lang], languageSource, sortFunction)
: []
}
listNodes={childLanguages.map((l) => (
listNodes={[...childLanguages].sort(sortFunction).map((l) => (
<HoverableObjectName key={l.ID} object={l} />
))}
emptyMessage="No languages come from this language."
emptyMessage={`${lang.nameDisplay} has no constituent languages or dialects.`}
/>
</DetailsField>
<DetailsField title="Locales">
<TreeOrList
treeNodes={
lang.locales.length > 0 ? getLocaleTreeNodes([lang], sortFunction, filterByScope) : []
}
listNodes={(lang.locales ?? []).map((v) => (
<HoverableObjectName key={v.ID} object={v} />
))}
listNodes={(lang.locales ?? [])
.filter(filterByScope)
.sort(sortFunction)
.map((v) => (
<HoverableObjectName key={v.ID} object={v} />
))}
emptyMessage="No locales available for this language."
/>
</DetailsField>
Expand Down
111 changes: 74 additions & 37 deletions src/widgets/details/sections/LanguageLocation.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react';
import { CheckSquare2Icon, SquareArrowUpLeftIcon, SquareIcon } from 'lucide-react';
import React, { useMemo } from 'react';

import HoverableButton from '@features/layers/hovercard/HoverableButton';
import ObjectMap from '@features/map/EntityMap';
import LocalParamsProvider from '@features/params/LocalParamsProvider';
import { ObjectType, PageParamsOptional, View } from '@features/params/PageParamTypes';
import { SelectorDisplay } from '@features/params/ui/SelectorDisplayContext';
import usePageParams from '@features/params/usePageParams';
import Field from '@features/transforms/fields/Field';
import LanguageScopeSelector from '@features/transforms/filtering/selectors/LanguageScopeSelector';
import useFilteredEntities from '@features/transforms/filtering/useFilteredEntities';

import { LanguageData } from '@entities/language/LanguageTypes';
import { LanguageData, LanguageSource } from '@entities/language/LanguageTypes';

import DetailsField from '@shared/containers/DetailsField';
import DetailsSection from '@shared/containers/DetailsSection';
Expand All @@ -17,17 +20,34 @@ import Pill from '@shared/ui/Pill';

import { getLanguageScopeLabel } from '@strings/LanguageScopeStrings';

const MAP_CIRCLE_LIMIT = 200;
const MAP_CIRCLE_LIMIT = 100;

const LanguageLocation: React.FC<{ lang: LanguageData }> = ({ lang }) => {
const { updatePageParams } = usePageParams();

return (
<DetailsSection title="Location">
<DetailsField title="Coordinates">
{lang.latitude && lang.longitude ? (
{lang.latitude != null && lang.longitude != null ? (
<>
{lang.latitude.toFixed(4)}°, {lang.longitude.toFixed(4)}° <Pill>Glottolog</Pill>
{lang.latitude.toFixed(4)}°, {lang.longitude.toFixed(4)}°{' '}
{lang.coordsSource && <Pill>{lang.coordsSource}</Pill>}
{lang.coordsSource === LanguageSource.Glottolog && (
<>
{' '}
These coordinates represent the &quot;primary&quot; location of the{' '}
{getLanguageScopeLabel(lang.scope).toLowerCase()}. This could be the centroid of the
area where the language is spoken or a significant location such as a major city for
which the language is known.
</>
)}
{lang.coordsSource === LanguageSource.Combined && (
<>
{' '}
These coordinates represent the average location of the constituents of this{' '}
{getLanguageScopeLabel(lang.scope).toLowerCase()}.
</>
)}
</>
) : (
<Deemphasized>
Expand All @@ -43,6 +63,7 @@ const LanguageLocation: React.FC<{ lang: LanguageData }> = ({ lang }) => {
objectType: ObjectType.Language,
languageFilter: lang.nameCanonical + ' [' + lang.ID + ']',
sortBy: Field.Population,
searchString: '',
}}
>
<Maps lang={lang} updatePageParams={updatePageParams} />
Expand All @@ -57,44 +78,60 @@ type MapsProps = {
updatePageParams: (newParams: PageParamsOptional) => void;
};
function Maps({ lang, updatePageParams }: MapsProps) {
const descendants = useFilteredEntities({}).filteredEntities.filter(
(l) => l.type == ObjectType.Language && l.latitude && l.longitude,
const [showConstituents, setShowConstituents] = React.useState(true);
const nodes = useFilteredEntities<LanguageData>({}).filteredEntities; // Use a LocalParamsProvider to limit the visible entities
const drawableNodes = useMemo(
() => [
lang, // always show the selected language
...nodes.filter((l) => l.ID !== lang.ID && l.latitude != null && l.longitude != null),
],
[lang, nodes],
);
if (nodes.length === 0) return null;
return (
<>
{lang.latitude && lang.longitude ? (
<>
These coordinates show the &quot;primary&quot; location of the language, as defined by
Glottolog. This could be the centroid of the area where the language is spoken, or a
significant location such as a major city where the language has a presence. It does not
represent all the locations where the language is spoken.
<ObjectMap entities={[lang]} maxWidth={400} />
</>
) : null}
{descendants.length > 0 && (
<>
<div>
These are the locations of descendant languages/dialects.
{descendants.length > MAP_CIRCLE_LIMIT && (
<div>
This map shows the center of the {getLanguageScopeLabel(lang.scope).toLowerCase()}
{drawableNodes.length > 1 ? ' and its constituents' : ''}. It does not capture every
location that the {getLanguageScopeLabel(lang.scope).toLowerCase()} is used.{' '}
<HoverableButton
style={{ padding: '0.25em', display: 'inline-flex', alignItems: 'center', gap: '0.25em' }}
onClick={() =>
updatePageParams({
view: View.Map,
languageFilter: lang.nameCanonical + ' [' + lang.ID + ']',
})
}
>
<SquareArrowUpLeftIcon size="1em" /> See the full map in the explore panel
</HoverableButton>{' '}
{drawableNodes.length > 1 && (
<HoverableButton
style={{
padding: '0.25em',
display: 'inline-flex',
alignItems: 'center',
gap: '0.25em',
}}
onClick={() => setShowConstituents((prev) => !prev)}
hoverContent={showConstituents ? 'Hide constituents' : 'Show constituents'}
>
{showConstituents ? <CheckSquare2Icon size="1em" /> : <SquareIcon size="1em" />}
{showConstituents ? ' Showing constituents' : ' Not showing constituents'}
{showConstituents && drawableNodes.length > MAP_CIRCLE_LIMIT && (
<Deemphasized>
{' '}
Showing first {MAP_CIRCLE_LIMIT} of {descendants.length}.
(first {MAP_CIRCLE_LIMIT} of {drawableNodes.length})
</Deemphasized>
)}{' '}
<HoverableButton
style={{ padding: '0.25em' }}
onClick={() =>
updatePageParams({
view: View.Map,
languageFilter: lang.nameCanonical + ' [' + lang.ID + ']',
})
}
>
See the full map in explore panel
</HoverableButton>
</div>
<ObjectMap entities={descendants} maxWidth={1000} />
</>
)}
</HoverableButton>
)}
</div>
<div style={{ margin: '.5em 0' }}>
<ObjectMap entities={showConstituents ? drawableNodes : [lang]} maxWidth={1000} />
</div>
{showConstituents && drawableNodes.length > 1 && (
<LanguageScopeSelector display={SelectorDisplay.Dropdown} />
)}
</>
);
Expand Down
Loading