tdf#127792 implement UNO chart attribute MajorOrigin

based on the specification in OFFICE-3936 for ODF 1.4:

https://issues.oasis-open.org/browse/OFFICE-3936

Note: import of the embedded chart of the DOCX unit test
document uses also ODF format in the background, testing
also the extension of the native file format.

Follow-up of commit 830e539547c463b932ce643517f880789185032d
(tdf#127393 OOXML chart import: fix X axis position setting
"CrossBetween"). See also commits with "ShiftedCategoryPosition"
in their descriptions.

Change-Id: I9cd278ac0172c0fab7c51d585a65c34a0ad60b82
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/90081
Tested-by: László Németh <nemeth@numbertext.org>
Reviewed-by: László Németh <nemeth@numbertext.org>
diff --git a/chart2/qa/extras/chart2export.cxx b/chart2/qa/extras/chart2export.cxx
index 589225e8..8e2e1a8 100644
--- a/chart2/qa/extras/chart2export.cxx
+++ b/chart2/qa/extras/chart2export.cxx
@@ -133,7 +133,7 @@ public:
    void testCrossBetweenODS();
    void testAxisTitleRotationXLSX();
    void testAxisTitlePositionDOCX();
    void testAxisCrossBetweenXSLX();
    void testAxisCrossBetweenDOCX();
    void testPieChartDataPointExplosionXLSX();
    void testCustomDataLabel();
    void testCustomPositionofDataLabel();
@@ -161,6 +161,7 @@ public:
    void testTdf130225();
    void testTdf126076();
    void testTdf75330();
    void testTdf127792();

    CPPUNIT_TEST_SUITE(Chart2ExportTest);
    CPPUNIT_TEST(testErrorBarXLSX);
@@ -257,7 +258,7 @@ public:
    CPPUNIT_TEST(testCrossBetweenODS);
    CPPUNIT_TEST(testAxisTitleRotationXLSX);
    CPPUNIT_TEST(testAxisTitlePositionDOCX);
    CPPUNIT_TEST(testAxisCrossBetweenXSLX);
    CPPUNIT_TEST(testAxisCrossBetweenDOCX);
    CPPUNIT_TEST(testPieChartDataPointExplosionXLSX);
    CPPUNIT_TEST(testCustomDataLabel);
    CPPUNIT_TEST(testCustomPositionofDataLabel);
@@ -285,6 +286,7 @@ public:
    CPPUNIT_TEST(testTdf130225);
    CPPUNIT_TEST(testTdf126076);
    CPPUNIT_TEST(testTdf75330);
    CPPUNIT_TEST(testTdf127792);

    CPPUNIT_TEST_SUITE_END();

@@ -2105,7 +2107,7 @@ void Chart2ExportTest::testAxisTitlePositionDOCX()
    CPPUNIT_ASSERT_DOUBLES_EQUAL(0.384070199122511, nY, 1e-2);
}

void Chart2ExportTest::testAxisCrossBetweenXSLX()
void Chart2ExportTest::testAxisCrossBetweenDOCX()
{
    load("/chart2/qa/extras/data/odt/", "axis-position.odt");
    xmlDocPtr pXmlDoc = parseExport("word/charts/chart", "Office Open XML Text");
@@ -2634,6 +2636,21 @@ void Chart2ExportTest::testTdf75330()
    }
}

void Chart2ExportTest::testTdf127792()
{
    load("/chart2/qa/extras/data/docx/", "MSO_axis_position.docx");
    {
        xmlDocPtr pXmlDoc = parseExport("word/charts/chart1", "Office Open XML Text");
        CPPUNIT_ASSERT(pXmlDoc);
        assertXPath(pXmlDoc, "/c:chartSpace/c:chart/c:plotArea/c:valAx/c:crossBetween", "val", "between");
    }
    {
        xmlDocPtr pXmlDoc = parseExport("word/charts/chart2", "Office Open XML Text");
        CPPUNIT_ASSERT(pXmlDoc);
        assertXPath(pXmlDoc, "/c:chartSpace/c:chart/c:plotArea/c:valAx/c:crossBetween", "val", "midCat");
    }
}

CPPUNIT_TEST_SUITE_REGISTRATION(Chart2ExportTest);

CPPUNIT_PLUGIN_IMPLEMENT();
diff --git a/chart2/qa/extras/data/docx/MSO_axis_position.docx b/chart2/qa/extras/data/docx/MSO_axis_position.docx
new file mode 100644
index 0000000..a9955b7
--- /dev/null
+++ b/chart2/qa/extras/data/docx/MSO_axis_position.docx
Binary files differ
diff --git a/chart2/source/controller/chartapiwrapper/AxisWrapper.cxx b/chart2/source/controller/chartapiwrapper/AxisWrapper.cxx
index 3516d7a..b0a1d5f 100644
--- a/chart2/source/controller/chartapiwrapper/AxisWrapper.cxx
+++ b/chart2/source/controller/chartapiwrapper/AxisWrapper.cxx
@@ -96,7 +96,8 @@ enum
    PROP_AXIS_GAP_WIDTH,
    PROP_AXIS_DISPLAY_UNITS,
    PROP_AXIS_BUILTINUNIT,
    PROP_AXIS_TRY_STAGGERING_FIRST
    PROP_AXIS_TRY_STAGGERING_FIRST,
    PROP_AXIS_MAJOR_ORIGIN
};

void lcl_AddPropertiesToVector(
@@ -323,6 +324,12 @@ void lcl_AddPropertiesToVector(
                  cppu::UnoType<bool>::get(),
                  beans::PropertyAttribute::BOUND
                  | beans::PropertyAttribute::MAYBEDEFAULT );

    rOutProperties.emplace_back( "MajorOrigin",
                  PROP_AXIS_MAJOR_ORIGIN,
                  cppu::UnoType<double>::get(),
                  beans::PropertyAttribute::BOUND
                  | beans::PropertyAttribute::MAYBEVOID );
}

struct StaticAxisWrapperPropertyArray_Initializer
@@ -616,6 +623,7 @@ std::vector< std::unique_ptr<WrappedProperty> > AxisWrapper::createWrappedProper
    aWrappedProperties.emplace_back( new WrappedLinkNumberFormatProperty );
    aWrappedProperties.emplace_back( new WrappedProperty("StackedText","StackCharacters") );
    aWrappedProperties.emplace_back( new WrappedDirectStateProperty("CrossoverPosition","CrossoverPosition") );
    aWrappedProperties.emplace_back( new WrappedDirectStateProperty("MajorOrigin","MajorOrigin") );
    {
        WrappedGapwidthProperty* pWrappedGapwidthProperty( new WrappedGapwidthProperty( m_spChart2ModelContact ) );
        WrappedBarOverlapProperty* pWrappedBarOverlapProperty( new WrappedBarOverlapProperty( m_spChart2ModelContact ) );
diff --git a/chart2/source/model/main/Axis.cxx b/chart2/source/model/main/Axis.cxx
index b7e19a4..ff42899 100644
--- a/chart2/source/model/main/Axis.cxx
+++ b/chart2/source/model/main/Axis.cxx
@@ -75,7 +75,8 @@ enum
    PROP_AXIS_DISPLAY_UNITS,
    PROP_AXIS_BUILTINUNIT,

    PROP_AXIS_TRY_STAGGERING_FIRST
    PROP_AXIS_TRY_STAGGERING_FIRST,
    PROP_AXIS_MAJOR_ORIGIN
};

void lcl_AddPropertiesToVector(
@@ -161,11 +162,13 @@ void lcl_AddPropertiesToVector(
                  cppu::UnoType<sal_Int32>::get(),
                  beans::PropertyAttribute::BOUND
                  | beans::PropertyAttribute::MAYBEDEFAULT );

    rOutProperties.emplace_back( "MinorTickmarks",
                  PROP_AXIS_MINOR_TICKMARKS,
                  cppu::UnoType<sal_Int32>::get(),
                  beans::PropertyAttribute::BOUND
                  | beans::PropertyAttribute::MAYBEDEFAULT );

    rOutProperties.emplace_back( "MarkPosition",
                  PROP_AXIS_MARK_POSITION,
                  cppu::UnoType<css::chart::ChartAxisMarkPosition>::get(),
@@ -193,6 +196,12 @@ void lcl_AddPropertiesToVector(
                  beans::PropertyAttribute::BOUND
                  | beans::PropertyAttribute::MAYBEDEFAULT );

    rOutProperties.emplace_back( "MajorOrigin",
                  PROP_AXIS_MAJOR_ORIGIN,
                  cppu::UnoType<double>::get(),
                  beans::PropertyAttribute::BOUND
                  | beans::PropertyAttribute::MAYBEVOID );

}

struct StaticAxisDefaults_Initializer
diff --git a/include/xmloff/xmltoken.hxx b/include/xmloff/xmltoken.hxx
index 5892f1e..4b4e416 100644
--- a/include/xmloff/xmltoken.hxx
+++ b/include/xmloff/xmltoken.hxx
@@ -1180,6 +1180,7 @@ namespace xmloff { namespace token {
        XML_MACTION,
        XML_MAIN_ENTRY_STYLE_NAME,
        XML_MAJOR,
        XML_MAJOR_ORIGIN,
        XML_MALIGNGROUP,
        XML_MALIGNMARK,
        XML_MANUAL,
diff --git a/offapi/com/sun/star/chart2/Axis.idl b/offapi/com/sun/star/chart2/Axis.idl
index fc3a0f5..678d9a9 100644
--- a/offapi/com/sun/star/chart2/Axis.idl
+++ b/offapi/com/sun/star/chart2/Axis.idl
@@ -141,6 +141,12 @@ service Axis
     */
    [optional, property] boolean TryStaggeringFirst;

    /** This attribute specifies the shift of the first major tick from the origin.

        @since LibreOffice 7.0
     */
    [optional, property] long MajorOrigin;

};

} ; // chart2
diff --git a/schema/libreoffice/OpenDocument-schema-v1.3+libreoffice.rng b/schema/libreoffice/OpenDocument-schema-v1.3+libreoffice.rng
index 05607e0..7491e0e 100644
--- a/schema/libreoffice/OpenDocument-schema-v1.3+libreoffice.rng
+++ b/schema/libreoffice/OpenDocument-schema-v1.3+libreoffice.rng
@@ -2439,4 +2439,13 @@ xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.
    </rng:optional>
  </rng:define>

  <!-- https://issues.oasis-open.org/browse/OFFICE-3936 -->
  <rng:define name="style-chart-properties-attlist" combine="interleave">
    <rng:optional>
      <rng:attribute name="loext:major-origin">
        <rng:ref name="double"/>
      </rng:attribute>
    </rng:optional>
  </rng:define>

</rng:grammar>
diff --git a/xmloff/source/chart/PropertyMap.hxx b/xmloff/source/chart/PropertyMap.hxx
index aa13299..0b8d52c 100644
--- a/xmloff/source/chart/PropertyMap.hxx
+++ b/xmloff/source/chart/PropertyMap.hxx
@@ -184,6 +184,7 @@ const XMLPropertyMapEntry aXMLChartPropMap[] =
    MAP_SPECIAL( "NumberFormat", STYLE, XML_DATA_STYLE_NAME, XML_TYPE_NUMBER, XML_SCH_CONTEXT_SPECIAL_NUMBER_FORMAT ),
    MAP_ENTRY( "LinkNumberFormatToSource", CHART, XML_LINK_DATA_STYLE_TO_SOURCE, XML_TYPE_BOOL ),
    MAP_ENTRY( "Visible", CHART, XML_VISIBLE, XML_TYPE_BOOL ),
    MAP_ENTRY_ODF_EXT( "MajorOrigin", LO_EXT, XML_MAJOR_ORIGIN, XML_TYPE_DOUBLE ),

    MAP_FULL( "CrossoverPosition", CHART, XML_AXIS_POSITION, XML_SCH_TYPE_AXIS_POSITION|MID_FLAG_MERGE_ATTRIBUTE|MID_FLAG_MULTI_PROPERTY, 0, SvtSaveOptions::ODFVER_012 ),
    MAP_FULL( "CrossoverValue", CHART, XML_AXIS_POSITION, XML_SCH_TYPE_AXIS_POSITION_VALUE|MID_FLAG_MERGE_ATTRIBUTE|MID_FLAG_MULTI_PROPERTY, 0, SvtSaveOptions::ODFVER_012 ),
diff --git a/xmloff/source/chart/SchXMLAxisContext.cxx b/xmloff/source/chart/SchXMLAxisContext.cxx
index 962fec9..577d87b 100644
--- a/xmloff/source/chart/SchXMLAxisContext.cxx
+++ b/xmloff/source/chart/SchXMLAxisContext.cxx
@@ -465,25 +465,6 @@ void SchXMLAxisContext::CreateAxis()
        if( m_bAxisTypeImported )
            m_xAxisProps->setPropertyValue("AxisType", uno::makeAny(m_nAxisType) );

        if( m_aCurrentAxis.eDimension == SCH_XML_AXIS_X )
        {
            bool bIs3DChart = false;
            if( (xDiaProp->getPropertyValue("Dim3D") >>= bIs3DChart) && bIs3DChart )
            {
                OUString sChartType = m_xDiagram->getDiagramType();
                if( sChartType == "com.sun.star.chart.BarDiagram" || sChartType == "com.sun.star.chart.StockDiagram" )
                {
                    Reference< chart2::XAxis > xAxis(lcl_getAxis(GetImport().GetModel(), m_aCurrentAxis.eDimension, m_aCurrentAxis.nAxisIndex));
                    if( xAxis.is() )
                    {
                        chart2::ScaleData aScaleData(xAxis->getScaleData());
                        aScaleData.ShiftedCategoryPosition = true;
                        xAxis->setScaleData(aScaleData);
                    }
                }
            }
        }

        if( !m_aAutoStyleName.isEmpty())
        {
            const SvXMLStylesContext* pStylesCtxt = m_rImportHelper.GetAutoStylesContext();
@@ -579,6 +560,30 @@ void SchXMLAxisContext::CreateAxis()
                }
            }
        }

        if (m_aCurrentAxis.eDimension == SCH_XML_AXIS_X)
        {
            Reference<chart2::XAxis> xAxis(lcl_getAxis(GetImport().GetModel(), m_aCurrentAxis.eDimension, m_aCurrentAxis.nAxisIndex));
            if (xAxis.is())
            {
                chart2::ScaleData aScaleData(xAxis->getScaleData());
                bool bIs3DChart = false;
                double fMajorOrigin = -1;
                OUString sChartType = m_xDiagram->getDiagramType();
                if ((xDiaProp->getPropertyValue("Dim3D") >>= bIs3DChart) && bIs3DChart
                    && (sChartType == "com.sun.star.chart.BarDiagram" || sChartType == "com.sun.star.chart.StockDiagram"))
                {
                    aScaleData.ShiftedCategoryPosition = true;
                    xAxis->setScaleData(aScaleData);
                }
                else if ((m_xAxisProps->getPropertyValue("MajorOrigin") >>= fMajorOrigin)
                        && (rtl::math::approxEqual(fMajorOrigin, 0.0) || rtl::math::approxEqual(fMajorOrigin, 0.5)))
                {
                    aScaleData.ShiftedCategoryPosition = rtl::math::approxEqual(fMajorOrigin, 0.5);
                    xAxis->setScaleData(aScaleData);
                }
            }
        }
    }
}

diff --git a/xmloff/source/chart/SchXMLExport.cxx b/xmloff/source/chart/SchXMLExport.cxx
index 07226e8..5566c0b 100644
--- a/xmloff/source/chart/SchXMLExport.cxx
+++ b/xmloff/source/chart/SchXMLExport.cxx
@@ -201,7 +201,7 @@ public:
    void exportAxis( enum XMLTokenEnum eDimension, enum XMLTokenEnum eAxisName,
                    const Reference< beans::XPropertySet >& rAxisProps, const Reference< chart2::XAxis >& rChart2Axis,
                    const OUString& rCategoriesRanges,
                    bool bHasTitle, bool bHasMajorGrid, bool bHasMinorGrid, bool bExportContent );
                    bool bHasTitle, bool bHasMajorGrid, bool bHasMinorGrid, bool bExportContent, OUString sChartType );
    void exportGrid( const Reference< beans::XPropertySet >& rGridProperties, bool bMajor, bool bExportContent );
    void exportDateScale( const Reference< beans::XPropertySet >& rAxisProps );
    void exportAxisTitle( const Reference< beans::XPropertySet >& rTitleProps, bool bExportContent );
@@ -2290,7 +2290,7 @@ void SchXMLExportHelper_Impl::exportAxis(
    const Reference< chart2::XAxis >& rChart2Axis,
    const OUString& rCategoriesRange,
    bool bHasTitle, bool bHasMajorGrid, bool bHasMinorGrid,
    bool bExportContent )
    bool bExportContent, OUString sChartType )
{
    std::vector< XMLPropertyState > aPropertyStates;
    std::unique_ptr<SvXMLElementExport> pAxis;
@@ -2298,6 +2298,20 @@ void SchXMLExportHelper_Impl::exportAxis(
    // get property states for autostyles
    if( rAxisProps.is() && mxExpPropMapper.is() )
    {
        const SvtSaveOptions::ODFDefaultVersion nCurrentODFVersion(SvtSaveOptions().GetODFDefaultVersion());
        if (nCurrentODFVersion > SvtSaveOptions::ODFVER_012 && eDimension == XML_X)
        {
            chart2::ScaleData aScaleData(rChart2Axis->getScaleData());
            bool bShiftedCatPos = aScaleData.ShiftedCategoryPosition;
            if (sChartType == "com.sun.star.chart.BarDiagram" || sChartType == "com.sun.star.chart.StockDiagram")
            {
                if (!bShiftedCatPos)
                    rAxisProps->setPropertyValue("MajorOrigin", uno::makeAny(0.0));
            }
            else if (bShiftedCatPos)
                rAxisProps->setPropertyValue("MajorOrigin", uno::makeAny(0.5));
        }

        lcl_exportNumberFormat( "NumberFormat", rAxisProps, mrExport );
        aPropertyStates = mxExpPropMapper->Filter( rAxisProps );

@@ -2419,6 +2433,7 @@ void SchXMLExportHelper_Impl::exportAxes(

    OUString aCategoriesRange;
    Reference< chart::XAxisSupplier > xAxisSupp( xDiagram, uno::UNO_QUERY );
    OUString sChartType = xDiagram->getDiagramType();

    // x axis

@@ -2440,7 +2455,7 @@ void SchXMLExportHelper_Impl::exportAxes(
                }
            }
        }
        exportAxis( XML_X, XML_PRIMARY_X, xAxisProps, xNewAxis, aCategoriesRange, bHasXAxisTitle, bHasXAxisMajorGrid, bHasXAxisMinorGrid, bExportContent );
        exportAxis( XML_X, XML_PRIMARY_X, xAxisProps, xNewAxis, aCategoriesRange, bHasXAxisTitle, bHasXAxisMajorGrid, bHasXAxisMinorGrid, bExportContent, sChartType );
        aCategoriesRange.clear();
    }

@@ -2450,7 +2465,7 @@ void SchXMLExportHelper_Impl::exportAxes(
    if( xNewAxis.is() )
    {
        Reference< beans::XPropertySet > xAxisProps( xAxisSupp.is() ? xAxisSupp->getSecondaryAxis(0) : nullptr, uno::UNO_QUERY );
        exportAxis( XML_X, XML_SECONDARY_X, xAxisProps, xNewAxis, aCategoriesRange, bHasSecondaryXAxisTitle, false, false, bExportContent );
        exportAxis( XML_X, XML_SECONDARY_X, xAxisProps, xNewAxis, aCategoriesRange, bHasSecondaryXAxisTitle, false, false, bExportContent, sChartType );
    }

    // y axis
@@ -2459,7 +2474,7 @@ void SchXMLExportHelper_Impl::exportAxes(
    if( xNewAxis.is() )
    {
        Reference< beans::XPropertySet > xAxisProps( xAxisSupp.is() ? xAxisSupp->getAxis(1) : nullptr, uno::UNO_QUERY );
        exportAxis( XML_Y, XML_PRIMARY_Y, xAxisProps, xNewAxis, aCategoriesRange, bHasYAxisTitle, bHasYAxisMajorGrid, bHasYAxisMinorGrid, bExportContent );
        exportAxis( XML_Y, XML_PRIMARY_Y, xAxisProps, xNewAxis, aCategoriesRange, bHasYAxisTitle, bHasYAxisMajorGrid, bHasYAxisMinorGrid, bExportContent, sChartType );
    }

    // secondary y axis
@@ -2468,7 +2483,7 @@ void SchXMLExportHelper_Impl::exportAxes(
    if( xNewAxis.is() )
    {
        Reference< beans::XPropertySet > xAxisProps( xAxisSupp.is() ? xAxisSupp->getSecondaryAxis(1) : nullptr, uno::UNO_QUERY );
        exportAxis( XML_Y, XML_SECONDARY_Y, xAxisProps, xNewAxis, aCategoriesRange, bHasSecondaryYAxisTitle, false, false, bExportContent );
        exportAxis( XML_Y, XML_SECONDARY_Y, xAxisProps, xNewAxis, aCategoriesRange, bHasSecondaryYAxisTitle, false, false, bExportContent, sChartType );
    }

    // z axis
@@ -2477,7 +2492,7 @@ void SchXMLExportHelper_Impl::exportAxes(
    if( xNewAxis.is() )
    {
        Reference< beans::XPropertySet > xAxisProps( xAxisSupp.is() ? xAxisSupp->getAxis(2) : nullptr, uno::UNO_QUERY );
        exportAxis( XML_Z, XML_PRIMARY_Z, xAxisProps, xNewAxis, aCategoriesRange, bHasZAxisTitle, bHasZAxisMajorGrid, bHasZAxisMinorGrid, bExportContent );
        exportAxis( XML_Z, XML_PRIMARY_Z, xAxisProps, xNewAxis, aCategoriesRange, bHasZAxisTitle, bHasZAxisMajorGrid, bHasZAxisMinorGrid, bExportContent, sChartType );
    }
}

diff --git a/xmloff/source/core/xmltoken.cxx b/xmloff/source/core/xmltoken.cxx
index a9cf682..253818c 100644
--- a/xmloff/source/core/xmltoken.cxx
+++ b/xmloff/source/core/xmltoken.cxx
@@ -1185,6 +1185,7 @@ namespace xmloff::token {
        TOKEN( "maction",                         XML_MACTION ),
        TOKEN( "main-entry-style-name",           XML_MAIN_ENTRY_STYLE_NAME ),
        TOKEN( "major",                           XML_MAJOR ),
        TOKEN( "major-origin",                    XML_MAJOR_ORIGIN ),
        TOKEN( "maligngroup",                     XML_MALIGNGROUP ),
        TOKEN( "malignmark",                      XML_MALIGNMARK ),
        TOKEN( "manual",                          XML_MANUAL ),
diff --git a/xmloff/source/token/tokens.txt b/xmloff/source/token/tokens.txt
index cd94bd5..fd3806a 100644
--- a/xmloff/source/token/tokens.txt
+++ b/xmloff/source/token/tokens.txt
@@ -1098,6 +1098,7 @@ macro-name
maction
main-entry-style-name
major
major-origin
maligngroup
malignmark
manual